[HN Gopher] A not-so-quick introduction to the C++ allocator model ___________________________________________________________________ A not-so-quick introduction to the C++ allocator model Author : jandeboevrie Score : 113 points Date : 2023-06-04 14:22 UTC (8 hours ago) (HTM) web link (quuxplusone.github.io) (TXT) w3m dump (quuxplusone.github.io) | MathMonkeyMan wrote: | > You must go far out of your way, at Bloomberg, to encounter a | container that works the same way as in the ordinary C++ STL. | | You need only disable the "bsl overrides std" build option. | Whether you can do that is another question. | cmrdporcupine wrote: | Have to admit to skimming -- this is complicated, but neat! -- | but I also have to say lack of pluggable per-container allocators | in Rust is perhaps my chief complaints at this point; really miss | this from the C++ STL, even if this article does point out how it | can get snakey and foot-gunny. | | I don't pay attention much the process or politics, but | allocator_api feels like it is stuck in permanent limbo. | https://github.com/rust-lang/rust/issues/32838 -- opened 6 years | ago, still pending. | rwmj wrote: | How do you update pointers when relocating the object? I assume | this must be combined with some rust-ish assertions about the | number of references to the object? Or I suppose it's C++ so you | just have to do it right or you get to keep the pieces. | monocasa wrote: | > Or I suppose it's C++ so you just have to do it right or you | get to keep the pieces. | | Yep. One reason why giving out pointers/references all | willynilly to items in something like a std::vector can be | fraught with peril. If you push to that vector, it could | trigger a realloc of all of the previous items, leaving you | with dangling pointers to everything you previously took | references of. | sakras wrote: | I was recently faced with this (luckily I realized it before | coming across any strange bugs), and realized std::deque | works great for this if you don't need random access (which | was fine for my use-case because I just grabbed references | immediately after insertion). | quietbritishjim wrote: | std::deque has O(1) access by index so it's fine for random | access (although it has some extra indirection so it's | still slower than vector) | sakras wrote: | Wow I was literally reading the docs last night and | didn't notice operator[]! Good to know | CoastalCoder wrote: | An occurrence of this bug caused a lot of pain for one of my | previous employers. | | One way we tried to isolate the bug was creating a | replacement for std::vector. This vector used an array for | the elements, but capitalized on the x86 page-protection | system: | | - Between every pair of adjacent elements, this vector | allocated a dummy page, marked as un-readable and un- | writeable. Any attempt to access it would immediately trigger | a segfault. This was to detect erroneous attempts to access | some vector element; e.g. punning it to a type larger than | the element's actual type. | | - Any time the vector needed to replace (or just retire) its | internal array, it would _not_ return the array to the free | pool. Instead, it would leave it allocated, but mark _all_ of | its elements as un-readable and un-writeable. This was to | detect usage of stale pointers into the vector. | | I think the team discovered the bug by other means, but I | still think this approach has some merit. | | TL;DR: | | - The basic idea was inspired by Electric Fence. | | - This vector wasn't exactly a drop-in replacement for | std::vector, because (at the time) it seemed like that would | require a _lot_ of additional code. | | - Obviously this wastes a lot of memory. It's only something | one would use in certain debugging situations. | jesse__ wrote: | I love using this trick. It's extremely useful for having | relatively high confidence you don't have memory bugs | yakubin wrote: | At that point you might as well use a dynamic array which | doesn't invalidate old pointers, when growing the internal | buffer: <https://yakubin.com/notes/comp/reserve-and- | commit.html>. It also exhibits much better space | utilisation than std::vector, not worse. | | (Sorry about the suboptimal formatting. It appears that web | technologies aren't up-to-par when it comes to this type of | content. I intend to rewrite this post in TeX and add some | graphs in TikZ when I find a spare moment.) | CoastalCoder wrote: | Just to clarify, our approach was to help find the bug | that was misusing an instance of std::vector. | | It was a temporary measure, put in place only until we | found the bug. | sapiogram wrote: | > Between every pair of adjacent elements, this vector | allocated a dummy page, marked as un-readable and un- | writeable. Any attempt to access it would immediately | trigger a segfault. | | I'm struggling to figure this one out. Essentially every | two elements would be stored at the start and end of a | page, respectively? Followed by a dummy page? | | 8KB per pair of elements I guess, hope your vectors weren't | too large :p | CoastalCoder wrote: | Sorry, let me clarify. Suppose you have a vector with 3 | elements, and each individual element fits within a | single memory page. | | Then the array layout is: | | page[0] : dummy page | | page[1] : element 0 | | page[2] : dummy page | | page[3] : element 1 | | page[4] : dummy page | | page[5] : element 2 | | page[6] : dummy page | | IIRC. It's been a while :) | jesse__ wrote: | It's probably between blocks of elements. Like, when the | vec needs to realloc they insert a guard page. This is a | pretty common allocator trick | CyberDildonics wrote: | By the time someone gets to the point where they are keeping | pointers while reallocating the data structure that contains | the objects, they have made a bunch of wrong turns already. | | It is much better to use indices or keys instead of pointers | since that is the interface to the data structure. | dundarious wrote: | Indices, regular or generational. | barbariangrunge wrote: | In game dev, you sometimes avoid holding on to pointers for | more than a single frame or operation because of this. | Especially since many things get created or destroyed over | time, and we want to keep our arrays densely packed to reduce | cache misses. | | We are trying a database like approach for retrieving | references to things in our current project. It adds a small | performance penalty, but it's far from a bottleneck and it's | been nice to work with | doctorpangloss wrote: | Did you ever work on a game development project where you | would have rather done what you did versus using Unity? | | Would you want to work in a way where performance didn't | matter? | | How could you tell the difference between the following two | scenarios: | | - A specific kind of higher performance was important. | | - Higher performance wasn't important: you'd get the same | reviews, sell the same copies, whatever graphics improvements | you got didn't matter, and maybe you even finish your game 2x | sooner because you weren't writing an engine, all else being | equal. But because it was intellectually stimulating to you | to deal with performance, you committed code every day, which | is better than nothing. | sillysaurusx wrote: | Yes. Sorry for the low brow comment, but fuck unity. I | really dislike the fact that if someone wants to become a | gamedev in 2023, their only practical options are to write | everything themselves, learn unity, or learn unreal engine. | I've been putting together an alternative. | https://github.com/shawwn/noh | | It's frankly amazing to work with a production grade engine | that compiles in three minutes. I wouldn't trade it for all | the complexity in unity, regardless of how many extra | copies I'd sell. But I realize I'm in the minority. | | I think it matters to love the tech stack you use, and | gamedev has gone out of fashion mostly because no one loves | any of the stacks. They deal with them, which is different. | elteto wrote: | Raw C or C++ pointers you can't, at least not trivially. You | either have a double pointer masquerading as a single pointer, | or some other fanciness. | | Come to think of it, std::shared_ptr has a control block that | points to the actual data. I think implementations allocate the | control block and the data contiguously for performance | reasons, but you don't really have to. | ksherlock wrote: | There is a std::make_shared( constructor arguments... ) | function which will do one allocation for the control block + | item data. I don't think compilers will do it automatically | (it has other implications over a normal std::shared_ptr) so | you need to opt into it. | Conscat wrote: | `boost::intrusive_ptr` or the proposed `std::retained_ptr` is | required to guarantee that. | maccard wrote: | std::make_shared exists to allocate the control block at the | same time as the memory itself. This has a pretty major | disadvantage, the memory for the shared pointer won't be | deallocated until the last std::weak_ptr has been destroyed, | even if the object has been destroyed. | elteto wrote: | Ah good point. But in exchange you get cache locality of | control block + data and single instruction deref. | maccard wrote: | Indeed. It's a tradeoff, and I personally prefer | std::make_shared. | pjmlp wrote: | And the major advantage that object data by being coupled | with control block reduces fragmentation, and avoids the | error of wrongly handling memory leaks when new obj() fails | as parameter to shared pointer construction. | maccard wrote: | Yep, I realise I only talked about the negatives, not the | advantages. In general I prefer to use make_shared and be | careful with uses of weak pointers. | gavinray wrote: | See also the proposal for language-support for scoped | allocations: | | https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p26... | | There are some issues with PMR usage that it hopes to resolve, | for example: struct Aggregate { | std::pmr::string data1; std::pmr::string data2; | std::pmr::string data3; }; | std::pmr::polymorphic_allocator a1; Aggregate ag = | {{"Hello", a1}, {"World", a1}, {"!", a1}}; | std::pmr::vector<Aggregate> va(a1); | va.emplace_back(std::move(ag)); // Correct allocator retained | by moves va.emplace_back(ag); // Error, | copied lvalue has wrong allocator va.resize(5); | // Error, new values have wrong allocator va.resize(1); | // OK, remove all objects with bad allocators | | The proposed syntax is via a "using" keyword: | std::pmr::polymorphic_allocator a4; ScopeAggregate s1 | using a4 {"Hello"}; ___________________________________________________________________ (page generated 2023-06-04 23:00 UTC)