-
-
Notifications
You must be signed in to change notification settings - Fork 388
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft: Add Small Buffer-Optimization for lambda and functor created delegates (C11) #867
Conversation
Review changes with SemanticDiff. |
Forgot to mention that I've also put a discussion on this here: |
98caba4
to
4370952
Compare
template <typename T> | ||
ETL_CONSTEXPR14 object_type(lambda_tag, T&& object) : buffer{0, } { new (&this->buffer[0]) T(etl::forward<T>(object)); } | ||
|
||
~object_type() = default; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this okay to default the destructor when you have new
'd the buffer https://github.com/ETLCPP/etl/pull/867/files#diff-e37e1a7180ece428e4933802e623de4e0e17c620c429fc52653c2ad00487e5b5R749 here?
IIRC you need to call the destructor for T after the placement new is used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, you're right. Forgot that one... but if we add std::is_trivially_destructible
as a guard, then we should be safe, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using std::is_trivially_destructible
would make it impossible for use with the ETL_NO_STL
option. Implementing a cross platform etl::is_trivially_destructible
is not simple as it relies on relevant compiler intrinsics being available.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also I would not want the destructor implemented. Currently we use delegates on our project and are able to store them in ROM. Once it's not trivially destructible, that goes away which would ruin a lot of our data. I think we would be left going back to the bad ol' days of straight function pointers :(
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that the default assignment makes it trivially destructible, or at least is what I thought when I was inspired by the following blog post: https://akrzemi1.wordpress.com/2012/12/13/constexpr-unions/
I think the main problem I have with this is that its usefulness is limited to functors and lambdas that fit in the space and alignment of a |
Hi @jwellbelove, I understand your concern about its usefulness, also I am beginning to doubt if it is possible to retrofit this functionality because it's a breaking change and relies on STL functionality. However, there are two main reasons why I started to tinker with this change of delegates:
It is not my intention to rant, I think ETLCPP is one of the best things that has happened to embedded C++ and is a reason why I think C++ is possible to use in an embedded environment, but I want to make a point that it's easy to misuse delegates. |
I'm just wondering whether a lambda wrapped in a singleton could be a solution and use that as the parameter type instead of a raw functor/lambda. |
We mostly use delegates' compile time construction with objects. Getting rid of it being trivially destructible removes all our delegates from ROM. This would be a major hit to my team and project. |
Hi @drewr95,
I hope my PR doesn't break that behavior. I believe I've only changed the dynamic construction and not the static ones. But I may be wrong. Are you talking about lambda/functor that is compile-time constructed? |
Hey @devjoa I think your PR will break that behavior. Even though you did not mess with the static construction, you cannot put anything in ROM that is not trivially destructible. I just had this issue with etl::variant (see #859). When I commented earlier about you needing to call the |
@drewr95 , to my understanding, after reading #859, this will only happen if I add a non-trivial destructor to the delegate. My suggestion was to add a precondition that lambda and functors must be trivially destructible, otherwise, they must be assigned via a reference_wrapper construction that is always trivially destructible. The capture size will be very small and therefore the only practical capture is to catch a reference |
Thank you, that's much appreciated. |
Yes, I've tried to find way of making it less verbose, but I've not found one yet that really makes a difference. The use of |
I've had a bit of a play around this morning.
|
@devjoa Thanks for the explanation, I understand now. That was my biggest concern. |
That depends on what you want to achieve. If I'm thinking about something like this:
The |
Yes, my idea was a limited workaround. |
How do you see the |
An alternative idea could be to define lambda buffers within the class.
|
Actually, I've just tried multiple instances of See https://godbolt.org/z/qWrG5osfc Output:
|
You can easily move the
(I can't help thinking there's a flaw in this method somewhere, but I've not seen it yet!) |
I realised the error while laying in bed this morning. |
Thanks for your effort. I will take a look at the suggestion. Though I haven't time until the weekend to look at it closer. |
The main issue to solve is ensuring that the lambda is stored somewhere that is valid for at least as long as the delegate. That means either storage internal to the delegate or external storage (all without using dynamic memory). Fixed internal storage would waste memory for non-capturing lambdas. The only way I can see to make lambdas in delegates fool-proof is to disallow capturing lambdas. |
Changes all delegate_cpp11.h' function templates that uses 'enable_if' SFINAE to be correctly implemented. reference: https://en.cppreference.com/w/cpp/types/enable_if
This commit adds 'Small Buffer Optimization' to functor and lambda functions with small footprint, i.e. the same size as 'void *' or less, and will therefore mimic the `std::function` behavior for these types, instead of being a reference to them as before. This means that the following operations are now allowed to delegate with deferred/out-of-scope calls: ``` etl::delegate<void()> Class::method() { etl::delegate<void()> d {[this](){ this->do_something(); }}; return d; } ``` A caveat is that the old behavior where everything became a reference instead of an object is changed and the user must now use a `reference_wrapper` to gain the same behavior, i.e. using `etl::ref` or `etl::cref`: ``` auto f = [&](){ /* ... */ }; etl::delegate<void()> d { etl::ref(d) }; d(); ``` This change of behavior also matches the C++ Core Guidelines for how to pass parameters [https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#f15-prefer-simple-and-conventional-ways-of-passing-information] BREAKING CHANGE: functors and lambda capture will not be references to any longer.
4370952
to
746a4fa
Compare
Finally weekend, Now I've had a chance to reflect on your suggestions, but none fulfill the functionality I'm searching for. What I'm searching for is a solution to put the However, I haven't yet done any more research into if objects will be allocated to #include <etl/delegate.h>
#include <iostream>
#include <type_traits>
#include <cassert>
struct S
{
S(int i, const char* name_)
: v{i}, x{i}, name{name_}
{
}
void hello() const
{
std::cout << "Hello (" << name << "): " << v << "\n";
}
void world()
{
x += v;
std::cout << "world (" << name << "): " << x << "\n";
}
void operator()() const {
std::cout << ".v=" << v << ", .x=" << x << "\n";
}
private:
int v;
int x;
const char *name;
};
int main()
{
S s1 {1, "s1"};
S s2 {10001, "s2"};
etl::delegate<void(void)> d1 = [&s1](){ s1.hello(); };
etl::delegate<void(void)> d2 = [&s1](){ s1.world(); };
etl::delegate<void(void)> d3 = [&s2](){ s2.hello(); };
etl::delegate<void(void)> d4 = [&s2](){ s2.world(); };
d1();
d3();
d2();
d4();
d2();
d4();
auto mega1 = [&s1, &s2](){ s1.hello(); s2.world(); };
auto mega2 = [&s1, &s2](){ s1.world(); s2.hello(); };
etl::delegate<void(void)> d5 = etl::ref(mega1);
etl::delegate<void(void)> d6 = etl::ref(mega2);
std::cout << "======\n";
d5();
std::cout << "------\n";
d6();
std::cout << "------\n";
d6();
std::cout << "------\n";
d5();
std::cout << "------\n";
d6();
std::cout << "======\n";
etl::delegate<void(void)> d7 = etl::cref(s1);
etl::delegate<void(void)> d8 = etl::cref(s2);
d7();
d8();
std::cout << "======\n";
static_assert(std::is_trivially_destructible_v<decltype(d1)>, "Not compliant to req if fail");
static_assert(std::is_trivially_destructible_v<decltype(d2)>, "Not compliant to req if fail");
static_assert(std::is_trivially_destructible_v<decltype(d3)>, "Not compliant to req if fail");
static_assert(std::is_trivially_destructible_v<decltype(d4)>, "Not compliant to req if fail");
static_assert(std::is_trivially_destructible_v<decltype(d5)>, "Not compliant to req if fail");
static_assert(std::is_trivially_destructible_v<decltype(d6)>, "Not compliant to req if fail");
static_assert(std::is_trivially_destructible_v<decltype(d7)>, "Not compliant to req if fail");
static_assert(std::is_trivially_destructible_v<decltype(d8)>, "Not compliant to req if fail");
static_assert(sizeof(d1) == (sizeof(void*) + sizeof(void(*)(void))), "Not compliant to req if fail");
static_assert(sizeof(d2) == (sizeof(void*) + sizeof(void(*)(void))), "Not compliant to req if fail");
static_assert(sizeof(d3) == (sizeof(void*) + sizeof(void(*)(void))), "Not compliant to req if fail");
static_assert(sizeof(d4) == (sizeof(void*) + sizeof(void(*)(void))), "Not compliant to req if fail");
static_assert(sizeof(d5) == (sizeof(void*) + sizeof(void(*)(void))), "Not compliant to req if fail");
static_assert(sizeof(d6) == (sizeof(void*) + sizeof(void(*)(void))), "Not compliant to req if fail");
static_assert(sizeof(d7) == (sizeof(void*) + sizeof(void(*)(void))), "Not compliant to req if fail");
static_assert(sizeof(d8) == (sizeof(void*) + sizeof(void(*)(void))), "Not compliant to req if fail");
return 0;
} |
If all you're really interested in is achieving functionality you get by using lambdas like
|
You can even make it compile time by moving the instances.
|
Well, my example is intended to show that the PR in its current state doesn't break the memory footprint nor the trivially destructible constraint of the class. It's not a real-world example. I'm well aware of the run-time member function assignment, it's one of the most used ways to assign delegates in my code base because I'm not a big fan of singletons. What I'm now suggesting, but haven't implemented yet in the PR is an alternative way to use small lambda captures, without breaking the current API. I'm thinking about to use something like this: etl::delegate<void(void)> d1 = etl::realize([this](){ /* do something here */ }}; where However, do you see a plausible chance of accepting such PR or do you feel quite satisfied with the current API? If the latter is true, we better close this PR; and if not, I'm glad and open to elaborate further on the topic. |
I'm open to ideas about how to solve this, it's just that I'm just not entirely happy with the solutions so far. Like I said earlier, the issue with internal delegate storage is that it will always be a fixed size, and so either wastes storage or will not be big enough for larger lambdas. Every time the coder creates a lambda with a larger size than the previous max, all of the delegates would have to declare more storage. If this wasn't the ETL then we could simply use dynamically allocated memory, job done! I think that a lambda/functor wrapper of some sort that creates static external storage for the object is the only real way forward (unless someone can come up with a better solution). The lambda/functor API could be modified to accept a reference to an instance of the wrapper. |
Hi @jwellbelove , sorry for the low activity from me for a while. However, I close this PR now as it doesn't match the manifesto of ETL. Cheers for now. 😃 |
Hi,
I've changed the creation of lambda and functor delegates, where I use the area for the pointer to create a Small Buffer-Optimized area instead of a reference to these types. This will be a breaking change, and may also in the worst case be a silent change, as the compiler will not detect some cases, i.e. no compile error.
However, IMHO I think this is a pretty good improvement that will make it a bit easier to use for lambdas, and add a behavior I've been missing for a long time.
This commit adds 'Small Buffer Optimization' to functor and lambda functions with small footprint, i.e. the same size as 'void *' or less, and will therefore mimic the
std::function
behavior for these types, instead of being a reference to them as before.This means that the following operations are now allowed to delegate with deferred/out-of-scope calls:
A caveat is that the old behavior where everything became a reference instead of an object is changed and the user must now use a
reference_wrapper
to gain the same behavior, i.e. usingetl::ref
oretl::cref
:This change of behavior also matches the C++ Core Guidelines for how to pass parameters
[https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#f15-prefer-simple-and-conventional-ways-of-passing-information]
Please review my MR, and I'm pleased to get feedback. I have not done any performance testing so it may add an extra penalty, but when I've elaborated with compiler explorer, it seems to be similar.
Also notice that I haven't changed the
create
functionality of delegates, as I don't know if it should be kept as is...?