entt
entt copied to clipboard
Sketch of a change to save binary size
I was doing some explorations on improving binary size and found this potential change in ENTT. Basically, what's going on here is that the 'placeholder' function-level "magic static" is more expensive than it may appear to be at first.
I analyzed a large game using ENTT, by using the SizeBench tool. Each 'placeholder' creates what is called an "atexit destructor" that ends up generating code that is unnecessary. When this magic static is first encountered, it gets constructed (thread-safe, too) which is a rad property of magic statics - but...who is responsible for destructing it? And when does that happen? That's what an atexit destructor does - it's code generated to run the destructor of the magic static (if it was ever constructed), and it runs when the module is unloaded or when exit() is used.
Each of these 'placeholder' objects is returned as a const reference, so it's never mutated, but it still has to be destructed and that code has to be generated. If, instead, this returns nullptr in the case of not having a pool, the placeholder need not be constructer or destructed.
There's not many callsites using assure that had to be updated to do -> instead of . for the reference-to-pointer switch, and tolerate a null return value.
In a large game I found this to save ~200KB of binary size (almost all in the .text section, or code, a little bit in .pdata which has an entry per function in .text so it's usually correlated, and it saved some .bss in-memory read/write data pages.)
I didn't actually run any tests after this change, and I only tested building and linking with MSVC, so consider this a sketch to see if you want to carry it forward and actually test it and ensure it's ok for all the compilers and platforms ENTT may support.
Hi, first of all: thank you very much!! Really interesting read and overview. I see the point of the change too. I don't think we can merge it as-is because the empty and const placeholders are used to construct views when their lazily initialized pools aren't available yet. The other option around is to make the internal map mutable but it also gives problems (it did and likely in the same game). Another alternative would be to make views use null pointer but this also introduces lots of checks all around and it might have an impact on the performance. I need to look into this a little more to see if I can find a different approach that makes me have the cake and eat it. 🤔
How efficient would be calling an appropriate function through a pointer for "doing the right thing" depending on whether the storage exists or not? Or turning view in to a kind of a proxy that hides an empty view or an actual view depending on the existence of the storage?
"doing the right thing"
I guess it depends on what doing the right thing means? 😅
a proxy that hides an empty view or an actual view depending on the existence of the storage
I think this is slightly more complicated due to the templated nature of the view. Unless you've something in mind already that I can't see.
"doing the right thing"
I guess it depends on what doing the right thing means?
"The right thing" is whatever is done now when there's no storage available.
a proxy that hides an empty view or an actual view depending on the existence of the storage
I think this is slightly more complicated due to the templated nature of the view. Unless you've something in mind already that I can't see.
An empty_view class for no storage situation and actual_view that is same as the current basic_view for existing storage. Both can be inherited from a common base and then you have the whole polymorphic stuff. The great downside of this solution is that you have a huge amount of code to support. The other version with just choosing an appropriate handler function and calling it through a pointer is way shorter.
This is finally available on branch wip where the static storage in the const assure is gone. 👍