Slitter: A slab allocator that trusts, but verifies
engineering.backtrace.io
engineering.backtrace.io
The allocators in the past have tried to use the space it is not currently using for user code for its own book-keeping, which is very efficient when it comes to usage of space. So the free-lists are interleaved with the actual allocations to be as cheap as they can be (not in a different page).
Oddly enough, packing all the metadata in a single area might actually be faster though at lower scales might be worse for the data itself.
I've used talloc[1] in the past, which traded some of the efficiencies for safety (I have my own sins trying to copy, but simplify pool based allocators for PHP apc with some tricks[3] borrowed from xcache).
If the author is reading, the missing magic I've always missed with jemalloc are the .gdb macros for walking the allocator and do things like print the allocations by size/content [2]. You can write them in python now and that can let you print them out neatly in a graphviz dot structure with linkages, which is the sort of project which you hand out to an intern who just joined.
[1] - https://talloc.samba.org/talloc/doc/html/libtalloc__dts.html
[2] - https://research.nccgroup.com/2015/08/11/libtalloc-a-gdb-plu...
This is really fascinating. Does anyone know how much the typical cost of such types of guarded allocations are? is it in the single/double digit percentages?
OpenBSD's allocator uses a separate index, as well as many more mitigations (many more than Slitter), and unfortunately OpenBSD's allocator ends up being substantially, even notoriously slower than most other allocators. But, again, this has little to do with using a separate index, except that using a separate index opens the door to many other potential mitigations. And where ever there's a choice between performance or mitigation, OpenBSD usually takes the latter (though sometimes they walk that back as usage and efficacy become more clear).
Computers are 64-bit by now. For most practical applications, address space is unlimited. This means you can allocate let’s say 1TB of continuous address space at startup, and implement allocator on top that allocates and commits physical pages in that range, implementing a stack allocator. Not only it never moves the data, it also guarantees the complete memory is at continuous range of addresses. For instance, you can view that thing as an infinite std::vector() which never requires reallocations.
I did it for Windows on top of VirtualAlloc() API. Linux has it too, mmap() with PROT_NONE protection flag to reserve address space, then mprotect() to allocate physical pages in that range.
It works for me because I have distinct moments in time when the whole stack is no longer needed. I’m working on a CAM/CAE app where work is organized as a sequence of optimization passes. I only need to keep the data within 1 iteration. At the end of every optimization pass I simply reset the stack pointer to the very bottom, and on the next iteration the stack starts growing again from zero reusing these physical memory pages.
BTW I’m using multiple of these stacks, one per thread. Helps with performance because no locks are needed, also L1 and L2 caches are inside cores, only the L3 is shared across the whole CPU.
The approach is pretty close to arena allocation https://en.wikipedia.org/wiki/Region-based_memory_management... The only twist, with modern 64-bit processors there’s no need for linked lists or multiple blocks, can instead add new physical pages into pre-allocated address space.
This way you never have to talk to the kernel if you need more memory.
In V8, cleaning up the reservations for Wasm memories is ultimately tied to garbage collection of the JS heap, since JS objects can root memory. That led to the somewhat clunky need to retry allocation in a loop, running a full GC if allocation fails the first few times (https://github.com/v8/v8/blob/master/src/objects/backing-sto...).
There are also fundamental kernel limitations on virtual memory size. They are system dependent but much lower than 2^64.
In my use case, I was lucky as I only need an allocator per hardware thread. They never approach thousands. So far, the maximum is 256 hardware threads, like in dual-CPU EPYC 7713.
[1] Ideally before most shared libraries have been loaded. I should do a simulation to get some quantitative estimates, but my hunch is that ASLR hurts badly even if each loaded library only takes up one 4 kB page. E.g. if you have an empty address space, allocate a 4 kB page at a random address and split the space in two around it, then try to densely pack as many 8 GB ranges in there as possible, you lose one potential 8 GB range (even if the ranges only require 4 kB alignment for their base address). Whenever such splits don't land in the same 8 GB clump, you lose a potential 8 GB range per split.
A few things the article talks about where one can maybe do even better:
1. likely()/unlikely() not being in stable Rust. This is true, but the hashbrown project has some hacked-up variants it claims work on stable: https://github.com/rust-lang/hashbrown/blob/bbda6e0077bafb75...
2. Rust not having fast thread locals. Same problem for me, so likewise did it in C with "initial-exec". But! If you use clang, you can get LTO across C and Rust, so you can get fast thread locals _and_ not have function call overhead. Basically need to use same version of Clang as Rust does (12 at the moment) and do a little song and dance in linker and compiler flags. See https://matklad.github.io/2020/10/03/fast-thread-locals-in-r...
3. For testing these sort of things, being able to assert "this test touched this code path" is extremely useful. In my case, for example, I have different code paths for sampled and unsampled allocations, but from perspective of code calling malloc() everything should be identical. So how to tell if correct code path was used? Coverage marks are a great solution for this: https://ferrous-systems.com/blog/coverage-marks/
(The Python profiler, if anyone is interested: I've already released an open source memory profiler that tracks all allocations, https://pythonspeed.com/fil/. Unlike most memory profilers it tells you source-of-allocations at peak memory, which is key for data processing applications. The commercial production-grade version I'm now working on uses sampling, and will be even more focused on data processing batch applications; the goal is to have essentially no performance overhead so it can always be on.)
Two people getting ready to leave the house.
A: "Did you turn off the stove?" B: "Yes" A: "Are you sure?" B: "Yes"
If you trust them, then you leave the house. If you go verify it yourself, then you don't trust them.
In case you aren't aware of its history: https://en.wikipedia.org/wiki/Trust,_but_verify
And as to support my point, even your link quotes Stalin being the inspiration of the concept specifically says:
"Healthy distrust makes good base for cooperation"
Ie the phrase is simply distrust - it has no meaning to trust at all.
Consider the different approaches to mitigating terrorism (proactive prevention against a hostile actor willing to make sacrifices to harm you) versus mitigating tax evasion (after-the-fact verification and punishment against a greedy actor willing to make rational selfish decisions).