The Horror in the Standard Library
zerotier.com
zerotier.com
Mine was different though, after tracking down a memory leak that was happening with the creation of just new empty string, I discovered in the stdlib that there was a shared pointer to the empty string with a reference count of how many locations were using it (ironic that this was intended to save allocations). It turned out this was on Intel and we had what was rare at the time, a multi-processor system. It turned out that the std::string empty string reference count was just doing a vanilla ++, no locking, nothing, variable not marked volatile, nothing.
A few emails with a guy in Australia, a little inline assembly to call a new atomic increment on the counter, and the bug was fixed. That took two weeks to track down, mostly because it didn't even cross my mind that it wasn't in my code.
From that point on, I realized you can't trust libraries blindly, even one of the most used and broadly adopted ones out there.
There is a corollary to development and debugging. When things break in mysterious ways, we tend to go through a familiar song and dance. As experience, skills and even personal networks grow, we can find ourselves diving ever further in the following chain.
1. "It must be in my code." -- hours of debugging
2. "Okay, it must be somewhere in our codebase." -- days of intense debugging and code spelunking
3. "It HAS TO be in the third party libraries" -- days of issue tracker excavations and never-before-enabled profiling runs
4. "It can't possibly be in stdlib..." -- more of the same, but now profiling the core runtime libraries
5. "Please let this not be a compiler bug" -- you become intensely familiar with mailing list archives
6. "I will not debug drivers. I will not debug drivers. I will not debug drivers."
7. "I don't even know anyone who could help me figure out the kernel innards."
8. "NOBODY understands filesystems!"
9. "What do you mean 'firmware edge case'?"
And the final stage, the one I have witnessed only one person ever achieve:
10. "Where is my chip lab grade oscilloscope?"
Apart from bullheadedness, this chain also highlights another trait of a good developer. Humility.
Hopefully, the number of frameworks at the top, and the size of your individual programs are relatively small (so that 1-3 aren't nightmares by themselves).
In my experience, 4-5 are seldomly the problem (thanks Linaro!). I suspect the ratio of C to C++ is significantly larger in embedded systems though.
In general, PowerPC/MIPS/ARM toolchains and drivers are not as mature as x86/AMD64. 6-8 tend to occur because CPU vendors usually have their own "blessed" toolchains and BSPs that have diverged from their upstream projects. Fortunately, this means that it's often the case that someone else has already fixed the problem. It's just as often that a driver has not been tested for your use-case since the last time that particular driver's infrastructure was refactored inside the kernel. Or... you wrote the driver and made the mistake (or it might be something from 9/10).
9-10 happen because we're often using hardware that is new and has not had all of its errata discovered yet.
When products need to ship, we're regularly going through this stack. I've seen every one of these, even in just the last 4 years.
Once, I used a chunk of ice to cool down a chip, and that made it work. The hardware guys were unimpressed. But hey, they've got cans of Chill and they use them a lot, and this software guy took a while to realize the reason the board worked in the morning and was dead by lunch, and worked for a little while again after lunch, was temperature related.
There were some devs who tracked down a nasty bug in a processor's TLB. I only heard about that one, wish I had been there. I only had to deal with the fallout in the hypervisor. Note: If you have to spend 20ms hunting down and killing lies with all interrupts turned off and everything basically stopped in its tracks, you are no longer a real-time operating system.
My ex-coworker has done the vanilla scope thing too and has a 400MHz scope at home. For some reason people like this are not too uncommon in Finnish oldskool[tm] IT scene. I remember how he isolated a latency and concurrency bug to an expensive interrupt handler. Rewriting isolated parts of core kernel code to make a really tricky problem go away was one of his more hardcore skills.
I'm not even near his level. My own experience is limited to slightly nibbling the edges of file system and block cache behaviour. It's a brave person who dares dive into that code. Not me.
But I do know one person who regularly works with decapped chips. He works for a company who do extremely low-level hardware investigations. Now that's hardcore.
When you find the problem it's usually a blinding flash of realization that illuminates a tiny, eensy bit of code that you tweak and make right in a couple of minutes. Invariably the mistake was pretty stupid. The glory moment is over quickly because you know all the test cases will pass and that you've just nailed another one.
You've got bragging rights during one lunch, but that's it. It's off to more mundane bugs in the mortal world, and you feel a little sad.
I need to do hardware again.
I would try it even before going for some harder software problems, because it's so easy.
I had a local-made spectrum clone, it didn't overheat, but I lost a multimeter on its power supply.
11. "Shit, where do I borrow a spectrum analyzer and a set of near field probes? These things cost an arm and a leg!"
Yes, STM32F1 MCUs generate inference that jams GPS receivers. No, it's not documented anywhere.
if (featureFlags[HN_DEBUG_HIER_FLAGS] = null) {
/* Who won't this trigger!?!?!?
*/
}
...oh god, kill me now.The Arduino software stack is not huge, there is no operating system involved. Our application is the only thing that runs on bare slow hardware with very limited memory. But this also makes debugging harder. The IDE is limited, you debug over serial output. You have to reflash the flash-memory after every re-compile, which can take a minute.
Building a IoT system for very specific tasks that has to run reliable for years without interruption, I would still use a 8-bit tiny ATMega CPU (e.g. Arduino), and to control this tiny CPU and do some networking stuff with a control center using a 32bit ARM CPU (e.g. RPi).
uh, you know that AVRs have debugWIRE (smaller parts) or JTAG (bigger parts)?
(We ended up underclocking the CPU by about 20% because there wasn't enough time for a redesign. Sigh. It's a miracle the thing even worked in the first place...)
An easy, one bit change in configuration registers of SDRAM fixed it. A week well spent!
... it's not widely known, whispers attribute it to a transcription error, unsure when it started, copied through ancient manuscripts, that the Dead Thing that lies dreaming at the bottom of the ocean, is actually named ... C++hulhu
It's a well-known syndrome. The classic motto for it is "'SELECT' isn't broken" https://blog.codinghorror.com/the-first-rule-of-programming-...
12. "Where is my electron microscope?"
[1] http://www.electronicdesign.com/eda/fib-circuit-edit-becomes...
That's a lot of stars to align there, but when they do, hell breaks lose just often enough to be sure it's not completely random, and obviously this hit one of our most finicky customers, and only in production because of course "#{customer}_production".size == 28 (and not 25, because nah that'd have been too easy to be able to reproduce the bug right away).
http://cs.dartmouth.edu/~sergey/cs258/2010/D2T1%20-%20Kris%2...
https://www.mikeash.com/pyblog/friday-qa-2013-06-28-anatomy-...
Basically we had a bug where a whole conditional branch was being skipped, and we traced it down to the branch being omitted entirely from the compiled IR.
And no, it was nothing fancy, just something like:
if (customer.country == "US") {
doSomething();
} else {
doDifferentThing();
}
The whole `else` branch was simply missing from the compiled program.If I remember correctly, we got around it by doing something like:
bool isUsCustomer = customer.country == "US"
if (isUsCustomer) {
doSomething();
} else {
doDifferentThing();
}
Anyway, the point is that the compiler fucked up it's handling of if/else statements, but only at that specific part of the code, leading to a few wasted days of effort tracking down the problem.I'm not finding it right now, but I recall seeing a few weeks ago a presentation with several of these sorts of bugs in a recent version of Java (all reported and fixed): after a number of iterations, it suddenly starts returning wrong results.
It probably was the optimiser at fault, but there wasn't anything special about this conditional, and certainly nothing that _should_ have caused the optimiser to throw away the else branch.
If memory serves right it was comparing a string field of an object to a static string, like `someObject.foo == "some string"`.
It would have to be specific. Put it this way: if this was a general bug and the "else" was always omitted, how long would it last before being found and fixed?
Related, if you were to say to me "I found the issue, the compiler isn't correctly handling if/else statements" Then my first thought would be about your medication not about the compiler.
And yet, it happened :)
And the senior engineers at the company looked at it and confirmed it was a compiler bug. Their best guess was that something about that part of the code was putting the compiler in a funny state, causing it to skip that particular `else` branch.
We reported it to Microsoft, but never heard anything back.
once it led me to discover a leak in a major travel website's purchase flow caused by Java's Thread class, related to thread groups.
most recently, I was writing some Linux auth code in C, reached a point where I could rule out my code, and found a bug in sudo. freaking sudo.
(also related to groups, though the Linux user kind, not Java threads.)
1. "It must be my code" -- minutes of debugging
2. "It must be in our codebase" -- hours of debugging
3. "Third party library or framework" -- If library use a different library, if framework accept the bug and work around it whilst cursing framework choice.
(https://bugs.launchpad.net/ubuntu/+source/linux/+bug/745836)
This got more attention that I thought possible, so I decided to pull it out as an item all by itself.
The whole implementation is smells of silliness, because there is no need to track how many references there are to a global null string, which need not even be dynamically allocated.
For high load backend processing data, you need at least a soft real-time approach: avoid dynamic memory usage at runtime (use dynamic memory just at process start-up or reconfig, and rely on stack allocation for small stuff, when possible).
I wrote a C library with exactly that purpose [1], in order to work with complex data (strings -UTF8, with many string functions for string processing-, vectors, maps, sets, bit sets) on heap or stack memory, with minimum memory fragmentation and suitable for soft/hard real-time requirements.
Had a VERY VERY hard time unlearning and catching up to that "paradigm" but I now have a much better perspective on "automatic memory management" .
>> The problem is forgetting that dynamic memory usage is not "free"
Totally Agree.
The thing that was more interesting (or sad) was to know that the GCC developers didn't expect the multithreaded applications to be long running.
"Operating systems will reclaim allocated memory at program termination anyway. "
[1] https://gcc.gnu.org/onlinedocs/libstdc++/manual/mt_allocator...
Wow. This is worth a Linus Torvalds-level rant. Whoever accepted this code into the source tree needs to be put on GNU's version of a performance improvement plan.
This is also why Valgrind separates reachable and unreachable memory and only considers unreachable memory as leaks.
In any case, hiding this sort of behavior in a way that sucks down days of debugging time on the part of one expert programmer after another, after another, after another, is terrible engineering.
On iOS and android you are expected to free whatever unused memory you can when you are notified of a low memory situation.
So it's not a traditional leak but because memory usage would continue to grow it causes the long lived process to choke itself and die.
[1] And you cannot have more than one heap without mmap. Without mmap, you only have sbrk = the one heap. On UNIX and those that pretend to be, anyway.
> On most operating systems, memory allocated to a program can never be returned to the system. ... Some operating systems (notably, systems that use mmap(2) for allocating large chunks of memory) can reclaim memory that is no longer used ...
Because you can't return unneeded memory to most operating systems (or because it used to be that you couldn't return unneeded memory, even if that has changed recently), it isn't a surprise that by default GCC's free() and operator delete -- which are meant to be cross platform -- don't try to return that memory. Instead it's all free list management.
I do think it's silly for operators new/delete to have a separate free list from malloc()/free().
They don't. This is a custom, simple, non-default pool allocator for standard containers (i.e nothing to do with new)
It's especially silly for a library (programs may want custom memory management and libraries really shouldn't go out of their way to make that harder), and the fact that it's the standard library doesn't make it less silly.
GLIBCPP_FORCE_NEW was the solution for the near term and since I was deploying on Solaris boxes I eventually switched to the Sun Forte C++ compiler.
It really bugs me that this problem still exists. :-/
I used to file lots of bug reports, because I know that as a developer, I'd want them. Every bug report is valuable, even if it's not reproducible. It happened to someone, so it surely also happened to 10 other people who did not report it, it's important.
Unfortunately, bug reports often get responses like "cannot reproduce", "please provide more details which is oh, about a day or two of work". Well, not everyone has a day or two to spend on a bug report, especially if you (like me) hit software bugs regularly.
These days I very rarely file bug reports. It just isn't worth the effort, as most developers do not appreciate the bug report. The dominating perception is that bug reports are annoyances that need to be closed ASAP, and if it isn't easily reproducible, it doesn't exist.
So I'm not surprised that nobody opens bugreports for bugs like the one described by the OP. Not easily reproducible? Rarely occurs? It's highly probable that nobody would care.
BTW, I ask users of my software to please do report bugs. Every bug report is a valuable data point, even if it isn't reproducible. And I do appreciate the effort that it takes just to file a bug report.
… I build all of my C++ projects with Clang and link them against libc++, so I don’t know if I am dodging a very high-caliber bullet (so to speak) or if the other shoe will drop at some point, and I will find myself going down the OP’s rabbit-hole of library-bug investigation.
The bug quality is higher than you might expect, because you have to register with bugzilla, and most bug reports are with development versions, as bugs are shaken out of new features. There are very few bugs in released versions, and where those bugs exist they are often of the form "stupid type where I have redefined & doesn't compile, while the standard technically days it should", where most users would never got them. Wrong answer or crash bugs in releases are extremely rare, although they could be rarer -- the test suite has less coverage than I would personally like.
[0] https://gcc.gnu.org/bugzilla/show_bug.cgi?id=13823
Edited to add the following text.
There is one result[1] for GLIBCXX_FORCE_NEW.
And that's just due to not freeing the pools, not whatever is the complex problem here.
Not identifying this until many hours after symptoms were impacting users sounds like a pretty big monitoring blind spot.
It seems like the libstdc++ maintainers are aware of the issue at least, so that's a start. It'd be nice if some of the mentioned discussions/complaints were linked, though, so we could see what has already been said/done.
And then you realize that it is.
[0]https://gcc.gnu.org/onlinedocs/libstdc++/manual/mt_allocator...
And, to be blunt, "it's never been fixed" should be written as "it's not been fixed yet" -- because you have an opportunity to fix it. That's how free software works after all.
Doesn't the author make that case at the end?
> GNU libstdc++ is broken. This is pretty unforgiveable. [...] Adding wheels to the wheel is sometimes forgiveable when dealing with closed systems that you can't fix but libstdc++ and glibc are both open source GNU projects.
I think there's a disconnect in the author's mind with regards to how free software projects work. If something is broken, you as a user are empowered to fix it. I mean, the author even went through the trouble of reading the libstdc++ code and figuring out what happened inside new (which is likely enough information to write a preliminary patch). At the very least open a bug report about it (or link to an existing one).
Unlike proprietary software, there are many ways to improve free software and ranting online is rarely one of them. I get that this bug screwed them over big time and that they are angry about it. But converting that emotional energy into something useful would help many people other than themselves.
tl;dr: If you find yourself ranting about "why wasn't X fixed before" in a free software project, it might be helpful to realise that you have the opportunity to be the person who fixes it.
There are plenty of things that people would like to change but can't because the maintainers disagree. Not saying that is necessarily bad but you are stupid if you think the answer to everything is "well did you write a patch?".
I'm not sure why this tone is necessary. If someone just rants about a problem without even _trying_ to submit a bug report with a proposed patch that strikes me as laziness not foresight.
As a maintainer myself, I am well aware that maintainers will reject code if it disagrees with our view of a project. But how on earth do you expect us to know there is a problem without reporting a bug (the only reason I asked whether the author wrote a patch is because they went through the trouble of debugging the problem so probably are in a good place to write a patch anyway)? And if you decide to write an angry and ranting blog post rather than interact with us, we aren't going to be very nice to you either.
Which is?
A patch isn't necessary for it to be fixed, but a bug report generally is. A blog post linking to unofficial copies of documentation from 2004 doesn't count.
Have you tested to see if this works better with LLVM libc++?
Have I mentioned lately how much I hate C++?
Great read.
https://gcc.gnu.org/onlinedocs/libstdc++/manual/mt_allocator...
>> Most operators in C++, including its memory allocation and deletion operators, can be overloaded. Indeed this one was.
Okay, well, firstly - the issue here seems to be a problem with the implementation of std::allocator, rather than anything to do with overloading global operator new or delete. Specifically, it sounds like the blog author is talking about one of the GNU libstdc++ extension allocators, like "mt_allocator", which uses thread-local power-of-2 memory pools.[1] These extension allocators are basically drop-in extension implementations of plain std::allocator, and should only really effect the allocation behavior for the STL containers that take Allocator template parameters.
Essentially, libstdc++ tries to provide some flexibility in terms of setting up an allocation strategy for use with STL containers.[2] Basically, in the actual implementation, std::allocator inherits from allocator_base, (a non-standard GNU base class), which can be configured during compilation of libstdc++ to alias one of the extension allocators (like the "mt_allocator" pool allocator, which does not explicitly release memory to the OS, but rather keeps it in a user-space pool until program exit).
However, according to the GNU docs, the default implementation of std::allocator used by libstdc++ is new_allocator [3] - a simple class that the GNU libstdc++ implementation uses to wrap raw calls to global operator new and delete (presumably with no memory pooling.) This allocator is of course often slower than a memory pool, but obviously more predictable in terms of releasing memory back to the OS.
Note also that "mt_allocator" will check if the environment variable GLIBCXX_FORCE_NEW (not GLIBCPP_FORCE_NEW as the author mentions) is set, and if it is, bypass the memory pool and directly use raw ::operator new.
So, it looks like the blog author somehow was getting mt_allocator (or some other multi-threaded pool allocator) as the implementation used by std::allocator, rather than plain old new_allocator. This could have happened if libstdc++ was compiled with the --enable-libstdcxx-allocator=mt flag.
However, apart from explicitly using the mt_allocator as the Allocator parameter with an STL container, or compiling libstdc++ to use it by default, I'm not sure how the blog author is getting a multi-threaded pool allocator implementation of std::allocator by default.
[1] https://gcc.gnu.org/onlinedocs/gcc-4.9.4/libstdc++/manual/ma...
[2] https://gcc.gnu.org/onlinedocs/gcc-4.9.4/libstdc++/manual/ma...
[3] https://gcc.gnu.org/onlinedocs/gcc-4.9.4/libstdc++/manual/ma...
I've searched now the code for GLIBCXX_FORCE_NEW, and it seems it is used in the pool_allocator and the mt_allocator [1].
String uses std::allocator [2].
I agree, the blog entry seems to missing some information to reproduce the issue. It looks to me, that the author was jumping to a conclusion, which confirmed his initial "insight". Not surprising, if you are under pressure and working over the whole weekend and nights. Who hasn't been there.
What bug me, that the standard answer seems quite often, that the whole thing is broken, and we have to switch to a complete different implementation, and/or re-write it from scratch.
[1] https://github.com/gcc-mirror/gcc/search?p=1&q=GLIBCXX_FORCE...
[2] https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-...
Exciting writing, but lacking a point.
I'd be nice if author created repro or actually spotted the bug.
Right now it's just a moaning. Quite probably, he's shooting into his leg himself in one of the numerous odd ways.
"Nothing worked. It's leaking but it's not. It's leaking but the debugger says no memory was lost. It's leaking in ways that are dependent on irrelevant changes to the ordering of mundane operations. This can't be happening."
This is a red flag for heap corruption - or multithreading bugs. (Stack corruption is usually a crash and a wrong stack trace). As it's not trivially reproducible, it's probably a multithreading bug. It's also easy to imagine that OP wrote a scripty-input generator in C++ to run through valgrind which gives single threaded inputs. So running under valgrind it won't be detected. So it's never fun to solve and ever since I changed languages, my multithreaded debugging skills have become a bit rusty. Hey-o!
But it would be good if he amended his post to reduce the vitriol aimed at GCC. It's demonstrably false that GCC's default allocator holds a cache. It makes OP look stupid to people in the know; and makes GCC look bad to people not in the know. No one wins.
Worker A allocates dynamic stuff. Algo take a segment (0+sof(str)(ofA) + n) Work B Allocates to create same kind of data structure (fragment of a JSON) [ofA, OfB] Wk A resume allocating, boundary of [0, ofA] exceeded, no free contiguous space up or down [Ofb, OfC] allocated Wk C enters wants to alloc, but sizeof(string) make it bigger than [0, OfA] so [ofD, ofE] asked .... and the more concurrent workers the more interleaving of memory allocation go on with fragmented memory.
Since malloc are costly the problem known, a complex allocator was created with pools of slab and else, probably having one edge case, very hard to trigger having phD proven really complex heuristic.
CPU power increase, more loads more workers, interleaving comes in, edge case gets triggered.
And C/C++ makes fun of fortran with its fixed size data structures embracing any new arbitrary size arbitrary depth data structure for the convenience of skipping a costly waterfall model before delivering a feature or a change in the data structure and avoiding bike shedding in committees.
Human want to work in a way that is more agile than what computers are under the hood.
Alternative:
Always allocated fixed size memory range for data handling, and make sure it will be enough. When doing REST make sure you have an upper bound, use paging/cursors, which require FSM, have all FP programmers say mutable are bads, sysadmin say that FSM are a pain to handle when HA is required, and CFO saying SLA will not be reached and business model is trashed, and REST fans saying that REST is dead when stateful.
Well REST is a bad idea.
But it’s fixable. In windows, they’ve implemented the solution in Windows XP (opt-in), and in Vista they use it by default:
https://msdn.microsoft.com/en-us/library/windows/desktop/aa3...
Embedded code:
* Missing "volatiles" which allow the optimizer to optimize out "unused" loads and stores to hardware or multitasking shared variables.
* Race conditions (e.g. unsynchronized access to multitasking shared variables). Making the code run slower changes the access pattern, often times obscuring the bug.
The crashing ones at least are easy. Somewhere a list or variable-argument array is missing the NULL terminator...
That's a nice idea, and we've considered "minus minus"ing the ZT core as part of an embedded port. But code like this that shleps a lot of structures around and works with JSON is eye gougingly painful to write in C and the chance of a worse and possibly exploitable memory bug is much higher.
This is the first time we have encountered an actual problem with C++ compilers or runtimes.
https://smartbear.com/product/aqtime-pro/overview/
has an allocation profiler that can be used to track down this sort of problem. You can take allocation snapshots while the application is running to see where the allocations are coming from (provided that you can run AQTime Pro against a binary with debug symbols/info).
I'm not affiliated with the company - just a happy customer that has used them for years with Delphi development.
Great writing, great read.
This isn't particle physics, it's code: we don't have to guess, we can look at it and see how it works.
I ran into this issue once debugging a performance issue with a CAD file parser. It made a lot of copies of large and small chunks, and some CAD files would cause catastrophic fragmentation. Switching out the allocator for a smarter one fixed the problem. A few versions of delphi later they put that allocator in by default, so that now it is not prone to fragmentation anymore.
like this one, with fputcsv in PHP. https://bugs.php.net/bug.php?id=43225
In the case of the standard library being broken, it is like a tool that is broken. Resmelting might not be a word but the idea of the hammer not being cast correctly and needing to be re-cast is ridiculous. In essence I was making fun of the "there's nothing wrong with C++, just like there's nothing wrong with a hammer" common claim. I was taking it to an extreme. (I thought it was funny.)
[1] https://www.google.com/search?q=%22C%2B%2B+is+just+a+tool%22
One small prototype, never run under full load, with mock up classes- not even the size of future classes, mock up operations(not even close to the real workload) and sometimes not even the final db-type attached. Yeah, hard to see the future, but why not drive the test-setup to the limits and go from their?
Instead the whole frankenservice is run once for ten minutes and declared by all involved worthy to bare the load of the project.
Here is to lousy component scouting and then blaming it on the architect.
Yes, yes they are.
I always deploy C++ server on jemalloc. Been doing it for years and while there's been occasional hicks up when updating it has provided much more predictable performance.
We're not talking about another optimization (small object / locality) as his issue was caused by libstdc++ alloc pools which would not need to exist in the first place if system malloc was better. So libstdc++ reinvents end up reinventing the wheel poorly.
As the author mentioned, when he disabled the optimization behavior GLIBCPP_FORCE_NEW he ended up burning more CPU via system (glibc) malloc(). Once he added jemalloc on top of GLIBCPP_FORCE_NEW, this pretty much evened out with previous behavior runtime performance.
The conclusion towards the end of article: > The right answer to "malloc is slow" is to make it faster.
That's one year after the ancient, bitrotted, unofficial copy of the libstdc++ documentation that the blog post links to, but still ancient history.
It's not exactly roses in C++ either of course. You can do better than the standard library facilities. Andrei Alexandrescu gave a great, entertaining, and technically elegant talk on memory allocation in C and C++ at Cppcon 2015 that is well worth watching
malloc/free aren't a great API, but for other reasons (namely, that you want things like multiple arenas, good control over synchronization, decent debugging and introspection, leak-tracking, tagging for figuring out what a block really is when things get smashed, block enumeration, small block pools, placement for cache alignment, and . . . you get the idea).
You have to do this anyway. You either know the size of the thing you allocated the memory for or, if it's a block, you need to keep track of the size for bounds checking purposes.
There are no circumstances in which you call malloc() in which you don't need to know the keep the size in your application.
It's also not a given that such an object even has a vtable. It's perfectly legal for it to not have one, and the memory is still supposed to be deallocated in full (only the base class dtor is invoked then, but if derived class additions are trivial, it's not necessarily a problem).
Now, yes, you could add a separate vtable slot for the deallocation function. Or just store the object size directly in the type info (that's usually attached to the vtable). But this is really just a way to optimize size storage for objects that already have a word utilized for shared type descriptor like a vtable.
1) There doesn't have to be a vtable.
2) If there is a vtable available, it's the wrong one (look carefully at the treatment of vtable pointers in destructors when inheritance is involved).
2) The object has been completely destroyed before the deallocator is called, and it's unclear whether the vtable pointer is available (the standard doesn't seem to make this guarantee). In any event, the deallocator is statically determined and cannot be a virtual call.
I will note that vtables are just an implementation detail, and that you can successfully implement virtual calls with other mechanisms, which are also not required (by the C++ standard) to remember object sizes. So you can replace the concept 'vtable' with 'abstract mechanism by which the set of members appropriate for this class is determined' -- use token-based dispatch, for example -- and still have a compliant implementation.
[I helped write a few object runtimes in the late 80s and early 90s -- and man, C++ gets gnarly -- and I've shipped 8 to 10 allocators of various types in commercial products over the years]
(1) receive packet: compute actual size, allocate buffer, expand data into it, start DMA to a speaker or something
(2) DMA-done: free the buffer
There, I didn't need the size on the free. There are many, many many similar cases, in fact these cases probably dominate.
Yes it does. See overload 6 here
Only because they all call malloc() and free() under the covers. It was however an ABI break, and one the C++ community thought worth doing.
If you replace your malloc with jemalloc you can easily wire this up to sdallocx(). The C++ standard library obviously can't do this out of the box because it can't assume you are using jemalloc
Furthermore, with Alexandrescu's solution, I'm not so sure that the 120 cycles that you gain here are not burned over there because your allocator has to construct a two-members structure and your program has to go through an extra indirection to access the actual memory block.
You are aware that malloc implementations tend to stick the size just before the part returned to the caller, right? eg. let's say you store size at p, return p+4 to caller, then have free() subtract 4 again to get at this "header"... So I'm guessing that's not your suggestion because free() wouldn't work at all without that kind of hack. Or more broadly, free() or realloc() and others already need to have some way to determine the size of the allocation based on the pointer, so they track that somehow in a way opaque to the caller.
So then what...? You want programs to be able to give back a prefix of the buffer? or...?
Edit to add: I've used both malloc()/free() and a custom memory allocation API that required the size to be passed in. I found the second API to be much more of a pain to use over the long term. Besides, it wastes memory because the memory API will have to track the size anyway to detect misuse of the API (or else blindly trust that the right size is passed in and hilarity ensues when it's not ...).
> Or more broadly, free() or realloc() and others already need to have some way to determine the size of the allocation based on the pointer
When I remembered I'd read a malloc implementation somewhere which actually didn't store the size in a header, but bakes in assumptions that you can determine the size of an allocation based on its address. So my naive thing isn't the only option, and I think if you have the kinds of concerns you raise there could be mitigations to be had.
It's terrible for performance because you're giving up control, even if it is a little safer overall.
It's better to pick exactly one of: (1) embrace the true nature of allocations and figure out lengths yourself or (2) if you are less comfortable with that, use some other language which doesn't expose any of this.
Right, and now you understand why free should have always accepted a size parameter, just like malloc.
Since the 'fine article' didn't really get in to any interesting details, i'll leave it at that.