People have such different perspectives. 26% slower does not sound "terrible" to me; it sounds like quite a reasonable price one might choose to pay for the convenience musl offers. If musl's allocator were 2.6x slower, I might call that "not so great"... but in order to qualify as "terrible" I think the difference would have to be an order of magnitude!
The 26% number at the top of the article is from using mimalloc (which is a high performance allocator, at least as fast as the glibc allocator) + musl for some task, and the slowdown is coming from (probably) slow musl implementations of memcpy/memset. The musl allocator is even worse.
Yeah, doing compute-heavy work a couple of jobs ago, we tried small images with musl, and the default allocator was a catastrophe: 75%+ slowdowns for our real life tasks. Even with a better allocator, we were way better off with the larger image.
> the slowdown is coming from (probably) slow musl implementations of memcpy/memset.
It's wild that such a fundamental piece of code (you can't really implement operation on structs without those) is library-supplied. I wish compilers would just have something like __builtin_memcpy and __builtin_memset, and provided some highly optimized, specialist-crafted assembly in those, instead of having to inline the library code and hopefully be able to optimize it.
Clang and GCC do provide these, and automatically use them in many situations (particularly small copies). But c-libraries can actually do it better in many cases, especially for large copies.
Glibc, for example, has perhaps ten different implementations of memcpy just for x86. The compiler certainly could provide all that, but the next step is harder:
glibc automatically dispatches to the proper one at runtime based on the actual microarchitecture that the binary is running on. You pay the extra dispatch cost once, but all of non-inline function call cost every time. This is what allows distros to compile to a nice baseline architecture, but still get near-optimal memcpy performance on many more architectures than a single inline instance could possibly give. These differences matter.
And it does it for not just memcpy, but half-a-dozen other extremely performance sensitive library functions, like strcpy and so on.
Inlining works very much against this strategy. If you can guarantee that the target microarch never changes, then it isn't a good one. But that is somewhat unusual for everyone but those who build their own binaries to run on a single class of machines forever.
Worse, inlining the really high performance versions of these ends up being terrible from a code size perspective, because they are often hundreds of instructions, which can have bad caching effects. And once you amortize the function-call cost over many iterations of the loop, it isn't so expensive to call out to the library.
Anyway, just some additional considerations to think about.
Even if the attempt is inside of a function called memcpy() which contains no code other than your copy loop, and links with priority over the libc implementation! (as all embedded firmware engineers learn at some point in their journey)
Maybe you're being sarcastic, but I'm pretty sure clang + gcc do offer these.
The problems at first glance :
- Not having control over the implementation detail of the interface that your library provides is probably not wise. Sounds like a lot of bad bug reports and edge cases that you have no control over.
As others have alluded, __builtin_memcpy doesn't resolve to a runtime implementation. GCC and clang treat functions like memcpy specially. Because they're defined by the standard and are reserved names, compilers can assume the exact semantics specified by the standard and elide library calls altogether with optimized inline code. But if the compiler can't do the optimization (can't prove alignment, indeterminate length, etc), it just emits a library call, even if your source has some other local function definition named "memcpy". Explicit use of __builtin_memcpy is treated identically to calls to memcpy, unless the compiler is invoked with -ffreestanding, in which case it only optimizes __builtin_memcpy and skips special treatment of calls to memcpy, but __builtin_memcpy could still expand into a call to memcpy. If you're writing a C library you want to use -ffreestanding. (I think. There may be more nuance. More info at https://gcc.gnu.org/bugzilla/show_bug.cgi?id=56888)
The 26% slower appears to be for their whole application, not just the allocator. For some parts of the application to make the whole this much slower it must mean that those parts are quite a lot slower, likely much more than 2x.
Moreover the 26% is with mimalloc, with musl's allocator it's 144%, so there are likely other parts that are slower (likely the memcpy implementation)
Ops here, I think if you NEED that convenience, sure, rock with MUSL BUT I also see a ton of devs crowing about using MUSL on my 128GB x86 Kubernetes hosts. I have plenty of Disk Space, you can ship glibc based container.
26% slower could turn into a huge hardware bill, and could render the library unusable for some purposes. There are many applications for which 26% is negligible, but it ain't nothing...
... you're fine with trading an app running 26% slower, to save a dozen megabytes ? that sounds positively insane to me. That's accepting to go from e.g. 60fps to ~45 fps (e.g. completely unacceptable)
That would depend on the application. The convenience isn't just a smaller binary, but also being independent of the distros glibc version (being on an LTS distro and not updating often, I have experienced incompatible glibc versions often enough). The megabytes shaved off are significant if the final binary size is < 1MB, which is a completely different size class.
And not all applications are performance sensitive. Something like UNIX's bc command would benefit more from having easier compatibility and a faster startup than more optimized allocators and string functions.
In the end, it ~doesnt even matter~ is all trade-offs. The nice thing is that it is up to the developer to make the decision of which libc to use, so everyone gets what they want.