Alexandrescu Replaces std::allocator With Composable Memory Model

CppCon 2015: Andrei Alexandrescu “std::allocator...” — CppCon

CppCon

Standard C++ memory allocation fails because std::allocator couples typed object construction with raw byte management, requiring a replacement built on untyped blocks and composable primitives.

AI-assisted summary of CppCon 2015: Andrei Alexandrescu “std::allocator...” — original source. Check the original for full context.

Source published:

Key ideas

  • std::allocator has fundamentally failed modern C++ because it was historically engineered to abstract 16-bit segmented x86 architectures rather than serve as a generic, composable memory allocation framework. (14:09)
  • True composability in allocator design requires operating on raw untyped memory blocks (void* and size_t) rather than strongly typed objects, separating object lifecycle management from storage acquisition. (18:03)
  • Deallocation in layered or fallback allocators is mathematically impossible without either storing block sizes or exposing an owns(Block) query to verify block provenance across tiers. (30:45)
  • Production-grade general allocators can be constructed entirely from orthogonal, composable building blocks—such as Segregators, FallbackAllocators, FreeLists, and Bitmapped Arenas—nested in compile-time search trees. (63:42)

Detailed notes

## Background This talk was delivered by Andrei Alexandrescu and published on YouTube by the CppCon channel on October 13, 2015. According to the page description, the presentation critiques the historical design failures of std::allocator and introduces a generic, componentized, and composable memory allocator architecture designed from first principles for modern, high-performance C++ applications.

## TL;DR Alexandrescu argues that std::allocator fundamentally failed because it was built around obsolete segmented-memory pointer models, tightly coupled object construction with untyped byte allocation, and completely lacked composability. He outlines the core requirements for an effective memory allocator, demonstrating that allocators must trade in raw memory blocks containing both a pointer and a size rather than typed elements. By introducing foundational building blocks—including NullAllocator, FallbackAllocator, FreeList, Affix Allocator, BitmappedBlock, and Segregator—he shows how developers can assemble industrial-strength, custom allocators tailored to exact application workloads. The proposed composable API incorporates crucial operational primitives such as owns(), good_size(), expand(), and scoped batch deallocation to deliver maximum runtime efficiency without manual heap chaos.

## Atmosphere & style The presentation is energetic, irreverent, and intellectually combat-ready, driven by Alexandrescu's sharp wit and theatrical declarations that he has arrived on a 'war path' to dismantle sacred cows. He commands the room with spontaneous audience banter, sharp comedic analogies comparing allocators to Crocs, and self-deprecating commentary on slide syntax highlighting bugs. Watching the talk feels like an intense, fast-paced masterclass that shifts fluidly from historical software post-mortems to live architectural design, blending technical rigor with genuine showmanship.

## Bulletin - [14:09](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=849s) std::allocator has fundamentally failed modern C++ because it was historically engineered to abstract 16-bit segmented x86 architectures rather than serve as a generic, composable memory allocation framework. - [18:03](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=1083s) True composability in allocator design requires operating on raw untyped memory blocks (void* and size_t) rather than strongly typed objects, separating object lifecycle management from storage acquisition. - [30:45](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=1845s) Deallocation in layered or fallback allocators is mathematically impossible without either storing block sizes or exposing an owns(Block) query to verify block provenance across tiers. - [01:03:42](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=3822s) Production-grade general allocators can be constructed entirely from orthogonal, composable building blocks—such as Segregators, FallbackAllocators, FreeLists, and Bitmapped Arenas—nested in compile-time search trees.

## Key takeaways - [05:18](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=318s) The classic C memory API exhibits a structural asymmetry where malloc takes a size but free takes only a bare pointer, forcing runtimes to manage hidden metadata or page tables. - [11:56](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=716s) Class-level operator new and operator new[] introduce severe language quirks and should be avoided in favor of dedicated memory allocation abstractions. - [18:03](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=1083s) std::allocator conflated raw memory allocation with object construction and destruction, unnecessarily binding allocation policies to C++ object types. - [21:44](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=1304s) Realistic allocators are inherently stateful, meaning stateless allocator models force undesirable reliance on global state or hidden monostates. - [24:10](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=1450s) Composability is the essential first principle of allocator design, matching the tiered architectures of industrial allocators like jemalloc and dlmalloc. - [25:25](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=1525s) NullAllocator acts as an immutable compositional terminator, safely terminating fallback allocation chains and asserting on invalid deallocations. - [30:45](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=1845s) A fallback allocator cannot safely implement deallocate without an owns() primitive that checks whether a given block originated from the primary allocator. - [35:44](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=2144s) Combining a fast stack bump allocator with a general-purpose heap fallback provides small-buffer optimization that can satisfy up to 95% of throughput locally. - [41:00](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=2460s) Compulsive alignment rounds every requested allocation size immediately to the boundary constraint, preventing cascading alignment drift across consecutive bump allocations. - [43:20](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=2600s) Free lists achieve near-zero runtime space overhead by reusing the payload memory of deallocated blocks for intrusive next-pointers, though they risk retaining memory indefinitely. - [45:28](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=2728s) Reused blocks in free lists are often cold in CPU cache, making writes to intrusive pointers during deallocation a hidden source of cache line misses. - [56:00](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=3360s) Affix allocators enclose payload allocations in compile-time prefixes and suffixes to provide canary guard bands, dynamic profiling, and leak tracking with zero overhead when disabled. - [59:08](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=3548s) Static registration structures embedded in allocation call sites allow tracking caller origins and invocation counts at runtime without triggering secondary dynamic heap allocations. - [01:01:48](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=3708s) BitmapBlock allocators isolate allocation metadata away from user data into dedicated bit arrays, keeping allocator bookkeeping hot in cache without polluting payload memory. - [01:03:42](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=3822s) Segregators split allocation routing strictly by size thresholds at compile time, enabling deep binary trees of specialized sub-allocators without requiring owns() checks. - [01:07:03](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=4023s) A comprehensive minimal allocator API should provide primitives like good_size(n) to allow containers such as std::vector to consume slack capacity without extra allocations.

## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=0s) Working Titles and the War Path Against `std::allocator`

Andrei Alexandrescu opens the session early, joking that starting early is necessary because they are bound to finish late. He explains the evolution of his working titles. Initially, he floated a cryptic title that left people baffled, leading him to consider a playful pun: an allegation about an alligator that ate someone's Crocs. The joke illustrates his core thesis: just as an alligator has virtually nothing to do with casual footwear, there is minimal connection between `std::allocator` and actual, practical memory allocation.

He then considered the title *"Design allocators that don't (not work)."* While grammatically malicious in English, it is logically sound to a programmer if one treats `"not work"` as a grouped verb expression inside parentheses. Alexandrescu makes his stance toward the standard library's allocator design unmistakably hostile, declaring to the audience:

> “I’m here to destroy, okay, I’m on a war path right now.”

The goal, however, is constructive. He notes that taking cheap shots at `std::allocator` is far too easy; the real objective of the talk is to understand how to design high-performance, composable memory allocators from first principles. Invoking Bjarne Stroustrup, he reminds the audience that this is C++, where runtime efficiency and layout control are paramount.

```mermaid flowchart LR accTitle: Evolution of Talk Titles accDescr: Andrei Alexandrescu's transition from humorous working titles to his serious thesis on allocators. A["Allegation about an alligator"] --> B["Design allocators that don't (not work)"] B --> C["Core Mission: Build allocator APIs that actually work"] ```

---

[02:05](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=125s) The Cyclical History of Memory Allocation (1983–Present)

Memory allocation has followed an intriguing historical cycle. Alexandrescu reflects on how the fortunes of memory allocator implementers have swung back and forth over several decades:

- **1983 (The K&R Era):** When Brian Kernighan and Dennis Ritchie published the second edition material featuring their minimal storage allocator, it was a revelation. For the first time, programmers could implement custom memory management entirely in a high-level systems language using `void*`, casts, and layout manipulation—without dropping into raw assembly language. Compared to contemporary languages like Pascal, Fortran, or Lisp, C offered unprecedented power to put bits precisely where the programmer wanted. Dynamic memory allocation, once an exotic feature, suddenly became the default hammer for every programming nail. - **1993 (The Custom Allocator Boom):** A decade later, the naive global `malloc` implementations shipped by standard libraries had become severe performance bottlenecks. Application developers realized that almost any hand-rolled custom allocator tailored to specific data structures outperformed the general-purpose system `malloc`. - **2003 (The Modern Allocator Renaissance):** General-purpose allocators fought back. Advanced multi-threaded designs—such as Hoard, jemalloc, and TCMalloc—reclaimed performance ground through thread caching and sophisticated binning, matching or beating naive custom allocators. - **Present Day:** The industry sits in a balanced, pragmatic middle ground. Modern general-purpose allocators are very capable, but high-performance applications still routinely require domain-specific allocation patterns to minimize overhead and fragmentation.

---

[05:18](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=318s) The Fundamental Asymmetry of `malloc` and `free`

Examining the classic C memory API reveals a long-standing architectural flaw. When allocating memory via `malloc(size)`, the caller specifies the requested byte count; but when releasing it via `free(ptr)`, the caller passes only the bare pointer:

```c void* malloc(size_t size); void free(void* ptr); ```

When asked why this signature is problematic, the audience immediately identifies performance. Because `free` receives no size parameter, the allocator itself is forced to record and look up the metadata for every allocated chunk—typically by prepending hidden header prefixes or maintaining out-of-band page-run tables. The caller frequently already knows the exact size of the buffer being destroyed (for instance, inside a typed object destructor or container), yet the API prevents that size from being communicated back down to the memory subsystem.

Alexandrescu highlights ongoing standards efforts—referencing a formal C++ proposal number (N3778 / sized deallocation)—aimed at fixing the performance penalties incurred by global `operator delete` lacking size information. Pointing out Howard Hinnant in the audience, he jokes about the exhausting grind required to get memory allocation proposals altered in standard committees:

> “I was talking to this fine gentleman Howard Hinnant, and he says, I’m like, 'Been there, done that, and I would hate trying to do it again—doing anything, exacting any change about memory allocation in the respective standards.'”

Had C started from a clean slate, the primitive unit of currency would be a self-contained block structure rather than a raw address:

```cpp struct Block { void* ptr; size_t length; }; ```

Under this model, allocation returns a `Block`, and deallocation accepts that same `Block` back, freeing the underlying engine from redundant size management and header tracking.

---

[08:48](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=528s) Operator `new`, Type Information, and Syntactic Quirks

Alexandrescu turns to the next historical step: C++'s `operator new`. While `new` introduces strong typing, it quickly throws that advantage away under the hood because the default implementations simply delegate directly down to `malloc`. This inheritance also explains the conspicuous absence of an `operator renew` to pair with C's powerful, if notoriously misused, `realloc`.

Beyond deallocation metadata loss, operator `new` introduces bizarre syntactic ambiguities in the language grammar itself. For example: - `int` is a distinct type. - `int[100]` is equally a well-formed type representing a fixed-size array of 100 integers.

Yet evaluating `new int[100]` does not return a pointer to a fixed-size array of 100 integers (`int(*)[100]`). Instead, the language hijacks the syntax, treating the bracketed expression as a request to dynamically allocate a variable runtime count of integers, degrading the return type to a raw `int*`.

Alexandrescu registers his vigorous disagreement with how the language evolved:

> “Operator `new` was designed to work with `malloc`, but you know, it didn't—I have, you know, like in that movie, like: I protest!”

Furthermore, native C++ operators impose a complete communication firewall between memory allocation and object construction: `operator new` runs in isolation without knowing which constructor arguments will be evaluated, severely hampering class-specific memory placement strategies.

[11:56](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=716s) "The Great Array Distraction" and Operator New

Andrei warns the audience against clever class-level allocation tricks, quipping that anyone struggling with it should consider attending "Operator New Anonymous" (OA) meetings. In his view, the healthiest choice a programmer can make regarding class-level `operator new` is complete avoidance: “I think the best trick you can do with class level operator new is to forget all about it and never use it and like really pretend it doesn't exist in the language and then you'll be happy you you lead a better life.” He then introduces what he dubs the "Great Array Distraction" (TGAD). The language already had the idiosyncratic `operator new`, but then language designers felt compelled to invent a bracketed variant, `operator new[]`, strictly for allocating arrays, alongside a rigid ritual: matching scalar `new` with scalar `delete`, and array `new[]` with array `delete[]`.

To demonstrate the absurdity of this rule, Andrei polls the room on why the distinction even needs to exist. An audience member points out that calling single-object `delete` on an array will simply invoke the first destructor it encounters and fail to tear down the rest. Andrei concedes this mechanical explanation, but argues that from an architectural perspective, there is no legitimate reason for this dual-syntax machinery to exist in the first place—it serves only to make life miserable for developers. Turning directly to the camera operator tracking his movements across the stage, Andrei jokes that if he were trying to escape from a prison, having this camera following his every step would make it impossible. He then leaves no doubt about his stance: “I unrecommend operator new for any serious use of memory allocation.”

- Class-level `operator new` is dismissed as an anti-pattern best purged entirely from one's coding habits. - The Great Array Distraction (TGAD) introduced `new[]` and `delete[]` to handle array destructions, complicating memory management without fundamentally improving allocation. - Andrei rejects `operator new` as unsuitable for serious, high-performance systems programming.

---

[14:09](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=849s) The Real Origin of `std::allocator` and the 20-Year Problem

Andrei pivots to the historical reality of `std::allocator`, asserting that the single funniest fact about it is that it was never genuinely designed for general-purpose memory allocation. When he asks the audience what its true purpose was, Pablo Halpern calls out pointer types, confirming Andrei's point: it was conceived to abstract away the segmented memory architectures of 16-bit systems—specifically 20-bit pointers broken into segment and offset pairs ("near" and "far" pointers). Andrei jokes that anyone old enough to remember segment/offset gymnastics should turn to the younger attendees to explain the horror, before advising that the younger generation is honestly better off not knowing. Despite the disappearance of segmented architectures, `std::allocator` lingered in the standard all the way through C++17, functioning like a single, stubborn square wheel on a multi-wheeled transport truck.

```mermaid flowchart TD accTitle: Historical Evolution of std::allocator accDescr: Diagram tracing std::allocator from 1990s segmented memory models to modern C++ where its original purpose was removed.

subgraph Origin["1990s Design: Segmented Architecture"] A["16-bit Systems / 20-bit Pointers"] --> B["Near & Far Pointer Abstractions"] B --> C["std::allocator finalized (pre-1994)"] end

subgraph Subsided["Hardware Evolution"] C --> D["Segmented memory models disappear"] D --> E["C++ Committee strips near/far pointer machinery"] end

subgraph Present["Modern C++ Reality"] E --> F["std::allocator remains: square wheel on the truck"] F --> G["Lacks native allocation algorithms"] end ```

To illustrate just how broken the abstraction remains, Andrei highlights Alisdair Meredith's presentation from CppCon 2014, titled "Making Allocators Work." Andrei performs a linguistic critique of that title: `std::allocator` had been standardized twenty years earlier (prior to 1994), yet two decades later, the community was still attending a grueling, 180-minute two-part marathon session just trying to get allocators to function properly. As Andrei puts it: “There's no talk like making sort work.” Furthermore, having sat through most of those three hours, Andrei noticed there was not a single slide dedicated to actual memory allocation algorithms. He emailed Alisdair asking for the slide deck so he could cite it, but received total silence in return because, Andrei jokes, Alisdair knew he was going to make fun of it.

- `std::allocator` originated as a stopgap abstraction for 16-bit segmented x86 architectures handling 20-bit near and far pointers, not as an engine for memory management. - The C++ standards committee eventually stripped the near/far pointer support from the allocator specification, thereby dismantling the exact problem it had originally been created to solve. - Alisdair Meredith's 2014 CppCon presentation ("Making Allocators Work") ran for 180 minutes across two back-to-back 90-minute slots without dedicating a single slide to allocation mechanics.

---

[18:03](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=1083s) Flaws in `std::allocator`: Types, Factories, and Void Stars

Andrei unpacks why `std::allocator` fails as an allocation mechanism from first principles, declaring: “The short story here is that std::allocator is not a great allocator because it was not designed for that purpose; it was designed for near and far pointers.” The most fundamental design error is parameterizing the allocator on a specific type `T`. Memory allocators do not need to care about object types; they care exclusively about size and alignment. Andrei asks the room whether anyone needs to allocate an `int` differently than an `unsigned int`—they share identical byte layouts and sizes. Even across completely unrelated types `T1` and `T2` that both happen to require 128 bytes, an optimal allocator configured for that size and alignment handles both equally well. Tying an allocator's identity to a data type was sensible only when pointer representations differed across types, but for modern allocation it is an active hindrance.

This conceptual confusion led directly to conflating an allocator with an object factory. Instead of merely handling memory buffers, `std::allocator` was saddled with `construct` and `destroy` member functions. Andrei playfully calls out Howard Hinnant in the audience, joking that the committee still had not excised these extraneous methods: “They didn't remove them, those bastards!” Adding insult to injury, despite claiming these high-level object-construction factory powers, the allocator still trades underneath in raw `void*` pointers. This architectural mismatch produced notorious crutches like `rebind`, which containers must use simply to allocate internal bookkeeping nodes of a different type than the stored value. Andrei summarizes the proper conceptual contract cleanly: “So what I'm seeing here is allocators should trade in memory blocks, void* and size_t, that's it.”

- Allocation strategies depend strictly on request size and alignment constraints, not semantic object types. - Allocators were mistakenly turned into factories by coupling memory acquisition with object lifecycle operations (`construct` and `destroy`). - The type-parameterized model necessitated awkward template metaprogramming workarounds, most prominently template `rebind`. - A clean memory allocator interface should operate entirely on memory blocks characterized by a base pointer (`void*`) and an extent (`size_t`).

---

[21:44](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=1304s) The Myth of Statelessness and the Need for Composition

Another foundational flaw Andrei attacks is the historical assumption that allocators are stateless. He ridicules the notion that an object dispensing finite physical resources could somehow carry no state: “A memory allocator that giving me memory has no state, right? This is very interesting, it's like the empty bottle that you keep on drinking water from and it's just you know, it's like Sinbad, you know the, it's the story, it's like magic, you know it's empty but I'm pouring from it.” In reality, every allocator has state; if it appears stateless, it is simply hiding its state behind a global heap or a monostate pattern ("a global except it has a great name"). Andrei acknowledges that C++14 finally permitted stateful allocators, pointing out committee members John Lakos and Pablo Halpern sitting in the front row and joking that the audience should either thank them or throw tomatoes at them depending on their perspective.

However, Andrei argues that even modern standard revisions missed the single most critical structural requirement of allocator design: composability. When building industrial memory allocators, one cannot rely on a single monolithic strategy. Looking at proven allocators like Doug Lea's `dlmalloc`, the implementation immediately branches into different sub-allocators based on request thresholds (for instance, applying small-object strategies below one kilobyte and completely different mechanisms above it). Andrei emphasizes that modularity is non-negotiable: “If you want to write an allocator, you got to make it composable.” An effective allocator architecture must consist of small, focused building blocks that can be stacked, cascaded, and composed to fit application-specific memory consumption patterns.

- Classical standard allocators enforced an unrealistic stateless model, forcing implementations to rely on hidden global state. - C++14 introduced stateful allocators thanks to proposals championed by committee members including Pablo Halpern and John Lakos. - High-performance memory allocators cannot be monolithic; they inherently require composition across distinct allocation strategies tuned to different size classes.

[24:10](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=1450s) Composition as the First Principle of Allocator Design

Every realistic general-purpose allocator relies on a tiered composition of allocation strategies driven by request size. A typical design might allocate anything under 1 kilobyte using an array of highly specialized bucket allocators, apply a completely different strategy between 1 kilobyte and 64 kilobytes, and for requests larger than that, simply bail out to an operating system call like `mmap`. The Windows heap allocator follows this exact architecture: small objects follow one specialized code path, medium objects follow an entirely different path, and enormous requests trigger out-of-memory or map calls. Under the hood, a sub-64 KB small-object allocator is itself composed of an entire array of smaller allocators, each tailored to a specific size class with its own discrete buckets.

Because production memory allocators are fundamentally aggregates of specialized strategies, composability cannot be an afterthought or an accidental feature. It must be the primary design constraint from day one. When composition is solved first, the underlying allocator abstractions and APIs fall into place naturally.

```mermaid flowchart TD accTitle: Tiered Allocation Strategies by Size accDescr: Incoming allocation requests are partitioned across specialized allocators based on size thresholds.

Req["Allocation Request (Size N)"] --> Check{"Size Range?"}

subgraph Small ["Small Objects"] Check -->|"< 1 KB"| Buckets["Bucket Allocators<br>(Array of size classes)"] end

subgraph Medium ["Medium Objects"] Check -->|"1 KB – 64 KB"| MedStrategy["Specialized Medium<br>Pool Strategy"] end

subgraph Large ["Large Objects"] Check -->|"> 64 KB"| OSMap["Direct OS Mapping<br>(mmap / VirtualAlloc)"] end ```

- High-performance allocators cannot use a single monolithic algorithm; they partition requests by size classes (e.g., `< 1 KB`, `1–64 KB`, `> 64 KB`). - Small-object allocators internally host an array of micro-allocators, each tuned to tight bucket intervals. - Andrei emphasizes: “Getting composition right is getting the allocators right.”

---

[25:25](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=1525s) The Null Allocator as a Compositional Terminator

To establish a design based on composition, Andrei asks the audience to identify the simplest possible composable allocator. The answer is the `NullAllocator`. Borrowing a comparison from *Seinfeld*, Andrei calls it “a show about nothing”: it does absolutely nothing, but it does so in style. Every request to `allocate()` immediately returns null. For `deallocate()`, the allocator asserts that the passed pointer is null, since a consumer must only pass back memory that the allocator itself previously handed out.

```mermaid flowchart LR accTitle: Allocation Strategy Pipeline with NullAllocator Terminator accDescr: Requests pass down an allocation chain and are terminated by a NullAllocator if all prior attempts fail.

Req["Allocate Request"] --> Alloc1["Primary Strategy"] Alloc1 -->|"Fails (null)"| Alloc2["Secondary Strategy"] Alloc2 -->|"Fails (null)"| NullTerm["NullAllocator<br>(Always returns null)"] NullTerm --> RetFail["Return Out-of-Memory"] ```

Though trivial, the null allocator serves an indispensable role as a sentinel or terminator in composite designs, analogous to wave terminators in electrical engineering waveguides. Placing a null allocator at the end of an allocation chain provides an explicit, well-defined terminal boundary when all previous allocation attempts have been exhausted.

- `NullAllocator::allocate(size_t)` always returns `null`. - `NullAllocator::deallocate(void* p)` asserts that `p == nullptr`. - Its total capacity can conceptually be modeled as either zero or infinite; the critical property is that it serves as an immutable terminator in layered pipelines.

---

[27:10](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=1630s) Fallback Allocator and the Derivation of API Primitives

Moving past the null allocator, Andrei introduces `FallbackAllocator<Primary, Fallback>`, the simplest non-trivial composite allocator. It holds two allocators: it attempts to satisfy an allocation using `Primary`, and if that returns null, it falls back to `Fallback`. Andrei implements this using private inheritance to take advantage of the Empty Base Optimization (EBO), keeping stateless sub-allocators at zero overhead.

Implementing `allocate(size)` in `FallbackAllocator` requires only three lines:

```cpp Block allocate(size_t n) { Block b = Primary::allocate(n); if (!b.ptr) b = Fallback::allocate(n); return b; } ```

However, when considering `deallocate(Block)`, the interface hits an immediate structural roadblock. The allocator receives a block of memory, but with only `allocate` and `deallocate` defined, it is impossible to know whether that block originally came from `Primary` or `Fallback`. Passing a foreign pointer into the wrong allocator is a fatal error. Andrei notes that designing strictly for composability forces the discovery of necessary primitives without relying on guesswork: “And that's a good sign right I'm not kidding actually to not have to be smart is a great thing in software engineering right because it means you you have the right frame of thinking and the right frame of reference and there's only one way to follow the tunnel.”

- Private inheritance allows the composite allocator to inherit state from base allocators while benefiting from the Empty Base Optimization (EBO). - `allocate()` requires a trivial check: if `Primary::allocate()` returns an empty/null block, forward the request to `Fallback::allocate()`. - The existing two-method interface (`allocate`/`deallocate`) is provably insufficient for composability because the deallocation path cannot deduce provenance.

---

[30:45](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=1845s) The `owns()` Primitive and Conditional Interfaces

To resolve the provenance dilemma in deallocation, the allocator interface must introduce a new fundamental primitive: `owns(Block)`. Using `owns()`, an allocator can inspect a block (via pointer range checks, pool address boundaries, or lookup tables) and report whether it allocated that memory. With this primitive, deallocation in `FallbackAllocator` becomes clean and robust:

```cpp void deallocate(Block b) { if (Primary::owns(b)) Primary::deallocate(b); else Fallback::deallocate(b); } ```

This requirement raises two questions about interface design: 1. Does `Fallback` also need to implement `owns()`? Not necessarily. It is common for the fallback allocator to be a blunt catch-all like standard `malloc`, which often cannot efficiently report address ownership. 2. When can `FallbackAllocator` itself implement `owns()`? Only when both `Primary` and `Fallback` implement it, returning `Primary::owns(b) || Fallback::owns(b)`.

```mermaid flowchart TD accTitle: Deallocation Flow in FallbackAllocator accDescr: Demonstrates how ownership checks route deallocation between primary and fallback allocators.

BlockIn["deallocate(Block b)"] --> OwnCheck{"Primary::owns(b)?"} OwnCheck -->|"Yes"| PrimaryDealloc["Primary::deallocate(b)"] OwnCheck -->|"No"| FallbackDealloc["Fallback::deallocate(b)"] ```

In C++, member templates take advantage of what Andrei playfully terms "MEDF" (Method Definition Failure Is Not An Error, riffing on SFINAE). If a method in a class template is never called by user code, it is not instantiated, allowing allocators to omit `owns()` unless a composite wrapper specifically queries it.

When an audience member asks whether `FallbackAllocator` could simply prepend a hidden boolean tag to every allocation to record which sub-allocator served it, Andrei explains that doing so burns time and memory on metadata overhead. For high-speed allocators, `owns()` is often a fast range check (e.g., checking whether a pointer falls within a pre-allocated stack buffer), making metadata tagging wasteful.

- `owns(Block)` returns `true` if the block originated from this allocator. - Andrei's dictum: “The primary allocator must be able to tell whether a block came from it or not and only with that knowledge I'm able to implement deallocate.” - The composite `FallbackAllocator` exposes `owns()` conditionally via logical OR if both sub-allocators support it. - Storing dynamic discriminator tags alongside allocations wastes both space and time compared to intrinsic address-range checks.

---

[35:44](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=2144s) Composing a Fast Stack-and-Heap Allocator

With just `allocate`, `deallocate`, and `owns`, fully functional and performant allocators can already be composed. Andrei demonstrates this by combining a fast, lightweight `StackAllocator` with a general-purpose heap allocator. The stack allocator manages a fixed contiguous buffer (e.g., 16 KB) directly on the execution stack, satisfying allocations by bumping an internal offset pointer.

By assembling a `FallbackAllocator<StackAllocator<16384>, Mallocator>`, small allocations are fulfilled on the stack with zero system overhead. Once that 16 KB buffer is exhausted, subsequent allocations seamlessly fall back to `malloc`.

```mermaid flowchart TD accTitle: Composition of StackAllocator and Mallocator accDescr: Requests attempt allocation from a 16 KB stack buffer, falling back to malloc if exhausted.

AllocCall["allocate(size)"] --> TryStack["StackAllocator<16384><br>(Pointer bump)"] TryStack -->|"Success"| StackReturn["Return Stack Block"] TryStack -->|"Exhausted (null)"| TryMalloc["Mallocator<br>(Calls malloc)"] TryMalloc --> HeapReturn["Return Heap Block"] ```

Andrei then poses a question to the audience: Can a stack allocator support deallocation? The room debates whether the answer is a strict "yes," "no," or a "qualified yes." Andrei jokingly asks whether the qualifier is "const or volatile," before exploring what conditions must be met for a bump allocator to safely unwind or reclaim memory.

- `StackAllocator<N>` satisfies allocation requests via rapid pointer-bumping within a local array of size `N`. - Composition via `FallbackAllocator<StackAllocator<16384>, Mallocator>` yields an optimal small-buffer optimization pattern in just a few lines of code. - Whether a bump-pointer stack allocator can deallocate depends entirely on the order of deallocations (LIFO unwinding versus arbitrary free order).

[37:10](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=2230s) Conditional Deallocation and Stack Allocator Mechanics

Alexandrescu dives into the mechanics of `deallocate` in a stack-based allocator. In a stack allocator, allocation simply moves an offset pointer forward through a preallocated buffer; everything behind that pointer is live allocated space, while everything in front is available memory. As a direct consequence, arbitrary deallocation is impossible if the target happens to sit somewhere in the middle of live blocks. Alexandrescu explains that deallocation can only proceed if the pointer to the beginning of the block, plus the size of the block rounded up to alignment, lands exactly at the current allocation frontier. As he puts it in plain terms: “if this deallocation is the deallocation of the last allocation that happened then I can deallocate because I can't roll back the pointer but if it's somewhere in the middle I can't.”

This leads to a conditional deallocation design. If the caller happens to free objects in strict LIFO (last-in, first-out) order, the allocator can step its pointer backward cleanly without causing any harm. In addition, checking whether a block is managed by this allocator via an `owns` member function is trivial and cheap, requiring only a simple range test against the buffer bounds. The interface also supports a `deallocate_all` primitive, which Alexandrescu describes as very cheap but very risky, as it unconditionally resets the allocation pointer back to the beginning, instantly wiping the state and making the entire buffer reusable.

```mermaid flowchart LR accTitle: Stack Allocator Layout and Rollback Condition accDescr: Diagram showing the buffer split into previously allocated space, the last allocated block, and free space, demonstrating where rollback is allowed.

subgraph Buffer["Stack Allocator Buffer"] A["Previously Allocated<br/>(Cannot roll back)"] B["Last Allocation<br/>(Pointer + rounded size)"] C["Free Available Space"] end

A --> B B --> C D["deallocate(p, s)"] -->|"p + roundUp(s) == frontier"| E["Roll back frontier<br/>(Success)"] D -->|"p + roundUp(s) < frontier"| F["No-op / Cannot deallocate"] ```

[39:15](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=2355s) Untyped Allocators and the Stack-Backed Pattern

Responding to an audience question about type qualifications, Alexandrescu emphasizes a fundamental philosophical separation in his design: memory allocators should deal solely with raw memory, operating strictly on untyped `void*` and byte sizes. While many C++ developers intuitively view allocation as tightly coupled to typed objects, Alexandrescu argues that separating the two creates a much cleaner architecture: “many people approach allocation as a kind of object and everything matter but actually can isolate the two quite cleanly it turns out and then you can build a type allocators on on top of untyped allocators.”

With this clean separation, stack allocators can be composed smoothly with system allocators. Combining a stack allocator with a general fallback like `malloc` yields a tremendous performance optimization. In typical workloads, such as a high-throughput web server handling incoming requests, around 95% of allocation requests are small enough to fit completely within a fixed, fast, cache-hot stack buffer. If an unusually large request arrives, the allocator simply delegates it to `malloc`. By pairing this pattern with RAII cleanup tools like `SCOPE_EXIT`, developers get maximum performance on the happy path while preserving total correctness under edge cases.

- Stack-backed allocation handles the vast majority of small, short-lived allocations directly on the stack without heap overhead. - When an allocation exceeds the available capacity of the local stack buffer, it cleanly delegates to a fallback allocator like `malloc`. - Scope guards (such as `SCOPE_EXIT`) provide a convenient and safe idiom for releasing stack resources in proper reverse order.

[41:00](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=2460s) Alignment Hazards and the "Compulsive" Strategy

Alexandrescu moves on to the alignment requirements inside the stack allocator's `allocate` function, posing a question to the room about the necessity of rounding allocation sizes. If an allocator begins with a 16-kilobyte buffer nicely aligned to a CPU word boundary, and the user asks for a single byte, simply advancing the allocation pointer by one byte leaves the remaining buffer misaligned. As Alexandrescu warns the audience about the cascading effect: “Do you see the tsunami that's being initiated here what is it please the next allocation is going to be unaligned right how do you fix that.”

An audience member asks why one cannot be lazy and align addresses on demand only when a typed request specifically requires it. Alexandrescu concedes that lazy alignment is a completely valid technique provided type alignment metadata is carried through, but explains that his implementation opts for a simpler, "compulsive" alignment strategy: every requested size is immediately rounded up to the required alignment boundary prior to adjusting the frontier. The method checks if the rounded request fits within the remaining space, returning `nullptr` if it exceeds bounds. On reviewing his slide, he jokes about modern C++ conventions: “notice that my code is like nullptr compliant I use nullptr and actually I'm using also like the brace initializers and everything but I do see a bug in the slide which is nullptr is not blue which bothers me to no end.”

[43:20](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=2600s) Anatomy and Mechanics of a Free List

Alexandrescu shifts focus to another indispensable building block in memory management: the free list. Clarifying that a generic fixed-size list is not necessarily a *free* list, he points out that the true defining characteristic of a free list is its intrusive nature: it incurs almost zero memory overhead because it repurposes the dead payload memory of previously deallocated blocks to store its internal linked-list node pointers.

The lifecycle of a free list involves lazy initialization and opportunistic recycling. A free list starts completely empty, relying on a backing allocator for initial requests. The free list only springs to life during deallocation:

```mermaid flowchart TD accTitle: Free List Allocation and Deallocation Flow accDescr: Flowchart illustrating how free list intercepts matching size deallocations and satisfies subsequent allocations in O(1).

subgraph AllocFlow["Allocation Request"] Req["Request size S"] --> CheckList{"Free list has<br/>node for S?"} CheckList -- "Yes" --> PopHead["Pop node from head<br/>O(1) Return"] CheckList -- "No" --> BackingAlloc["Forward to<br/>Backing Allocator"] end

subgraph DeallocFlow["Deallocation Request"] FreeReq["Free block of size S"] --> CheckSize{"Size matches<br/>free list S?"} CheckSize -- "Yes" --> PushHead["Write next pointer into block<br/>Push to intrusive head"] CheckSize -- "No" --> BackingFree["Pass to Backing Allocator"] end ```

When an allocation request matches the exact chunk size managed by the free list, the allocator checks its single head pointer; if non-null, it pops and returns the node in $O(1)$ time without touching the parent allocator.

[45:28](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=2728s) The Hidden Pitfalls of Free Lists: Retention, Contention, and Cache

Despite their raw speed and $O(1)$ efficiency, free lists suffer from substantial trade-offs. The first flaw is unconstrained memory retention: free lists hold onto memory indefinitely. If an application suddenly allocates and frees one million 64-byte objects, the free list retains all one million chunks, refusing to return that capacity to the operating system or backing storage and causing severe memory fragmentation.

Fedor and other audience members raise concurrency and threading costs. Alexandrescu observes that basic free lists have terrible thread affinity: if thread $A$ allocates memory and drops it onto a shared global free list, and thread $B$ subsequently picks it up, thread $B$ inherits a cold cache and suffers multithreaded synchronization bottlenecks. When John asks why cross-thread recycling matters so much, a spirited room discussion breaks out regarding contention. Alexandrescu clarifies the industry standard defense: “anybody who defines a freelist allocator the first thing they do they make a thread local free list and that's the first line of defense there.” Each thread gets a private, contention-free mini-pool before ever falling back to a shared global list.

Beyond fragmentation and multithreading contention, Alexandrescu reveals the most non-intuitive drawback of free lists: their hostility toward the CPU hardware cache. While programmers assume reusing recently freed chunks leads to "hot" cache hits, reality often behaves in reverse. Memory being released by applications is very frequently cold because the application finished working with it some time ago. Threading a linked list through that block forces an explicit write of a `next` pointer directly into cold memory, causing avoidable cache line fills. Alexandrescu notes that “a good allocator tries to not write to cold memory it turns out memory that's being freed is very often cold.” In spite of these subtle complexities, he concludes that when sized properly for small objects, free lists remain exceptionally fast tools.

[50:43](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=3043s) Generic FreeList Allocator Mechanics and Optimizations

Andrei Alexandrescu walks through the implementation of a generic `FreeList` allocator template. The class takes a template parameter `A`, which represents the parent allocator sitting underneath the free list. Because the design is generic, this parent can be any conforming allocator—`malloc`, a stack-based allocator, or another custom design. The free list targets a specific allocation size. Pointing his cursor to the `allocate` function on the slide, he explains that if a requested size matches the targeted size and the list's root pointer is not null, the allocator simply pops the next item off the list in an $O(1)$ constant-time operation. If either condition fails, the request falls back and delegates directly to the parent allocator. Deallocation behaves symmetrically: if a freed block matches the designated size, the allocator writes the pointer to the next free node directly into the first word of the returned memory buffer to maintain an intrusive singly linked list in-place without auxiliary memory overhead; otherwise, it passes the block back to the parent allocator to free.

```mermaid flowchart TD accTitle: FreeList Allocation Logic accDescr: Flowchart demonstrating how FreeList satisfies an allocation request by checking target size and root availability, falling back to parent allocator. subgraph RequestCheck["Evaluate Request"] REQ["allocate(n)"] --> COND{"n == targetSize and root!= nullptr?"} end subgraph FastPath["FreeList Fast Path"] COND -- "Yes" --> POP["Pop node from intrusive free list (O(1))"] POP --> RET["Return pointer to memory"] end subgraph FallbackPath["Parent Delegation"] COND -- "No" --> FALLBACK["Delegate allocation to Parent Allocator A"] FALLBACK --> RET end ```

Alexandrescu highlights several practical improvements needed to transform this basic structure into a production-grade free list: - **Size tolerance (Min/Max range):** Allocators often introduce a range (for example, between 512 bytes and 1 kilobyte) where any request within the band is satisfied using fixed 1-kilobyte chunks rather than enforcing strict equality. - **Batch allocation:** Instead of requesting individual blocks from the parent allocator one at a time, the free list can allocate large multi-object blocks and thread the intrusive list through them. However, Alexandrescu cautions: “What's the problem there? It's hard to detect when you can deallocate that big block, right? So that's it's kind of more difficult.” - **Upper-bound capping:** To prevent unbounded memory consumption from freed objects accumulating indefinitely, a cap (such as 1,024 items) can be placed on the free list length; once saturated, excess deallocations bypass the list and return to the parent.

---

[54:02](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=3242s) Realistic FreeList Parameters and Composition

Alexandrescu presents a complete, realistic declaration for a `FreeList` allocator: it parameterizes the underlying allocator, a minimum size (17 bytes), a maximum size (32 bytes), an allocation batch size (8 objects at a time), and an upper capacity limit (1,024 elements). When an audience member asks how individual elements inside a batch are freed, Alexandrescu clarifies that freed elements are recycled into the intrusive list rather than returned individually to the system: “You put it back in the list, you never free it.” An upper bound of 32 means that every request between 17 and 32 bytes receives a 32-byte chunk, while the 1,024 limit caps the maximum number of idle recycled blocks held in cache.

He emphasizes the architectural power of composability: - A `FreeList` can be layered directly on top of a stack allocator. - Stack allocators normally demand strict LIFO (last-in, first-out) deallocation and cannot free arbitrary allocations made in the middle of their buffer. - Layering a free list over the stack allocator allows intermediate allocations of matching size to be recycled and reused out-of-order, bypassing the stack's strict lifetime limitations. - Alexandrescu observes: “I'm gaining composition strength here, right?”

---

[56:00](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=3360s) The Affix Allocator: Bookends, Guard Bands, and Debugging

Alexandrescu introduces the affix allocator, a design pattern where an allocator wraps a parent allocator by prepending a prefix object and appending a suffix object to each allocated block. By default, the suffix can be `void` (occupying zero bytes). The pattern provides a generic composition device with numerous applications, most notably memory safety and debugging:

```mermaid flowchart LR accTitle: Affix Allocator Memory Layout accDescr: Memory block layout showing the prefix metadata, the usable memory block payload, and the trailing suffix guard. subgraph MemoryChunk["Composite Allocated Block"] P["Prefix Metadata\n(File/Line, Size, Guard)"] U["Usable Payload\n(Returned to Caller)"] S["Suffix Metadata\n(Electric Fence / Void)"] end P --> U U --> S ```

Prefixes and suffixes act as "electric fences" or guard signatures: a known signature or canary is written before and after the client payload, and verified upon deallocation to catch buffer overruns and memory corruptions. Beyond guard bands, affix allocators are useful for tracking allocation origins by embedding the caller's source file and line number in the prefix. Referring to a plenary performance talk given that morning by Chandelle, Alexandrescu remarks on system profiling tools: Linux has the powerful `perf` tool, Windows has comprehensive visual profiling utilities, while OS X lacks equivalent built-in facilities. However, even with `perf`, tracking down memory allocation provenance remains elusive:

> “All of these great things you can see with `perf`, you can't see who's asking for memory in `perf`. You can't. It's dark matter; you can't see it. With this kind of stuff you can.”

By wrapping allocations with file and line metadata, a program can pinpoint exactly which lines of code in a massive multi-million-line codebase consume the most dynamic memory. Furthermore, an affix allocator can collect global or per-allocation statistics—such as total allocate and deallocate invocations—by configuring capacity flags and slicing the metrics after program termination.

---

[59:08](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=3548s) Static Registration Trick for Zero-Allocation Profiling

Facing a sudden time warning from the session monitors indicating only five minutes remain, Alexandrescu bargains with the room to borrow five minutes from the upcoming break, joking about attendees dozing off in their seats. Rather than skipping past tracking techniques, he shares a clever C++ implementation trick for collecting memory allocation statistics without incurring any secondary dynamic memory allocations.

The technique relies on an allocation macro (such as `ALLOCATE(...)`) that defines an internal static structure at every call site:

```cpp // Conceptual pattern explained by Alexandrescu #define ALLOCATE(size) ([] { \ struct AllocSite { \ const char* file; \ unsigned long line; \ size_t count; \ AllocSite* next; \ }; \ static AllocSite info = { __FILE__, __LINE__, 0, globalHead }; \ globalHead = &info; \ info.count++; \ return do_allocate(size); \ }()) ```

- Every allocation macro invocation instantiates a local `static` struct containing `__FILE__`, `__LINE__`, call counters, and a pointer to the `next` record. - Because these records reside entirely in static program storage, linking them into an intrusive singly linked list requires zero dynamic allocations. - At application termination, the runtime traverses the linked list from the head pointer to dump fine-grained allocation diagnostics across the entire binary without perturbation from tracking overhead.

---

[01:01:48](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=3708s) Introduction to Bitmap Block Allocators

As the session timer is extended to ten minutes, Alexandrescu introduces the `BitmapBlock` allocator. This allocator divides a contiguous memory arena into fixed-size blocks (for instance, 64-byte blocks) and tracks occupancy using a compact bitmap that uses exactly one bit of metadata per block to indicate whether it is free or in use. This produces minimal metadata overhead while offering fast bitwise scanning for available chunks.

[01:02:14](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=3734s) The Bitmapped Block and Metadata Separation

Andrei Alexandrescu details the architecture of the bitmapped block allocator, contrasting it with free lists. By assigning a single bit per contiguous block, this scheme eliminates nearly all the disadvantages associated with free lists. Allocating an arbitrary size is straightforward because one can simply claim multiple consecutive blocks. Most importantly, metadata is stored completely separate from user data, meaning the metadata stays hot in cache. Alexandrescu emphasizes this design rule: “This is a recipe for great things in memory allocation: store metadata elsewhere than the data.” Modern CPU manufacturers have heavily optimized bitwise operations, enabling high-performance scans using SIMD and dedicated instructions.

However, no allocator design comes without trade-offs. The fixed block granularity leads directly to internal fragmentation whenever a request does not perfectly align with block boundaries. Additionally, multithreaded environments introduce concurrency headaches; bit manipulation across shared memory words causes false sharing and race conditions. Despite these limitations, Alexandrescu strongly recommends bitmapped blocks as a core building block.

```mermaid flowchart TD accTitle: Bitmapped Block Layout accDescr: Separates bit metadata from contiguous payload memory blocks.

subgraph Meta ["Hot Metadata (Cache Friendly)"] BM["Bit Vector / Words<br/>(SIMD & Bitwise Scans)"] end

subgraph Data ["Contiguous Payload Storage"] B0["Block 0"] B1["Block 1"] B2["Block 2"] B3["Block..."] end

BM -.->|"tracks status of"| B0 BM -.->|"tracks status of"| B1 BM -.->|"tracks status of"| B2 BM -.->|"tracks status of"| B3 ```

- Bit manipulation benefits directly from SIMD primitives and dedicated CPU bitwise instructions. - Storing metadata away from data payloads ensures allocator bookkeeping stays hot in cache without cache-polluting client memory. - The primary disadvantage is internal fragmentation caused by fixed-size block rounding. - Multithreading introduces complications due to bit-sharing across adjacent allocation tracking words.

[01:03:42](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=3822s) Cascading Allocators, Segregators, and Composition

Alexandrescu moves on to more advanced, composable designs, beginning with the cascading allocator. Imagine placing a bitmapped block allocator atop a 1-megabyte buffer; once that megabyte is exhausted, the cascading allocator lazily allocates a second megabyte, organizes it with another bitmapped block, and chains them together. When blocks are vacated, it deallocates them cleanly.

He then introduces the segregator, teasing the audience when Marshall Clow immediately shouts out the answer: “You guys can't have Marshall answer every question here.” A segregator divides requests according to a size threshold: requests less than or equal to the threshold route to a small-object allocator, while those exceeding it go to a large-object allocator. Crucially, segregators do not need an `owns()` member function because the requested byte size alone dictates routing. Segregators can nest recursively: a primary segregator can branch requests at 4 KB, routing larger allocations to a system fallback, while sub-4 KB allocations pass to an inner segregator that splits at 128 bytes between a free list and a medium allocator. Alexandrescu notes that chaining roughly 16 segregators into a compile-time binary search tree yields an industrial-strength allocator, though a linear search can sometimes be preferable because tiny allocations are far more frequent and large allocations can afford slight routing overhead.

```mermaid flowchart TD accTitle: Segregator Allocation Hierarchy accDescr: Hierarchical tree showing request routing by allocation size.

Req["Allocate(n bytes)"] --> S1{"n <= 4 KB?"} S1 -->|"Yes"| S2{"n <= 128 B?"} S1 -->|"No"| Fallback["Large / System Allocator"] S2 -->|"Yes"| FL["FreeList Allocator"] S2 -->|"No"| Med["Medium Allocator"] ```

- Segregators route solely by size: `size <= Threshold? SmallAlloc: LargeAlloc`. - Segregator implementations take at most around 50 lines of code. - Compositions of ~16 segregators can be organized as compile-time binary search trees or linear cascades based on profile frequencies.

[01:07:03](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=4023s) The Bucketizer and the Complete Minimal Allocator API

The final structural component presented is the bucketizer. Operating across a range between a minimum and maximum size with a defined step size (or powers-of-two for logarithmic growth), the bucketizer maps size classes directly to dedicated allocators. Alexandrescu asserts that between bitmapped blocks, cascading wrappers, segregators, and bucketizers, almost every known memory allocation architecture can be expressed with minimal code.

```mermaid flowchart LR accTitle: High-Level Allocator API Map accDescr: Categorization of core allocator methods into allocation, sizing, and management.

subgraph Sizing ["Size & Alignment"] A1["alignment"] A2["good_size(n)"] end

subgraph Lifecycle ["Allocation Lifecycle"] B1["allocate(n)"] B2["allocate_all()"] B3["deallocate(b)"] B4["deallocate_all()"] end

subgraph InPlace ["In-Place Growth & Query"] C1["expand(b, delta)"] C2["reallocate(b, new_size)"] C3["owns(b)"] C4["aligned_allocate(n, align)"] end ```

He then summarizes the universal, clean allocator API discovered through this composable framework: - `alignment`: A `static constexpr` value advertising alignment guarantees. - `good_size(n)`: Returns the rounded-up capacity the allocator will actually provision for a request of size $n$, enabling containers like `std::vector` to exploit slack memory without reallocating. - `allocate(n)`: Standard allocation returning an untyped memory block. - `allocate_all()`: Captures all remaining storage in a single operation, ideal for nesting arena or stack allocators within pre-reserved memory chunks. - `expand(b, delta)`: Expands an existing allocation in place without relocating pointers. - `reallocate(b, new_size)`: Mimics C `realloc` behavior. - `owns(b)`: Identifies whether a given memory block originated from this specific allocator instance. - `deallocate(b)` and `deallocate_all()`: Reclaims individual blocks or drops all allocations at once. - `aligned_allocate(n, a)`: Supports over-aligned allocations, an unavoidable requirement in performance-critical domains like game development.

[01:10:47](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=4247s) Conclusion: Why std::allocator Failed and How Composition Wins

Alexandrescu concludes by reflecting on the core lesson of the talk: `std::allocator` failed because it lacked composability. Trapped by its historical baggage as a 1990s segmented-memory workaround, `std::allocator` conflated object construction with memory sourcing, omitted block sizing, and offered no mechanism for layering strategies. Alexandrescu admits that shedding that legacy was mentally challenging: “And it was difficult for me to get away from the frame of mind in which it exists, therefore it must be good, to the point where, you know, this may be a mistake and let me see if I kind of clean my mind off of everything, let me see if I can do better right, and better means composable.”

By discarding existing patterns and building from first principles around raw byte blocks, compile-time metadata queries, and composable structural components, developers can assemble custom, optimal memory allocators for any platform or application pattern. With the session time elapsed, Alexandrescu closes to audience applause and remains on stage for informal follow-up questions.

## Notable quotes > [01:02:14](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=3734s) This is a recipe for great things in memory allocation: store metadata elsewhere than the data. — Andrei Alexandrescu

> [11:56](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=716s) I unrecommend operator new for any serious use of memory allocation. — Andrei Alexandrescu

> [24:10](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=1450s) Getting composition right is getting the allocators right. — Andrei Alexandrescu

> [18:03](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=1083s) std::allocator is not a great allocator because it was not designed for that purpose; it was designed for near and far pointers. — Andrei Alexandrescu

## Mentioned Andrei Alexandrescu, Bjarne Stroustrup, Brian Kernighan, Dennis Ritchie, Hoard, jemalloc, TCMalloc, Howard Hinnant, Pablo Halpern, Alisdair Meredith, John Lakos, Doug Lea, dlmalloc, StackAllocator, FallbackAllocator, NullAllocator, Mallocator, FreeList, Affix Allocator, BitmapBlock, Marshall Clow, SCOPE_EXIT, std::allocator, std::vector, perf, Linux, Windows, OS X

## Video description http://www.Cppcon.org — std::allocator Is to Allocation what std::vector Is to Vexation -- Presentation Slides, PDFs, Source Code and other presenter materials are available at: https://github.com/cppcon/cppcon2015 — std::allocator has an inglorious past, murky present, and cheerless future. STL introduced allocators as a stop gap for the now antiquated segmented memory models of the 1990s. Their design was limited and in many ways wasn't even aiming at helping allocation that much. Because allocators were there, they simply continued being there, up to the point they became impossible to either uproot or make work, in spite of valiant effort spent by the community.

But this talk aims at spending less time on poking criticism at std::allocator and more on actually defining allocator APIs that work.

Scalable, high-performance memory allocation is a topic of increasing importance in today's demanding applications. For such, std::allocator simply doesn't work. This talk discusses the full design of a memory allocator created from first principles. It is generic, componentized, and composable for supporting application-specific allocation patterns. — Andrei Alexandrescu is a researcher, software engineer, and author. He wrote three best-selling books on programming (Modern C++ Design, C++ Coding Standards, and The D Programming Language) and numerous articles and papers on wide-ranging topics from programming to language design to Machine Learning to Natural Language Processing. Andrei holds a PhD in Computer Science from the University of Washington and a BSc in Electrical Engineering from University "Politehnica" Bucharest. He works as a Research Scientist for Facebook.Website: http://erdani.comTwitter handle: @incomputable — Videos Filmed & Edited by Bash Films: http://www.BashFilms.com

Work at Hudson River Trading (HRT): https://tinyurl.com/safxfctf

[Source](https://www.youtube.com/watch?v=LIb3L4vKZ7U)

中文简报

Alexandrescu提出可组合内存模型以替代std::allocator 标准C++内存分配机制之所以失败,是因为std::allocator将类型化对象构造与原始字节管理耦合在一起,因而需要一种基于无类型内存块和可组合原语的替代方案。 Alexandrescu认为,std::allocator从根本上是失败的,因为它围绕已淘汰的分段内存指针模型构建,将对象构造与无类型字节分配紧密耦合,且完全缺乏可组合性。他概述了高效内存分配器的核心需求,指出分配器必须以同时包含指针和大小的原始内存块而非类型化元素为操作单位。通过引入NullAllocator、FallbackAllocator、FreeList、Affix Allocator、BitmappedBlock以及Segregator等基础构件,他展示了开发者如何针对特定的应用程序负载组装出工业级的定制分配器。该提议的可组合API集成了owns()、good_size()、expand()以及作用域批量释放等关键操作原语,旨在不引发手动堆管理混乱的前提下实现最高的运行时效率。 std::allocator在现代C++中已彻底失败,因为其历史设计初衷是抽象16位分段x86架构,而非作为通用、可组合的内存分配框架。 (14:09) 分配器设计中真正的可组合性要求对原始无类型内存块(void*和size_t)进行操作,而非强类型对象,从而将对象生命周期管理与存储分配彻底分离。 (18:03) 在分层或回退式分配器中,若不存储内存块大小,或者不公开owns(Block)查询来跨层验证内存块来源,释放操作在数学逻辑上是无法实现的。 (30:45) 生产级通用分配器完全可以通过正交、可组合的基础构件构建而成,例如嵌套在编译期搜索树中的Segregator、FallbackAllocator、FreeList以及Bitmapped Arena。 (63:42)

Reading summary

Andrei Alexandrescu argues that C++'s standard memory facilities fail because they conflate object lifecycle with memory acquisition instead of treating composability as the core design requirement. Historical Baggage and the Flaws of std::allocator (14:09) Alexandrescu recounts that std::allocator was not designed as an allocation engine, but as an early 1990s abstraction for 16-bit segmented x86 architectures handling 20-bit near and far pointers. Once the standards committee removed segmented pointer support, the allocator became an outdated abstraction that nonetheless persisted through C++17 without native allocation algorithms. The design crippled memory efficiency by parameterizing allocators on a specific type T rather than byte size and alignment. This forced the introduction of workarounds like template rebind so containers could allocate internal bookkeeping nodes of differing types. Furthermore, std::allocator conflated memory management with object factories by embedding construct and destroy methods while trading underneath in raw void* pointers. Alexandrescu also critiques classical allocator models for treating memory managers as stateless, which forced implementations to rely on hidden global heaps. While C++14 subsequently introduced stateful allocators, Alexandrescu contends that the standard still missed the fundamental requirement of composability. API Asymmetry and the Untyped Block Model (5:18) Alexandrescu highlights a long-standing architectural flaw inherited from C's malloc and free: while allocation requests require an explicit byte size, deallocation receives only a bare pointer. This asymmetry forces the memory subsystem to store and look up hidden metadata—such as page runs or prepended chunk headers—even though calling containers often already know the size of the objects being destroyed. Alexandrescu points to standards proposals like N3778 for sized deallocation to mitigate this penalty in operator delete. To correct this foundation, Alexandrescu argues that allocators should operate strictly on untyped memory blocks consisting of a base pointer (void*) and a byte extent (size_t). Decoupling memory handling from object construction avoids the syntactic distortions and metadata loss inherent in native C++ operators like new, new[], and delete[]. Separating allocation from type mechanics allows high-level typed wrappers to be composed cleanly on top of untyped byte allocators. Deriving Composability: Null, Fallback, and Ownership (24:10) Production memory systems cannot rely on a single monolithic algorithm; real-world engines like dlmalloc or the Windows heap split allocations across size tiers, such as dedicated small-object pools, medium pools, and operating system mappings like mmap. Alexandrescu asserts that allocator design must treat composability as the primary constraint. This composable hierarchy begins with a NullAllocator that always returns null upon allocation and asserts null on deallocation, serving as an explicit pipeline terminator. Composing higher-level allocators reveals the need for missing interface primitives. A FallbackAllocator<Primary, Fallback> attempts an allocation in Primary, delegating to Fallback if the result is empty. However, deallocating a block requires knowing which allocator supplied it. To avoid prepending wasteful metadata tags, Alexandrescu introduces the owns(Block) primitive, enabling fast address-range checks to route deallocation properly. For example, composing FallbackAllocator<StackAllocator<16384>, Mallocator> allows 95% of small, short-lived allocations to use an ultra-fast stack pointer bump, while larger allocations smoothly fall back to malloc. Reusable Building Blocks: Free Lists and Bitmaps (45:28) Alexandrescu examines the mechanics and trade-offs of specialized allocator building blocks. Free lists offer constant-time O(1) recycling by writing next pointers directly into the payloads of freed buffers. However, they suffer from unconstrained memory retention, thread contention, and cache degradation, as writing linked-list pointers directly into cold memory triggers unwanted cache fills. To mitigate these issues, production free lists require size-range tolerances, batch allocations, upper-bound capacity limits, or thread-local pooling. In contrast, the BitmapBlock allocator stores allocation metadata as a compact bit vector entirely separate from client payloads. This separation keeps bookkeeping metadata hot in cache and leverages SIMD and dedicated CPU bitwise instructions to scan for consecutive free blocks, though it introduces internal fragmentation from fixed block sizes and concurrency hazards when threads share tracking words. Allocators can also be wrapped in AffixAllocators to prepend guard bands or source-location metadata (__FILE__, __LINE__) for zero-allocation debugging and memory profiling. Structural Routing and the Minimal Allocator Interface (63:42) To combine these pieces into full allocators, Alexandrescu introduces structural routing primitives. Segregators split allocation requests based on a size threshold, directing small requests to one allocator (such as a free list) and larger requests to another without needing an owns() check. Cascades can chain roughly 16 segregators into binary search trees or linear cascades. Bucketizers map specific size ranges to dedicated allocators across uniform or logarithmic intervals. From these building blocks, Alexandrescu derives a minimal, composable allocator API. Essential queries include alignment guarantees, good_size(n) to let containers leverage allocated slack capacity, and owns(b). Core lifecycle primitives include allocate(n), allocate_all(), deallocate(b), and deallocate_all(), alongside in-place modification primitives like expand(b, delta), reallocate(b, new_size), and aligned_allocate(n, a) for over-aligned requirements. Alexandrescu concludes that std::allocator failed because its legacy constraints prevented modular composition. By abandoning type coupling in favor of untyped byte blocks, fast ownership queries, and structural combinators like segregators and bucketizers, developers can assemble custom, high-performance allocators out of small, reusable components.

精炼阅读摘要

Andrei Alexandrescu在CppCon 2015演讲中指出,C++标准库中的std::allocator由于历史包袱混合了对象生命周期与内存分配,缺乏组合能力,因而无法胜任现代高性能系统的需求;他主张从第一性原理出发,使用非类型化的内存块和高组合性的微型构建块来设计内存分配器体系。 标准分配器与C语言内存API的历史缺陷 (5:18) Alexandrescu回顾了内存分配的历史演进。在C语言中,malloc(size)与free(ptr)之间存在根本性的不对称性:free不接受大小参数,迫使系统实现必须在内存块前端增加隐式包头或维护外部页表来跟踪元数据,哪怕调用方(如类型析构函数)已知该尺寸。C++标准委员会通过N3778等定长释放提案试图弥补这一性能损失。此外,语言级的operator new将类型系统绑定到底层malloc,同时割裂了内存分配与构造函数参数之间的通信,而为数组单独设计的operator new[]与delete[](Alexandrescu称之为“大数组歧义”)进一步增加了语法冗余却未提升分配效能。 针对std::allocator,Alexandrescu指出其最初设计目的是在1990年代16位x86架构上抽象段地址与偏移量构成的近指针和远指针(near/far pointers),而非为了通用内存管理。随着分段架构淘汰,委员会移除了远近指针机制,却保留了其繁杂的躯壳。std::allocator将自身参数化为具体类型T,并混入了construct和destroy等工厂方法,导致容器在分配内部节点时必须依赖复杂的template rebind机制。而在实际硬件层面,分配器核心关心的唯有字节大小与对齐约束,强行绑定对象类型反而破坏了抽象的纯粹性。 无状态假象与基于组合的设计原则 (21:44) 传统标准分配器长期假定分配器是无状态的,Alexandrescu批评这种假定脱离了物理现实:提供有限物理资源的分配器必然包含状态,宣称无状态只是将状态隐藏于全局堆或单例之中(C++14才正式接纳有状态分配器)。工业级通用分配器(如dlmalloc、jemalloc或Windows堆分配器)本质上均依赖分层分级策略,即根据请求尺寸将流量分流到不同子算法,例如小于1 KB走专用桶分配器,1 KB至64 KB采用池化策略,超大对象直接调用操作系统的mmap或VirtualAlloc。因此,生产级分配器必须是由专注的小构建块组合而成的复合系统,可组合性(composability)必须作为首要约束。 为构建组合体系,Alexandrescu定义了最底层的NullAllocator作为终止符,它在allocate时永远返回空指针,并在deallocate时断言指针为空,其作用类似于电子工程中的波导终端,为多级管道提供确定的失败边界。在实现最简单的FallbackAllocator<Primary, Fallback>时,代码暴露了仅有allocate和deallocate两项接口的结构缺陷:当调用deallocate释放一个内存块时,复合分配器无法判定该内存最初来自Primary还是Fallback,若将外部指针传给错误的底层分配器会导致严重错误。为了在不引入昂贵元数据标签的前提下解决内存归属判定问题,接口中必须引入owns()原语,通过地址区间或查找表高效识别内存来源。 基础组件:栈分配器与空闲列表的工程权衡 (37:10) 在基础分配单元中,StackAllocator管理一块固定连续内存(如16 KB),通过内部游标指针递增实现极速分配。然而其deallocate操作存在严格限制:只有当释放请求的目标恰好是最近一次分配且加上对齐填充后的前沿地址时,游标才能向后回退;任意乱序释放则只能成为空操作或依赖deallocate_all重置整个缓冲区。为避免每次分配破坏后续地址的对齐,StackAllocator必须采用强迫式对齐策略,在推进游标前将请求大小统一向上圆整至对齐倍数。将StackAllocator与Mallocator组合为FallbackAllocator<StackAllocator<16384>, Mallocator>,可以在栈上满足典型网络服务中约95%的小型短生命周期请求,耗尽后无缝退回至系统堆。 FreeList通过侵入式链表复用已释放内存的有效载荷空间存储next指针,实现O(1)的回收与再分配,可叠加在StackAllocator之上以绕过严格的后进先出(LIFO)限制。然而Alexandrescu强调FreeList存在显著缺陷:它会无休止地保留内存导致碎片化;在多线程环境下容易产生锁争用和跨线程冷缓存转移(通常需前置线程局部缓存缓冲);且在硬件层面,应用刚释放的内存往往已经在CPU缓存中变冷,侵入式写入链表指针会强行触发昂贵的缓存行重填。在工程落地时,FreeList需要引入尺寸容差区间(如17至32字节统一给32字节)、批量从父分配器获取内存以及设置容量上限(如上限1024项,饱和后退回父分配器释放)等手段来进行约束。 高级组合结构与性能调试工具 (56:00) Alexandrescu介绍了通过元数据包装分配块的Affix Allocator:在用户有效载荷前后分别添加前缀和后缀。前缀与后缀可作为金丝雀哨兵(“电网”)在释放时校验越界破坏,亦可记录调用点的__FILE__和__LINE__信息。Linux下的perf等常规剖析工具往往难以追踪内存申请的具体代码源头,前缀记录为排查多百万行代码中的内存归属提供了透明手段。针对统计监控带来的二次内存开销,Alexandrescu演示了利用宏在调用点生成局部static结构体的静态注册技巧,各静态节点在程序加载期串联成单向链表,在运行时实现零动态分配开销的全程序统计收集。 在结构组织方面,BitmapBlock使用紧凑的位图(每个块占1比特)跟踪连续区域,使元数据与数据彻底分离并保持在CPU缓存热区,支持SIMD高速位运算扫描,但也承担了固定块大小带来的内部碎片与多线程位操作伪共享风险。在此基础上,级联分配器(Cascading Allocator)在缓冲区耗尽时按需惰性链式分配新内存页;分流器(Segregator)根据阈值(如4 KB)将大小请求分流至两端,且因仅依据尺寸路由而无需查询owns();组合约16个分流器即可构成编译期二叉搜索树或线性级联结构;分桶器(Bucketizer)则按线性步长或2的幂次区间将尺寸映射到专用分配器,以此拼装出完整的工业级内存架构。 基于第一性原理的极简分配器通用接口 (67:03) 基于可组合性推导出的通用分配器接口彻底抛弃了面向具体对象的观念,转而围绕包含void*和size_t的未类型化内存块(Block)构建。Alexandrescu总结的核心API契约包含:alignment(静态对齐保证常量);good_size(n)(返回分配器实际分配的规格化容量,使std::vector等容器能够利用余量空间避免频繁重分配);allocate(n)与allocate_all()(捕获全部剩余存储,专用于嵌套arena或栈结构);expand(b, delta)(原地无位移扩容)与reallocate(b, new_size);owns(b)(块归属判定,组合去分配的核心);deallocate(b)与批量丢弃的deallocate_all();以及满足游戏开发等特殊场景的aligned_allocate(n, align)。该契约使上层有类型容器与底层无类型存储解耦,各分配策略能够自由嵌套并按需实例化成员函数。 Alexandrescu总结道,std::allocator的核心失败在于缺乏组合能力,摆脱该设计思维的途径是回归底层内存块本身。虽然基于位图块、分流器和分桶器的组合架构能够以极小代码量表达出复杂的定制分配器,但在高并发场景下跨线程竞争与位操作的同步开销仍是实际实现中需要权衡的工程难题。

Detailed notes — zh

## Background 本内容由 CppCon 官方频道于 2015 年 10 月 13 日在 YouTube 平台发布。视频记录了著名计算机科学家、Facebook 研究科学家 Andrei Alexandrescu 的主题演讲,页面简介指出,`std::allocator` 带有过时的分段内存模型历史包袱且难以适应现代高性能需求,演讲旨在从第一性原理出发,重新构建一套通用、组件化且高度可组合的现代内存分配器 API 设计。 ## TL;DR Andrei Alexandrescu 深度批判了 C++ 传统内存分配机制的缺陷,指出 `std::allocator` 将类型绑定、工厂模式与无状态假设强加于分配器,并彻底忽略了最重要的“可组合性”。为了解决这一历史困局,他从第一性原理推演,提出底层分配器应当仅与纯粹的内存块(指针与大小)打交道,并引入了 `owns` 等核心原语以支持安全的跨分配器分流。随后,他展示了通过将 `NullAllocator`、`FallbackAllocator`、`StackAllocator`、`FreeList`、`AffixAllocator`、`BitmapBlock`、`Segregator` 以及 `Bucketizer` 等微型积木相互嵌套组合,用极少且高度内聚的代码即可拼装出媲美工业级分配器的复杂分层架构。最终,他系统归纳了一套包含尺寸推断、对齐保障与批量释放等完备契约的现代化内存分配 API 规范。 ## Atmosphere & style 整场演讲充满强烈的批判锋芒与极富感染力的极客幽默,开场便以“我不是为了和平而来,我来是为了搞破坏”奠定戏谑而犀利的基调。Andrei 游刃有余地在历史考据与硬核底层推演之间穿梭,频繁与台下诸如 Howard Hinnant、Pablo Halpern 等 C++ 委员会重量级嘉宾展开机锋相对的互动调侃,现场掌声与笑声不断。随着代码推导层层推进,观众能够真切感受到那种从繁琐混乱的标准困境突围、走向极简模块化美学时的豁然开朗与智力愉悦。 ## Bulletin - [14:09](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=849s) Andrei Alexandrescu 深入批判了现有的 std::allocator 架构,指出其本质是 20 世纪 90 年代为 16 位分段寻址妥协的过时产物,存在无状态悖论与严重缺乏组合能力等根深蒂固的问题。 - [24:10](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=1450s) 提出高性能内存分配的核心在于“可组合性(Composability)”,优秀的多级分配器应当通过极简基础构件(如 NullAllocator、FallbackAllocator、Segregator)相互嵌套组装而成。 - [30:03](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=1803s) 通过引入 owns 原语,解决了多分配器路由释放的难题,并阐明了将元数据与用户数据物理隔离(如 BitmapBlock)是保障高缓存局部性与 SIMD 硬件加速的黄金法则。 - [01:08:43](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=4123s) 提炼了一套基于第一性原理的现代分配器契约规范,涵盖 alignment、good_size、expand 及 deallocate_all 等按需可选接口,使底层无类型内存管理与上层类型系统彻底解耦。 ## Key takeaways - [05:18](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=318s) C 风格 malloc 与 free 的核心缺陷在于尺寸管理的不对称性:free 不接收大小参数,迫使底层实现必须为每一块内存隐式追踪元数据,严重拖累性能。 - [08:35](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=515s) C++ operator new 在语法和语义上存在严重缺陷,不仅在处理数组分配时引入了容易混淆的配套法则,还过早丢弃了有价值的类型信息。 - [14:09](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=849s) std::allocator 最初是为 16 位系统的 Near/Far 指针设计的历史补丁,将其作为类模板强绑定具体类型是方向性的抽象错误,因为分配器本质只关心大小与对齐。 - [18:03](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=1083s) std::allocator 职责越界,试图同时充当工厂角色(暴露 construct/destroy),并且在早期假定分配器无状态,违背了现代多级内存管理的基本逻辑。 - [24:10](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=1450s) 将可组合性(Composability)作为分配器最核心的设计考量;现代高性能分配器本质上都是由多种不同策略按照尺寸阶梯嵌套组合的流水线。 - [25:25](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=1525s) NullAllocator 扮演类似电路终端器的兜底角色,allocate 始终返回 null,deallocate 断言检查指针,是构建组合链的极简基石。 - [30:03](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=1803s) FallbackAllocator 尝试先在主分配器分配,失败则退回后备分配器;该模式倒逼出 owns 原语,通过地址范围判定归属来正确分流释放调用。 - [37:10](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=2230s) 基于缓冲区的 StackAllocator 仅在待释放内存块恰好位于栈顶游标处时支持条件释放,并提供代价极低的 deallocate_all 整体重置原语。 - [40:07](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=2407s) 游标分配必须严谨处理内存对齐,如果不将每次请求向上圆整到合法字长边界,极易引发后续分配连环越界的“对齐雪崩”。 - [43:20](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=2600s) 侵入式 FreeList 带来 O(1) 极速复用的同时,也存在内存滞留碎片化、多线程争用严重以及强制向冷内存写入指针破坏 CPU 缓存的物理代价。 - [52:11](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=3131s) 工业级 FreeList 应当引入容差区间、批量分配以及链表容量上限机制,防止未使用的内存无休止膨胀导致隐式内存泄漏。 - [56:00](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=3360s) AffixAllocator 允许在外层透明注入 Prefix 和 Suffix 元数据,可用于实现防缓冲区溢出的金丝雀围栏以及精准定位内存申请源头(__FILE__/__LINE__)。 - [59:08](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=3548s) 利用宏内定义的局部静态结构体变量串联侵入式单向链表,可以在运行期零动态内存分配的前提下精准追踪全程序的内存统计热点。 - [01:02:14](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=3734s) BitmapBlock Allocator 践行了“元数据与数据分离”的准则,极高的缓存热度配合 SIMD 位运算扫描极为迅速,后续可通过 Cascading Allocator 按需懒加载追加大块。 - [01:04:39](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=3879s) Segregator(分流分配器)根据阈值二分请求,可自组合嵌套为二叉搜索树;Bucketizer(分桶分配器)则按步长将区间切分为尺寸桶,覆盖了主流分配器的调度逻辑。 - [01:08:43](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=4123s) 终极分配器 API 规范应将底层无类型的内存块操作(包括 good_size、expand、aligned_allocate 等)与上层对象构造完全解耦,以微型原语实现高内聚与灵活组装。 ## Detailed walkthrough **我的思考过程:深入研究分配器和 `new` 运算符** 我将深入分析这段关于分配器和 `new` 运算符的讨论。我的目标是提供一个忠实、细致的演练,以简体中文编写。 ### [00:00](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=0s) 开场白和标题讨论 一开始,我开玩笑地讨论了我的工作标题,例如“std::allocator 是分配的烦恼,就像 std::vector 是向量的烦恼一样”。我并没有带来和平,而是想“摧毁”(或者批判)当前的状态。 * **我在这里不是为了和平...我来是为了破坏。** ### [01:53](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=113s) `malloc` 的历史和演变 然后,我将探讨为什么关注分配器。我引用了 Bjarne Stroustrup 关于为什么我们在乎的观点。在 1983 年 K&R C 时代,`malloc` 是一个巨大的进步,因为它不需要汇编。我将其与当时的 Pascal、Lisp 和 Fortran 进行了比较。分配器性能的周期:从天真、到糟糕,再到像 `jemalloc` 和 `tcmalloc` 这样的优秀分配器。 * **这是 C++,这就像国王的终极论点,我们关心。** ### [05:18](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=318s) API 中的缺陷(大小管理) 我将重点放在 API 设计的缺陷上。`malloc`(传递大小)和 `free`(不传递大小)之间的不对称性。这迫使分配器内部跟踪大小(元数据),这对性能不利。我提到了 Howard Hinnant(标准委员会成员)也同意更改这一点是一场噩梦。我提出了一个“块”结构的想法。 flowchart LR A["块结构"] --> B["指针 (void*)"] A --> C["大小 (size_t)"] * **大小管理是我们在谚语中讨厌的痛苦。这是一件坏事。** ### [08:35](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=515s) `operator new` 的问题 最后,我将讨论 `operator new` 和 `malloc`。`new` 带来了类型信息,但随后通过转发到 `malloc` 而将其丢弃。我提到了语法上的怪异之处,例如 `new int[100]` 与 `new int`。并提到 `realloc` 很难正确使用。以及 `new` 和构造函数之间缺乏沟通。 * **运算符 new 被设计成与 Malloc 一起工作,但你知道它没有。** ### [00:00](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=0s) 关于标题的幽默与演讲基调 Andrei Alexandrescu 开场时带着一丝幽默和调侃,向观众解释了他这次演讲的拟定标题:“`std::allocator` is to allocation what `std::vector` is to vexation”(`std::allocator` 之于内存分配,正如 `std::vector` 之于烦恼)。他承认这个标题在与人交流时引起了一些困惑,甚至联想到了“短吻鳄”(alligator)和“鳄鱼咬坏洞洞鞋(Crocs)”这种荒谬的联想。他通过这种轻松的开场迅速活跃了气氛,并明确了这次演讲的主题:他并不是来客套的,也不是来赞美现状的,而是要对当前内存分配机制进行深度的批判。 他郑重地声明,他并不是来通过贬低 `std::allocator` 来找乐子的,因为那实在太容易了。他真正的目的是讨论如何编写高质量的内存分配器,探讨那些“设计上行不通”的分配器 API。他用一种近乎挑衅的方式宣布了自己的意图:“I'm not here in peace, I'm here to destroy.”(我不是带着和平来的,我是来搞破坏的。)他甚至开玩笑说,他喜欢这种语法上的“恶意”,把“not work”变成一个动词,以此展示他对语言结构那种独特的严谨又恶搞的态度。 ### [01:53](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=113s) 内存分配的历史演进与轮回 演讲转入严肃的议题,Andrei 提到内存分配是一个充满“轮回”的领域。他引用了 Bjarne Stroustrup 的话来强调这个话题的重要性,指出在 C++ 的语境下,开发者确实应当关心底层实现。他回顾了 1983 年 K&R C(Kernighan and Ritchie《C 程序设计语言》)时代,当时 `malloc` 的出现是一个巨大的里程碑。对于当时的程序员来说,能够在没有汇编语言的情况下编写内存分配器简直是“神迹”,它第一次让开发者能够直接控制内存布局。 Andrei 将当时的 C 语言与 Pascal、Lisp 和 Fortran 进行了对比,指出在那些语言中实现类似的底层控制是极其困难甚至不可能的。然而,随着时间推移,这种对动态内存分配的狂热带来了一系列的挑战。他描述了一个循环:早期 naive 的 `malloc` 表现尚可,后来因为各种缺陷变得非常糟糕,再到后来诸如 `jemalloc` 和 `tcmalloc` 等新一代分配器的出现,再次提升了性能。现在的状态处于一个“中间地带”,既不是完美无缺,也不是一无是处。 * **技术观点**:内存分配器的发展并非线性进化,而是一个性能与设计权衡的循环。 * **关键引用**:“This is C++ and this is like the ultimate argument of Kings we care.”(这是 C++,这是帝王般的终极辩论,我们在意。) ### [05:18](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=318s) API 设计的缺陷:尺寸管理的痛点 Andrei 深入探讨了 `malloc` 和 `free` API 设计中的一个核心缺陷:不对称性。当你调用 `malloc` 时需要传入大小,但调用 `free` 时却不需要。这意味着分配器内部必须管理并记录每一块内存的大小,这在性能上是一个沉重的负担。为了证明这一点,他提到了与 C++ 标准委员会成员 Howard Hinnant 的交流,对方也表示修改内存分配相关的标准 API 是一场噩梦,因为“曾经沧海难为水”,历史包袱太重,很难做出任何改变。 他指出,如果能够重写历史,我们应该采用一种更合理的结构,例如将指针和大小封装在一个 `block` 结构体中,这样在释放内存时就能直接利用这个结构,从而彻底解决尺寸追踪的问题。 * **核心痛点**:`malloc` 必须隐式管理内存块大小,这是现代高性能内存分配器的巨大性能瓶颈。 * **关键引用**:“Size management is a kind of a complete pain in the proverbial we hate it it's a bad thing to.”(尺寸管理简直是一种无法言说的痛苦,我们讨厌它,这是一件糟糕的事情。) ```mermaid flowchart LR A["Block 结构体"] --> B["Pointer (void*)"] A --> C["Size (size_t)"] style A fill:#f9f,stroke:#333,stroke-width:2px ``` ### [08:35](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=515s) 深入剖析 `operator new` 的设计问题 接下来,Andrei 将矛头转向了 C++ 中的 `operator new`。他承认 `new` 的优点在于它携带了类型信息,这比原始的 `malloc` 要好得多。然而,问题在于它紧接着就把这些信息“扔掉了”,因为它最终还是要转发给 `malloc`。他详细拆解了 `new` 在语法上的怪异之处,特别是在处理数组时。他指出,`int[100]` 是一个类型,但当你把它放在 `new` 后面时,`new` 的语法解释方式完全不同,它并不是分配一个固定大小为 100 的数组并返回指针,而是会产生一种程序员意料之外的动态分配行为。 他最后批评了 `new` 与构造函数之间的通信缺失问题,指出这对于任何尝试编写类级 `operator new` 的开发者来说都是一个“痛苦的教训”。他认为,尽管 `operator new` 有其存在的价值,但其设计并未完全达到预期的优雅。 * **语法陷阱**:`new int[100]` 的语义解析常常让开发者困惑,因为它改变了类型信息的传递逻辑。 * **关键引用**:“Operator new was designed to work with Malloc but you know it didn't.”(`operator new` 的设计初衷是与 `malloc` 配合工作,但你知道,它实际上并没有做到。) ### [11:56](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=716s) 运算符重载的弯路:从类级 operator new 到数组 operator new[] Andrei Alexandrescu 针对 C++ 中类作用域的 `operator new` 给出了极其直白的忠告:程序员能对它耍出的“最佳花招”,就是把它彻底忘得一干二净,假装这门语言里从来没有过这个特性。他开玩笑说,甚至应该成立一个“匿名 operator new 戒瘾互助会”(Operator New Anonymous, 简称 OA),大家真应该去参加聚会。他甚至调侃现场亦步亦趋的摄像师,称如果这是座监狱而自己打算越狱,这名时刻紧跟的摄像大哥绝对会成为最大的灾难。 随后,Andrei 提出了他热爱的缩写概念——“伟大的数组分心症”(The Great Array Distraction,缩写为 TGAD)。在 C++ 演进历史上,当语言设计者搞出了古怪的 `operator new` 之后,面对数组分配又不得不引入了带方括号的 `operator new[]`,并且配套订立了一条铁律:如果用普通 `new`,就必须配对调用普通 `delete`;如果用了数组版本的 `new[]`,就必须严格配对调用 `delete[]`。现场有听众指出如果配对错误,程序只会执行碰到的第一个析构函数。对此,Andrei 直言这完全没有存在的必要: “说实话,这完全没有任何理由,它根本就不应该存在,纯粹是让每个人的生活都变得艰难。在任何严肃的内存分配场景中,我都不推荐使用 operator new,你可以把这句话当作我的定论引用出去。” ```mermaid flowchart TD accTitle: C++ operator new 与 delete 历史配对陷阱 accDescr: 展示标量与数组版本的 new 和 delete 必须严格配对,以及混用时导致的析构错误 subgraph Scaler["标量分配路径"] N1["operator new"] -->|"配对"| D1["operator delete"] end subgraph Array["数组分配路径"] N2["operator new[]"] -->|"配对"| D2["operator delete[]"] end Scaler -.->|"错误混用语法"| D2 Array -.->|"错误混用(仅调用首个析构函数)"| D1 ``` --- ### [14:09](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=849s) 揭开 std::allocator 的身世之谜与 Alisdair Meredith 的演讲 顺着历史脉络,Andrei 指向了核心议题——`std::allocator`。他抛出了一个石破天惊的事实:`std::allocator` 最滑稽的地方在于,它从一开始就不是为了内存分配而生的。台下听众先后猜测它是为了“对象构造”或“指针类型抽象”,坐在前排的 Pablo Halpern 也随之确认。Andrei 指出,它的真正起源是为了应对 16 位系统下的 Near 指针与 Far 指针。在那段上古时代,系统使用 20 位的段加偏移量(Segment and Offset)模型来寻址,而这段早已被历史淘汰的软硬件插曲,最终沉淀成了 C++ 标准中尾大不掉的包袱。 Andrei 借用了一个生动的比喻:一辆装满轮子的大卡车在路上开,其中有一个轮子是方的,而那个方轮子就是 `std::allocator`。即使到了 C++17,它依然赖在标准库里没有死掉。为了证明 `std::allocator` 到底有多失败,他提到了 Alisdair Meredith 前一年在同一会议上作的题为《让分配器工作起来》(Making Allocators Work)的演讲。Andrei 进行了无情的“语言学剖析”: - 那个演讲发生在 2014 年,而 `std::allocator` 在 1994 年以前就已经定型,整整过去了 20 年; - 那是一场长达 180 分钟的双倍时长马拉松讲座(连续两个 90 分钟); - 演讲标题居然叫“让它工作起来”。世界上从来没有一场技术讲座需要叫《让排序工作起来》(Making Sort Work)或者《让 std::vector 工作起来》;这个标题本身就是无可辩驳的罪证(Smoking Gun); - Andrei 听完了那场演讲,震惊地发现整场报告连一张专门讨论“内存分配”本身的幻灯片都没有。 Andrei 调侃说,自己后来发邮件管 Alisdair 索要讲义用于引用,结果对方连一封信都没回——因为对方显然很清楚,Andrei 拿到幻灯片后肯定会当众开涮。 --- ### [18:03](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=1083s) 设计缺陷剖析:强类型绑定与工厂模式的错位 Andrei 严肃地指出:“我这次来可不是为了讲和的,朋友们,绝非为了和平而来。”他强调,C++ 委员会后续甚至把关于 Near/Far 指针的内容从 `std::allocator` 中彻底剥离了,这等于亲手阉割了它最初存在的唯一理由。随后,他列举了 `std::allocator` 的一系列根本性硬伤: 第一,**将类型作为分配器的模板参数**。从本质上讲,底层的内存分配根本不需要关心具体是什么类型,它唯一关心的两件事是**大小(Size)**和**对齐(Alignment)**: - 分配一个 `int` 和一个 `unsigned int`,从分配器的角度看完全没有区别; - 就算分配两个完全不同的类型 `T1` 和 `T2`,只要它们的大小都是 128 字节且对齐一致,分配策略就完全相通;一个为 128 字节打造的高效分配器,可以同时服务于这两种类型。 第二,**分配器职责越界**。`std::allocator` 试图扮演一个对象工厂(Factory)。它不仅负责内存申请与释放,还硬生生塞进了 `construct` 和 `destroy` 成员函数去管理对象生命周期。Andrei 现场向委员会成员 Howard Hinnant 确认这两个接口是否还健在,在得到肯定答复后忍不住爆粗调侃委员会“这帮混蛋居然还没把它俩删掉”。他指出,既然宣称有工厂的功能,其底层核心却又在交换裸露的 `void*`,这种割裂令人费解,甚至还引入了像 `rebind` 这样令人痛苦的补丁设施。他强调,现代分配器理应只在内存块的层面上运作,只跟 `void*` 和 `size_t` 打交道。 --- ### [21:44](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=1304s) 无状态的悖论与可组合性(Composability)的缺失 `std::allocator` 另一个荒谬的早期假设是:**分配器是无状态的(Stateless)**。Andrei 讥讽这根本就是逻辑上的矛盾修辞(Oxymoron):一个源源不断对外提供真实物理内存的分配器,自己居然没有状态。这就像一千零一夜里辛巴达的神奇空瓶子,明明是空的,却能永远倒出水来,纯属计算机科学里的“神棍巫术”。 现实中的所有分配器都有状态。哪怕像系统底层的 `malloc`,表面上不用传实例,内部也是依靠维护全局状态或单态模式(Monostate Pattern)来工作的。尽管在 C++14 中,得益于坐在台下的 John Lakos 和 Pablo Halpern 的推进,分配器终于被允许拥有状态,但 Andrei 指出他们依然忽略了最核心的能力——**可组合性(Composition)**: “如果你想写一个分配器,你必须让它具备可组合性。如果去翻阅任何成熟分配器的实现文档,你都会发现它是由数种截然不同的分配策略组合而成的。” 以著名的 Doug Lea 分配器(dlmalloc)为例,其实现逻辑的第一句话通常就是条件分支判断:如果请求的内存小于 1 KB,就采用一种特定的策略;如果是介于 1 KB 与更大尺寸之间,则转而采用另一种策略。内存分配天然是由小构件层层组装而成的流水线,而缺乏组合能力的分配器架构从根基上就是残缺的。 - 无状态分配器的悖论:内存是实体资源,分配追踪必然带来状态管理,无论是线程局部堆、全局锁表还是游标指针; - C++14 的贡献者:现场被点名的 John Lakos 和 Pablo Halpern 推动了有状态分配器(Stateful Allocators)的标准化,Andrei 开玩笑让台下听众先别忙着朝他们扔西红柿; - 组合策略的现实代表:Doug Lea allocator (dlmalloc) 依据尺寸梯队(Size Thresholds,例如以 1KB 为界)自动分流调度,这是构建高性能内存系统的标配思路。 ### [24:10](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=1450s) 分配器的核心在于“组合” 现代内存分配器的架构本质上是多种策略按尺寸分流的组合体。Andrei 指出,对于 1 KB 到 64 KB 的内存分配,系统会采取一套策略;超过这个范围,就会直接退回调用 `mmap` 或类似的系统调用;即使在 Windows 分配器中也能看到一模一样的思路:小对象走一套机制,中等对象走完全不同的机制,大对象可能直接报错返回内存不足。进一步拆解,在那个专管 64 KB 以下的小对象分配器内部,其实嵌套着一整套小型分配器数组,每个分配器各管特定的尺寸范围,并辅以各种 bucket 设计。 ```mermaid flowchart TD accTitle: 现代分配器的多级组合架构 accDescr: 展示根据请求内存尺寸将请求路由到不同分配策略的多层组合模型。 subgraph Router["尺寸分流策略"] REQ["内存请求"] --> SML{"尺寸判定"} SML -->|"<= 64 KB"| SA["小对象分配器数组"] SML -->|"> 64 KB"| LA["系统调用 (如 mmap)"] end subgraph Buckets["小尺寸内部实现"] SA --> B1["Bucket 1 (细分尺寸 A)"] SA --> B2["Bucket 2 (细分尺寸 B)"] SA --> BN["Bucket N (细分尺寸 N)"] end ``` 如果想要设计出优秀的分配器,必须将“组合(Composition)”作为最优先的设计考量。正如 Andrei Alexandrescu 强调的那样:“Getting composition right is getting the allocators right.”(把组合做对了,分配器就做对了)。因此,整个演讲后续的重点全部放在可组合分配器(composable allocators)的设计上。 --- ### [25:25](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=1525s) 最简单的可组合分配器:NullAllocator 为了引出组合的设计理念,Andrei 让听众闭上眼几秒钟,随后抛出问题:“你能想象到的最简单的可组合分配器是什么?”观众席有人回答是 `NullAllocator`(空分配器)。Andrei 幽默地借用美剧《宋飞正传》的名梗形容它:“It is a show about nothing. Okay, it does nothing, but it does it in style.”(这是一出“无事生非”的空剧。它什么都不干,但干得极具风格)。 `NullAllocator` 的行为非常极端且明确: - 每次调用 `allocate`,它永远只返回 `null`。 - 每次调用 `deallocate`,由于规范要求传入 `deallocate` 的指针必须是先前由 `allocate` 返回的,因此它通过断言(assert)确保传入的指针必为 `null`。 - 它既可以被视作拥有“0 内存”,也可以被视作拥有“无限内存”,完全取决于视角。 - 它在工程上扮演着电路工程中波导管“终端器(Terminator)”的角色,适合挂在长长分配链的末尾兜底。 ```mermaid flowchart LR accTitle: NullAllocator 行为逻辑 accDescr: NullAllocator 在调用 allocate 和 deallocate 时的行为机制。 subgraph NullAlloc["NullAllocator"] direction TB A["allocate(bytes)"] -->|"始终返回"| B["null"] C["deallocate(p, bytes)"] -->|"断言检查"| D["assert(p == null)"] end ``` --- ### [27:16](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=1636s) FallbackAllocator 的诞生与设计推演 观众接着提出 slab、pass-through 或 stack 分配器,但 Andrei 指出接下来的关键不是单一分配器,而是实现最简单的组合分配器:`FallbackAllocator`。它持有两个子分配器(主分配器 Primary 与备选分配器 Fallback),其核心逻辑非常直接:先尝试在第一个分配器中分配,如果失败,就退而求其次尝试第二个。 Andrei 提到,一旦从可组合性出发,所有原语的演进就会像多米诺骨牌一样顺理成章地自动呈现,工程师甚至“不需要变聪明”:“To not have to be smart is a great thing in software engineering, right? Because it means you have the right frame of thinking and the right frame of reference, and there's only one way to follow the tunnel.”(在软件工程中,不需要自作聪明是一件绝妙的好事,因为这意味着你拥有了正确的思考框架与坐标系,隧道前方只有一条清晰的路可以走)。 在具体的 C++ 类设计上: - `FallbackAllocator<Primary, Fallback>` 通过私有继承(private inheritance)包含这两个分配器,这样既可以在幻灯片有限的空间内排版,又能利用空基类优化(Empty Base Optimization, EBO)。 - 分配逻辑只需要三行代码:先调用 `primary.allocate(size)`,若拿到的指针为空,则转而调用并返回 `fallback.allocate(size)`。 ```mermaid flowchart TD accTitle: FallbackAllocator 的 allocate 流程 accDescr: FallbackAllocator 尝试从 Primary 分配,若失败则退回到 Fallback 分配的逻辑分支。 A["allocate(bytes)"] --> B["Primary.allocate(bytes)"] B --> C{"返回值是否有效?"} C -->|"是"| D["返回有效内存块"] C -->|"否 (返回 null)"| E["Fallback.allocate(bytes)"] E --> F["返回 Fallback 的结果"] ``` --- ### [30:03](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=1803s) 释放难题倒逼新原语:`owns` 当进入 `deallocate` 的实现时,Andrei 开玩笑说这就像问“这条裤子显我屁股大吗”一样的陷阱问题(trick question):仅仅依靠现有的 `allocate` 和 `deallocate` 接口,根本无法写出正确的释放逻辑。如果拿到了一个指针,分配器怎么知道它究竟该还给 Primary 还是还给 Fallback?直接传给错误的分配器会导致未定义行为或程序崩溃。 为了解决释放路由的问题,分配器必须提供一个全新的原语——`owns`。`owns` 负责查询某个内存块是否归该分配器所有(例如通过简单的地址范围检查来判断)。这样,`deallocate` 的四行代码实现便水到渠成: ```cpp if (primary.owns(b)) { primary.deallocate(b); } else { fallback.deallocate(b); } ``` 关于 `owns` 的设计约束与机制: - Primary 分配器**必须**实现 `owns`,否则无法安全分流释放操作。 - Fallback 分配器**不一定**需要实现 `owns`,它往往是像标准库 `malloc` 那样粗暴且对所有权一无所知的兜底分配器。 - `FallbackAllocator` 自身只有在 Primary 和 Fallback 同时实现了 `owns` 时,才能对外提供 `owns`,其判定为二者的逻辑或(`primary.owns(b) || fallback.owns(b)`)。 - 这依赖于模板类的特性,Andrei 调侃这是他最爱的概念之一:**MDEINAE**(Method Definition Failure Is Not An Error,方法定义失败并非错误)——在 C++ 类模板中,只要未被调用的成员函数,即使内部语法无法实例化也不会引发编译报错。 ```mermaid flowchart TD accTitle: FallbackAllocator 的 deallocate 流程 accDescr: 使用 owns 原语对内存块来源进行检查并路由到对应分配器释放。 A["deallocate(block)"] --> B{"Primary.owns(block)?"} B -->|"true"| C["Primary.deallocate(block)"] B -->|"false"| D["Fallback.deallocate(block)"] ``` 观众席提问:能否不引入 `owns`,而是每次分配时多申请一个 bool 标记的头部空间来记录来源?Andrei 回应称,这在空间和时间上都是巨大的浪费;而在很多特定分配器中,`owns` 往往只是一行极速的指针范围比较,绝不能为了绕过接口设计而给每个小块内存强行增加元数据开销。 --- ### [35:44](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=2144s) 组合初显威力:栈分配器加 Malloc 至此,仅凭几行极简的代码组合,一套实用的系统已经诞生:可以用一个固定 16 KB 连续缓冲区的栈分配器(`StackAllocator`)充当 Primary,分配时简单递增游标(bump pointer);一旦 16 KB 耗尽,`FallbackAllocator` 自动无缝降级到系统级的 `Mallocator`。 Andrei 随即抛出下一个讨论话题:“我们能在纯栈分配器(`StackAllocator`)里实现内存释放(deallocation)吗?”面对台下迟疑的反应,他发起举手投票:谁赞成“能”,谁赞成“有条件的能(qualified yes)”?台下观众包括 John 纷纷表示必须是“有条件的能”,Andrei 借此引出类型系统中的限定符(qualifier)笑话,并借此展开对更精细内存归还行为的探讨。 ### [37:10](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=2230s) 栈分配器的条件释放与基本语义 在讨论基于缓冲区的栈分配器(Stack Allocator)时,Andrei Alexandrescu 深入剖析了释放内存(`deallocate`)的内在逻辑。栈分配器的行为类似于系统调用栈:分配时指针不断向前移动,指针之前是已分配内存,指针之后则是可用空间。因此,当外部请求释放某块内存时,分配器必须先进行判断。计算方式是将待释放内存块的起始指针,加上对齐后的块大小;如果该地址恰好等于当前栈分配器内存池的末尾指针,释放才能真正执行。 ```mermaid flowchart LR accTitle: 栈分配器内存布局与释放判断 accDescr: 展示栈分配器中已分配内存与未分配空闲内存的关系,以及最后一块分配如何支持回滚。 subgraph Pool["栈分配器内存池 (Stack Buffer)"] direction LR A["历史分配 (不可单独释放)"] --> B["最后一次分配 (Top)"] B --> P{"当前分配指针 (Top Pointer)"} P --> C["剩余空闲内存"] end ``` 用通俗的语言来说,只有当这次释放请求恰好对应“最后一次发生的分配”时,分配器才能执行回滚操作,将游标指针安全地往后退一步;如果待释放的块位于内存池的中间位置,则分配器无能为力。这就形成了一种独特的“条件释放(Conditional Deallocation)”。同时,这类分配器通常还包含极简的成员函数: - `owns`:通过极其廉价的指针范围比较,检查某块内存是否落在自身的缓冲区内; - `deallocate_all`:一种既极其廉价又充满风险的原语,它直接重置整个游标指针,让所有内存重新可用。 面对台下观众关于对象类型与限定符的提问,Andrei 明确界定了底层分配器的职责边界:“在这个层面,我只关心 `void*` 和大小,仅此而已,不涉及任何复杂机制。”很多开发者在设计内存分配时总是将它与“对象”及其构造析构杂糅在一起,但实际上两者完全可以清晰地解耦。正确的架构应当是先构建不感知类型的无类型底层分配器,再在此之上封装具备强类型感知的高级分配器。 ### [40:07](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=2407s) 对齐问题与栈内存回退模式 在实际工程中,将栈分配器与后备分配器(如 `malloc`)结合是一种极其强悍的优化模式。Andrei 指出,对于绝大多数日常场景(例如 Web 服务器处理请求),大约 95% 的内存分配请求都足够小,可以直接放入栈上的极速局部缓冲区中。只有当出现罕见的超大内存请求时,才自动委托给底层的 `malloc` 处理。配合基于 RAII 的 `scope(exit)` 语义,不仅能够彻底榨干 CPU 缓存的高性能优势,还能保证异常安全与正确性。 ```mermaid flowchart TD accTitle: 对齐雪崩效应与防护机制 accDescr: 展示请求非对齐字节数时,强制向上对齐如何阻止指针偏离对齐边界。 Req["分配请求 (如 1 字节)"] --> Check{"策略选择"} Check -->|"未强制对齐"| Bad["指针前移 1 字节 (基址 + 1)"] Bad --> Disaster["下一次分配地址完全失准 (对齐雪崩)"] Check -->|"向上对齐 (Round to Aligned)"| Fix["大小圆整至对齐倍数 (如 8 字节)"] Fix --> Safe["指针保持在合法的字长边界"] ``` 然而,游标推进必须严谨处理内存对齐(Alignment)。假设初始缓冲区完美对齐到机器字长,若程序请求分配 1 个字节,游标如果不加处理地向前移动 1 字节,就会从对齐地址落入非对齐地址。Andrei 将这种隐患形容为一场即将爆发的“对齐雪崩”,因为接下来的每一次分配都将从非对齐边界开始,导致严重的硬件访问惩罚甚至硬件异常。解决此问题最简单稳妥的方案是“强制向上圆整”:无论传入的大小是多少,进入分配逻辑的第一步就是先将其向上圆整到对齐边界。有观众提出是否可以采用“惰性对齐(Lazy Alignment)”,即只在下一次分配确实有对齐需求时再调整偏移。Andrei 认可这也是一种有效且无明显硬伤的思路,但通常需要类型信息的辅助。 - 幻灯片代码细节:实现完全遵循现代 C++ 标准,使用 `nullptr` 与列表初始化(`{}`)。 - 演讲花絮:Andrei 调侃自己的 PPT 语法高亮,“幻灯片里有个 Bug,`nullptr` 居然不是蓝色的,这让我抓狂得要命。” ### [43:20](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=2600s) 空闲列表(Free List)的核心机制与固有代价 随后,Andrei 转向了内存分配领域另一大基石模式:空闲列表(Free List)。针对台下观众对固定大小分配器的描述,他纠正指出,常规的“固定大小列表”与“空闲列表”有着本质不同。Free List 之所以冠以“Free”,关键在于它具有极低的内存开销——它采用侵入式指针(Intrusive Pointer)结构,直接借用被归还(已释放)的空闲内存本身来存放链表节点的指针。 空闲列表启动时通常处于全空状态,没有预置节点。初期的内存分配全部穿透并委托给后备分配器(Backup Allocator)。一旦上层触发 `deallocate` 且大小恰好命中该空闲列表关注的规格,分配器便会“苏醒”,将这块被释放的内存串入单向链表中,整个分配器内部往往只需维护一个指向链表头的指针。后续若有相同大小的分配请求,分配器即可在 $\mathcal{O}(1)$ 常数时间内瞬时返回头节点,极度高效。 ```mermaid flowchart TD accTitle: 空闲列表 (Free List) 权衡分析 accDescr: 展示空闲列表高吞吐优势背后的多线程、碎片化及缓存命中缺陷。 FL["空闲列表 (Free List)"] FL --> P1["优点: O(1) 极速分配与归还"] FL --> P2["优点: 侵入式设计零额外存储开销"] FL --> N1["缺点: 内存不退还导致的潜在碎片化"] FL --> N2["缺点: 多线程争用 (Thread Contention)"] FL --> N3["缺点: 缓存不友好 (向冷内存写入指针)"] ``` 然而,这种激进的优化带来了不可忽视的代价,Andrei 引导现场观众展开了热烈讨论: 1. **内存无法返还与碎片化**:一旦程序在某个时刻突发分配了数百万个 64 字节的对象随后全部释放,空闲列表会将它们死死攥在手中而不交还给底层系统,造成极其严重的内存滞留和碎片。 2. **多线程亲和性极差与锁争用**:正如观众 Fedor 所补充,全局空闲列表的线程亲和性非常糟糕。一个线程刚刚申请并写入的对象,释放后被挂入全局列表,纳秒之后被分发给另一个完全不相关的线程,不仅引发极大的全局并发争用,还伴随着冷缓存惩罚。工业级方案的第一道防线必然是线程本地空闲列表(Thread-local Free List)。 3. **针对 CPU 缓存的逆向冲击**:这是一种反直觉的物理现实。表面上看,刚释放的内存似乎是“热”的,但实际生产应用中,大量被释放的内存其缓存行早已变冷;而侵入式空闲列表为了穿针引线,必须在释放时强行向这块冷内存写入链表指针,从而引发昂贵的高速缓存行缺失(Cache Miss)。正如 Andrei 所总结的那样:“一个优秀的分配器会极力避免向冷内存写入数据,而事实证明,正在被释放的内存往往就是冷的。” ### [50:43](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=3043s) FreeList 分配器的模板设计与实现机制 Andrei Alexandrescu 开始展示一个通用空闲链表(FreeList)分配器的具体实现结构。该分配器是一个模板类,其第一个模板参数 `ParentAllocator`(简称 `A`)代表它所依附的父分配器。因为它是通用的,底层父分配器可以是任意实现,包括 `malloc` 或者前面提到的栈分配器(Stack Allocator)。空闲链表会指定一个专门关心的固定块大小(size)。在调侃观众“谁眼神好能看到光标正停在 `public` 旁边”之后,Andrei 带着大家逐步阅读核心方法: 在 `allocate` 函数中,如果请求的大小恰好是链表负责的固定尺寸且链表头指针(root)非空,它就会直接弹出链表中的下一个空闲块并返回。这是一个极为迅速的 $O(1)$ 操作;否则,请求将直接委托给父分配器处理。在 `deallocate` 释放函数中,逻辑恰好对称:如果待释放内存块的长度不属于该空闲链表负责的尺寸,分配器不做截留,直接交由父分配器去 `deallocate`;如果尺寸匹配,它便使用经典的嵌入指针技巧(intrusive pointer),直接将下一个节点的指针写入被释放内存块的前几个字节(第一个字长)中,从而以单向链表的形式将其回收并挂回链表顶端,整个过程同样极其轻量迅速。 ```mermaid flowchart TD accTitle: FreeList 分配与释放决策流程 accDescr: 展示 FreeList 分配器在处理内存分配与释放时,如何根据块大小及内部状态分流至链表自身或委托给父分配器。 subgraph Allocation["内存分配 allocate(n)"] A["检查请求大小 n 是否匹配目标尺寸"] -->|"匹配且 root!= nullptr"| B["O(1) 弹出链表头节点并返回"] A -->|"不匹配或 root == nullptr"| C["转调 ParentAllocator::allocate(n)"] end subgraph Deallocation["内存释放 deallocate(block)"] D["检查块大小是否匹配目标尺寸"] -->|"匹配"| E["将 next 写入内存块前字长,挂回单链表"] D -->|"不匹配"| F["转调 ParentAllocator::deallocate(block)"] end ``` --- ### [52:11](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=3131s) FreeList 的实战改进:容差、批量分配与容量上限 针对最简版本的空闲链表,Andrei 提出了几项极具实操价值的优化思路: 1. **容差区间(Tolerance)**:如果链表专门管理 1 KB 的块,而调用方请求了 900 字节,“难道我不该稍微宽容一点吗?也许我应该这么做”。因此可以在分配器中设定一个范围,例如在 512 字节到 1024 字节之间的请求均统一分配 1 KB 的块,工业界普遍采用这种策略。 2. **批量分配(Batch Allocation)**:与其每次只向底层分配一个对象,不如一次性向父分配器申请一大批连续内存,并在内部将其串成单向链表供后续切分。这种批处理显著摊薄了底层调用的开销。然而,Andrei 直言该做法的难点在于释放追踪:“难点在于很难察觉何时才能真正把那一整块大内存释放回给父分配器”。 3. **链表长度上限(Upper Bound)**:为了防止空闲链表无休止膨胀导致内存泄漏,分配器需要设定一个最大节点数(例如上限 1024 个元素)。一旦链表容量饱和,后续多余的释放请求不再留在链表中,而是直接退还给父分配器,这是防范内存失控的关键防线。 在回答现场关于参数含义的提问时,Andrei 详细解读了一个兼具工业实用性的 FreeList 参数配置(例如支持 17 到 32 字节的容差区间、每次批量分配 8 个元素、链表容量上限为 1024 个节点)。针对听众关于“链表中单个元素如何真正释放回操作系统”的疑问,Andrei 解释说在设计上它们只会被放回空闲链表供复用,除非整个分配器重置。这种组件化设计的巨大魅力在于其组合能力:将 FreeList 叠加在栈分配器之上,就能巧妙赋予原本只能后进先出释放的栈内存“在中途释放特定尺寸对象”的能力。关于这种组合优势,Andrei 强调: > “I'm gaining composition strength here, right?” --- ### [56:00](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=3360s) 词缀分配器(AffixAllocator)与元数据注入 Andrei 随后介绍了内存分配领域的经典利器——词缀分配器(AffixAllocator)。这种分配器会寄生在某个父分配器之上,在每次分配的内存块前部(Prefix)与尾部(Suffix,默认为 `void`)悄悄附加额外的元数据。这种机制最直观的应用就是“电子围栏”(Electric Fence)或金丝雀校验:在内存的前后填入特定魔数签名或插入 1 KB 的防护区,在释放时校验这些签名是否完好无损,从而秒级排查内存越界与缓冲区溢出损坏。 除了做越界守卫,词缀分配器在调试和性能分析方面同样大放异彩。在分配时,可以在前缀中直接记录发起分配的源文件(`__FILE__`)和行号(`__LINE__`)。Andrei 提及当天上午关于 Linux `perf` 工具的主题演讲,虽然 `perf` 在分析性能热点时极度强大,但它对分配上下文却束手无策: > “All of these great things you can see with perf, you can't see who's asking for memory in perf — you can't, it's dark matter, you can't see it.” 借助词缀分配器,被申请的内存块中自带溯源信息,开发者在释放时能一眼揪出“百万行代码的庞大工程里,究竟是哪一行代码在疯狂吞噬内存”。此外,通过 AffixAllocator 还能轻松统计全局分配指标(如 `allocate` 与 `deallocate` 的具体调用频次与容量分布),并在程序退出时进行多维度切片分析。 --- ### [59:08](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=3548s) 零动态分配的静态链表内存追踪黑科技与 BitmapBlock 当现场工作人员提示只剩 5 分钟时,Andrei 惊讶地表示刚才还以为有 15 分钟,但他向台下保证会借用 5 分钟的休息时间,把极具启发性的思路讲完。他向大家传授了一个即使在 Slides 里也没写出来的顶级追踪技巧:**如何在完全不进行任何动态内存分配的前提下,精准收集全程序的内存分配统计信息**。 这个机制的核心是在分配宏内部声明局部静态结构体变量: ```text 每次分配宏展开: struct StatNode { const char* file; long line; StatNode* next; // 计数器与统计指标... }; static StatNode node = { __FILE__, __LINE__, head }; head = &node; // 将节点穿成侵入式单向链表 ``` 在这个技巧中,所有的源文件指针、行号、统计数据以及串联各节点的链表指针本身,全都在编译期被安放在只读或静态存储区(Static Storage)中。程序运行期间记录数据不需要调用任何哪怕一字节的动态内存分配;而在程序生命周期结束时,只要沿着静态链表头指针遍历一遍,整套软件所有分配热点的精确统计数据便尽收眼底。 在确认争取到额外的时间后,Andrei 引入了下一项核心组件——位图块分配器(BitmapBlock)。这是一种空间利用率极高的数据结构,它将连续的大内存划分为固定尺寸的块(如 64 字节),而在内存块的起始区域仅用 1 个比特(1 bit per block)作为元数据来标记该块是空闲还是已被占用。借助这种极其紧凑的元数据管理,分配器得以用极低的空间开销实现精准的状态跟踪。 ### [01:02:14](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=3734s) 基于位图的内存块分配器与级联分配器 Andrei Alexandrescu 深入剖析了位图块分配器(Bitmap Block Allocator)的设计思路与优劣势。在该方案中,系统为每个连续内存块维护一个独立的标志位(bit per block),并将所有实际的数据块连续排布。这种结构彻底克服了传统空闲链表(Free List)的各项缺陷:调用者可以一次性分配连续的多个块来满足任意大小的需求,更重要的是元数据(位图)与用户数据物理隔离。Andrei 强调了内存设计中的黄金法则:“把元数据存放在除数据本身之外的其他地方,这是在内存分配领域成就伟业的秘诀(store metadata elsewhere than the data, this is a recipe for great things in memory allocation)。”因为元数据集中紧凑,CPU 缓存命中率极高(always hot),加之现代 CPU 厂商针对位操作进行了深度硬件优化,开发者能够轻松借助 SIMD 指令实现极速的批量扫描与标记。 然而,没有任何设计是十全十美的。Andrei 坦言,位图块分配器的劣势在于固定块大小必然导致内部碎片(Internal Fragmentation),分配容量往往略大于实际请求量;此外多线程环境下该方案的表现相当棘手,不同线程竞争写入相邻位时会引发严重的伪共享(false sharing)与并发同步开销。 紧接着,Andrei 引入了一种更加灵活的组合构件——级联分配器(Cascading Allocator)。设想一个位图块管理着一段 1MB 的内存,当这一兆空间耗尽后该怎么办?答案是延迟按需再向底层申请下一个 1MB,并在新块上覆盖安装另一个位图块管理器。级联分配器根据需求懒加载分配新的大块,并在内存块完全空闲时智能释放。虽然内部涉及一定的管理逻辑,但整体概念高度内聚且实现清晰。 ```mermaid flowchart TD accTitle: 级联分配器结构示意图 accDescr: 级联分配器在顶层按需懒加载分配1MB大块,并在每个大块内通过独立的位图块分配器管理具体对象。 subgraph Alloc["Cascading Allocator (级联分配器)"] direction TB B1["1 MB 内存块 A<br/>[Bitmap Block 分配器]"] B2["1 MB 内存块 B<br/>[Bitmap Block 分配器]"] BN["1 MB 内存块 N<br/>(按需懒加载创建...)"] end Req["用户内存申请"] -->|"容量不足时级联扩容"| Alloc ``` - **元数据分离优势**:元数据集中保存在独立区域,保持在 CPU 高速缓存中,避免与用户数据交织破坏局部性。 - **硬件加速潜力**:支持使用单指令多数据流(SIMD)与硬件内置位运算指令对分配状态进行超高速检索。 - **固有缺陷**:块粒度导致不可避免的内部碎片;跨线程共享位图时同步代价高。 - **级联扩容机制**:以大块(如 1MB)为单元懒加载追加,生命周期结束时自动回收。 --- ### [01:04:39](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=3879s) 组合式构件:分流分配器与分桶分配器 在介绍下一款构件时,Andrei 抛出问题询问其职责,听众席中的 Marshall Clow 立即抢答为“分区”,Andrei 风趣地调侃大家不能总指望 Marshall 来回答所有问题。这个核心组件正是分流分配器(Segregator)。它的工作机制极其纯粹:设定一个阈值(Threshold),任何小于等于阈值的请求分流给小对象分配器(Small Allocator),大于阈值的请求直接交由大对象分配器(Large Allocator)。除特定复杂块分配器外,这类适配器代码量通常不超过 50 行,却展现出模板组合的无穷威力。实现 Segregator 甚至完全不需要查询所有权 `owns` 接口,仅凭传入的尺寸即可完成精确路由。 更有价值的是,Segregator 具备高度的自组合性(Self-composable)。例如,外层 Segregator 以 4KB 为界,大于 4KB 的走回退分配器,小于 4KB 的则委托给内层 Segregator;内层 Segregator 再以 128 字节为界,将小对象导向空闲链表(Free List),中等对象导向专用中型分配器。工业级分配器完全可以通过嵌套十几个 Segregator 组织成二叉搜索树(Binary Search)结构进行尺寸分派,甚至出于对高频极小尺寸分配的性能极致倾斜,刻意采用线性短路排布。 随后 Andrei 介绍了分桶分配器(Bucketizer)。它通过编译期指定的步长,将指定最小与最大区间内的请求切分为等差或等比的一系列尺寸桶,每个桶绑定专用的分配构件。结合简单的对数递增(以 2 的幂次划分)或线性递增,经典内存分配器体系中的绝大部分复杂调度逻辑便已被这些轻量模块彻底覆盖。正如 Andrei 总结道:“我们在内存分配领域的工作基本就告一段落了,而且代码量少得惊人,也没用什么晦涩的高深技巧(we're kind of done here with the domain of memory allocation with remarkably little code and remarkably little smarts)。” | 分配器组件 | 核心逻辑 | 典型应用场景与特点 | |:--- |:--- |:--- | | **Segregator(分流分配器)** | 基于阈值将请求二分分流给小/大分配器 | 无需 `owns` 查询,仅凭尺寸路由;支持树状递归嵌套组合 | | **Bucketizer(分桶分配器)** | 在指定尺寸范围内按步长离散划分为多个桶 | 常用 2 的幂次对数增长或固定步长,消除外部碎片 | | **FreeList(空闲链表)** | 针对单一固定尺寸小对象的回收与复用 | 搭配 Segregator 处理极端高频的极小内存(如 $\le 128$ 字节) | --- ### [01:08:43](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=4123s) 终极分配器 API 规范与全场总结 在梳理完各类构件后,Andrei 展示了他提炼出足以承载上述所有模式的完整分配器 API 契约: - `alignment`:编译期静态常量,标明当前分配器的默认对齐边界; - `good_size(n)`:传入期望尺寸,返回分配器实际分配的对齐后规格(极其利于 `std::vector` 预估真实容量并最大化内存利用率); - `allocate(n)`:基础内存分配入口; - `allocate_all()`:专供栈式分配器(Stack Allocator)等快速清空型结构使用,`malloc` 等全局分配器则选择性不实现该接口; - `expand(b, delta)`:尝试在原内存块末尾就地扩展尺寸; - `reallocate(b, s)`:语义对齐标准 `realloc` 的重新分配并迁移; - `owns(b)`:判定内存指针是否归属本分配器管辖; - `deallocate(b)` / `deallocate_all()`:释放单个内存块或批量一次性完全释放; - `aligned_allocate(n, a)`:满足游戏开发与底层硬件必须的指定对齐分配,包装操作系统底层原语。 在全场总结中,Andrei 直指旧标准的死穴:“首先,我们把 `std::allocator` 彻底批驳了一番,因为它根本不懂组合,无论他们拿它做什么,它对组合一无所知(first of all we destroyed the allocator because it doesn't know composition, whatever they do with it has knows nothing about composition)。”写出优秀分配器的本质就在于实现自由无缝的模块化组合。 Andrei 坦言,自己很长一段时间也陷入了思维定势:因为 `std::allocator` 拥有 `allocate`、`deallocate`、`construct`、`destroy` 等看似包罗万象的接口,人们下意识觉得它既然存在就必然有其合理性。直到探寻 STL 历史才发现,它只是为应对上世纪 90 年代分段内存模型(Segmented Memory Models)临时拼凑的产物。唯有彻底清空既有成见从第一性原理出发,以“可组合性(Composability)”为核心指引,才能打造出真正适应现代高性能计算的内存分配工具链。在听众的掌声与离开会场的动身声中,Andrei 致谢并结束了演讲。 - **对齐支持的现实妥协**:尽管增加了 API 表面积,但为满足游戏等行业硬性需求,`aligned_allocate` 属于不可或缺的“必要之恶”。 - **契约式可选特性**:诸如 `allocate_all` 与 `deallocate_all` 依靠静态接口反射实现按需特化,普通分配器未定义也不会引发编译断错。 - **历史反思结论**:理解历史包袱是摆脱不良抽象的前提,内存分配设计的根本演进方向是高内聚、易组合的微型原语。 ## Notable quotes > [24:10](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=1450s) Getting composition right is getting the allocators right. — Andrei Alexandrescu > [43:20](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=2600s) 一个优秀的分配器会极力避免向冷内存写入数据,而事实证明,正在被释放的内存往往就是冷的。 — Andrei Alexandrescu > [01:02:14](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=3734s) store metadata elsewhere than the data, this is a recipe for great things in memory allocation — Andrei Alexandrescu > [11:56](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=716s) 在任何严肃的内存分配场景中,我都不推荐使用 operator new,你可以把这句话当作我的定论引用出去。 — Andrei Alexandrescu > [27:16](https://www.youtube.com/watch?v=LIb3L4vKZ7U&t=1636s) To not have to be smart is a great thing in software engineering, right? Because it means you have the right frame of thinking and the right frame of reference, and there's only one way to follow the tunnel. — Andrei Alexandrescu ## Mentioned std::allocator, std::vector, Andrei Alexandrescu, Bjarne Stroustrup, Kernighan and Ritchie, Pascal, Lisp, Fortran, jemalloc, tcmalloc, Howard Hinnant, Alisdair Meredith, Pablo Halpern, John Lakos, Doug Lea, dlmalloc, Fedor, Marshall Clow, perf, SIMD, Linux, Windows, 宋飞正传 ## Video description http://www.Cppcon.org — std::allocator Is to Allocation what std::vector Is to Vexation -- Presentation Slides, PDFs, Source Code and other presenter materials are available at: https://github.com/cppcon/cppcon2015 — std::allocator has an inglorious past, murky present, and cheerless future. STL introduced allocators as a stop gap for the now antiquated segmented memory models of the 1990s. Their design was limited and in many ways wasn't even aiming at helping allocation that much. Because allocators were there, they simply continued being there, up to the point they became impossible to either uproot or make work, in spite of valiant effort spent by the community. But this talk aims at spending less time on poking criticism at std::allocator and more on actually defining allocator APIs that work. Scalable, high-performance memory allocation is a topic of increasing importance in today's demanding applications. For such, std::allocator simply doesn't work. This talk discusses the full design of a memory allocator created from first principles. It is generic, componentized, and composable for supporting application-specific allocation patterns. — Andrei Alexandrescu is a researcher, software engineer, and author. He wrote three best-selling books on programming (Modern C++ Design, C++ Coding Standards, and The D Programming Language) and numerous articles and papers on wide-ranging topics from programming to language design to Machine Learning to Natural Language Processing. Andrei holds a PhD in Computer Science from the University of Washington and a BSc in Electrical Engineering from University "Politehnica" Bucharest. He works as a Research Scientist for Facebook.Website: http://erdani.comTwitter handle: @incomputable — Videos Filmed & Edited by Bash Films: http://www.BashFilms.com Work at Hudson River Trading (HRT): https://tinyurl.com/safxfctf [Source](https://www.youtube.com/watch?v=LIb3L4vKZ7U)

Source quotes

[24:10] Andrei Alexandrescu: Getting composition right is getting the allocators right. [43:20] Andrei Alexandrescu: 一个优秀的分配器会极力避免向冷内存写入数据,而事实证明,正在被释放的内存往往就是冷的。 [62:14] Andrei Alexandrescu: store metadata elsewhere than the data, this is a recipe for great things in memory allocation [11:56] Andrei Alexandrescu: 在任何严肃的内存分配场景中,我都不推荐使用 operator new,你可以把这句话当作我的定论引用出去。 [27:16] Andrei Alexandrescu: To not have to be smart is a great thing in software engineering, right? Because it means you have the right frame of thinking and the right frame of reference, and there's only one way to follow the tunnel.

Source quotes

[62:14] Andrei Alexandrescu: This is a recipe for great things in memory allocation: store metadata elsewhere than the data. [11:56] Andrei Alexandrescu: I unrecommend operator new for any serious use of memory allocation. [24:10] Andrei Alexandrescu: Getting composition right is getting the allocators right. [18:03] Andrei Alexandrescu: std::allocator is not a great allocator because it was not designed for that purpose; it was designed for near and far pointers.

Original transcript

[0:00] let's start so we start early because [0:01] we're going to finish late so that's the [0:04] right thing to [0:05] do well um I sent this working title to [0:09] a number of folks and ask what what do I [0:12] mean by this title and it was kind of [0:14] difficult to so I had to explain my [0:18] title so my my title [0:21] means okay here's my working title I had [0:23] this working title which was [0:26] the [0:28] okay and then I thought it's not a very [0:31] good title because you could make an [0:34] allegation about an [0:36] alligator for this alligator caught my [0:41] foot it it ate my [0:44] Crocs right so there is a possibility of [0:48] a connection there um what I'm saying [0:51] there's not a lot of connection between [0:54] St allocator and memory [0:57] allocation this is my point and and to [1:00] it my uh another working title of mine [1:02] was design allocators that don't not [1:06] work so you know being a program so this [1:09] is not English it's not correct but it's [1:12] sort of logic logically correct because [1:15] you can imagine as a program you put a [1:16] parenthesis around not work and that's [1:19] like a verb not work like not work is a [1:24] verb and you put a parenthesis and it [1:26] makes sense so it's Mak it's I I love [1:27] grammatical malice that way in any [1:30] language as it were anywh who uh I hope [1:35] uh it's okay [1:37] so does this is does this make clear [1:39] that I'm not here in [1:42] peace [1:43] okay is this clear to everybody I'm not [1:48] here I'm here to [1:49] destroy okay I'm on a war path right now [1:53] okay so just to make sure that my yes [1:57] question ah okay um as like oh my what [2:00] question could be at this [2:03] point so there's been something very [2:05] interesting about memory allocation so [2:06] this talk is not about um U making fun [2:09] of stud allocator because it's just too [2:11] easy it's more about how to write good [2:14] memory [2:16] allocators do do you have an interest in [2:18] that kind of [2:19] stuff yeah you know in the immortal [2:22] words of B this is C+ plus and this is [2:24] like the ultimate argument of Kings we [2:27] care so this is kind of stuff that we we [2:30] care about so let's see we have a [2:33] pointer [2:35] here it doesn't work but it's the if I [2:38] if I look like it works I can see it but [2:42] now I'm going to use my left eye for the [2:43] rest of my [2:45] life um okay so it's very interesting [2:49] that memory allocation has been a [2:51] cyclical business and it's very [2:53] interesting like if you if you're a [2:54] vendor of Malo if you made your a living [2:56] if you paid bills by implanting Malo [2:59] that's that was kind of a wild ride [3:02] because in 83 so when Malo when when [3:05] sort of the king and Richi book came out [3:07] it had a mini memory allocator which is [3:09] what why today standard is rather naive [3:12] and [3:13] simple uh but at that point was kind of [3:15] a huge thing it was a huge deal first of [3:18] all it was that you could write it [3:20] without [3:21] assembler like oh my God they could do [3:23] this c is awesome look at that void star [3:27] and that cast and all you know it was it [3:30] was like wonderful that you could do [3:32] like layout control and that kind of [3:34] stuff in with memory the way you wanted [3:36] really you put your God for the first [3:39] time could put bits where you [3:42] wanted and this was amazing because like [3:44] by those stands like pcal was there and [3:47] it was kind of weird to do that kind of [3:49] stuff in Pascal if not impossible right [3:52] um there's like list which is way high [3:54] level and there's Fortran which is like [3:56] again like it couldn't do that kind of [3:58] stuff right [4:00] so uh when mow came about and you know [4:03] and see and that kind of lowlevel coding [4:05] it was some sort of an eye opener and [4:08] everybody went on the bandwagon and said [4:10] oh all of my everything I'll do from now [4:12] on is going to use dynamic memory [4:14] allocation I not sure if you remember [4:16] but before that it was dynamic memory [4:19] allocation was kind of um what's the [4:21] word it was an exotic thing to do oh in [4:25] my Pascal program I use like uh you know [4:27] that carrot and new and I forgot The [4:30] Operators they had right so it was kind [4:33] of an interesting thing to do at system [4:35] level to use Dynamic allocation so 193 [4:38] and you know fast forward 10 years um [4:41] essentially like Malo was turned out to [4:42] be so bad that you know any code you [4:45] wrote was better than [4:47] it so uh another 10 years later again [4:51] Malo caught up so you know new horde [4:53] allocators and J Mal and you know a lot [4:55] of good new allocators came about TC Mal [4:58] and whatnot and uh today you know it's [5:01] kind of in the middle so today is like [5:03] middle of the road you know so you [5:05] should know about Malo and you should [5:07] know how it's done and you you should [5:08] know that sometimes you can do better [5:10] and here's where we are [5:12] today okay well two years later but [5:14] that's okay [5:18] so I'm going to start with a bit of [5:23] History this silence doesn't bold well I [5:26] was expecting some murmurs like what's [5:29] going on here you yeah that kind of [5:31] stuff yes thank you thank you for the [5:33] sound effects here so by means of a of a [5:38] bit of History so it started with C when [5:40] we had where we had Malo and free and uh [5:44] there's an as symmetry here because when [5:46] I maloc memory I pass the allocator the [5:49] size but when I free memory I don't pass [5:50] the size [5:53] back right is this good or [5:57] bad bad why is it bad performance [6:01] performance so essentially by committing [6:05] to this this API you already stated that [6:07] I'm going to have to mine that size [6:09] somewhere or another and it turned out [6:12] in the first Malo that was sort of part [6:15] for the course because they stored the [6:16] size anyway because they needed it for [6:18] the side of the block that held the [6:19] memory allocated and so on and so forth [6:22] turns out uh this has been sort of a bad [6:25] decision it was kind of a um we we've [6:28] been living with ever since so I was [6:30] just talking with to Howard H Howard are [6:32] you here there he is okay so I was [6:35] talking to this fine gentleman Howard [6:37] hinant and he says I'm like uh being [6:41] there done that and I would hate trying [6:43] to do it again doing anything any change [6:46] exacting any change about memory [6:47] allocation in in the respective [6:49] standards so um this is kind of an [6:52] unpleasant thing so the Coler knows the [6:55] size the Coler knows the size but it [6:56] doesn't pass it along and the allocator [6:58] must manage size turns out this is kind [7:00] of a big deal size management is a kind [7:04] of a complete pain in the proverbial we [7:07] hate it it's a bad thing to Wi give me a [7:11] second here where are we I'm going to [7:14] accelerate a few slides ahead then I'm [7:15] going to turn back just to make a point [7:17] here Google for that [7:20] number and you're going to find a you [7:23] know probably a part name at Sears it's [7:25] a nail that you can pound in the ground [7:27] or whatever but it's also a a proposal [7:30] for the C [7:31] standard and omission of size [7:34] information Global operator delete AKA [7:37] fre has unfortunate performance [7:39] consequences and there's a lot of [7:41] discussion about how to improve that [7:43] what's the state of that [7:46] Howard is [7:48] it yeah all right why don't you Google [7:52] for it thank you very [7:53] much delegation it's you know getting [7:56] into management now so I got to okay let [7:58] me turn back back here so this kind of a [8:02] bummer okay uh and to uh if we could [8:05] turn the time back and do it all over [8:07] again we would do something like this we [8:09] would say oh let's define a structure [8:10] that's [8:11] block and it has the pointer and the [8:14] length thank you very much and whenever [8:16] I'm going to Malo I'm going to get back [8:18] the block and I'm going to pass the [8:20] block again when I release the memory [8:22] the block has a size boom big deal right [8:26] who [8:27] cares essentially we do care right so [8:30] there is importance attached to this so [8:32] let's kind of uh say uh if we want to [8:35] design a new [8:36] allocator uh let's start with this block [8:39] simple [8:40] structure all right it could hold two [8:43] pointers etc etc and you know this uh [8:46] it's pretty much equivalent and uh as [8:48] the history goes the next generation was [8:51] operator new what's wrong about operator [8:54] new okay what's good about operator new [9:00] type it carries the type information [9:03] into the type right and it it does all [9:05] it can to throw it away as soon as [9:07] possible because it forwards to Malo so [9:09] the good and bad about new is it works [9:11] with Malo it must work with Malo because [9:14] historically it you know there's so much [9:17] so many things you could do at the same [9:18] time so new was not like the highest [9:21] priority there so like new must work [9:24] with Malik and then there's this whole [9:26] side discussion about Realo because [9:28] there's no renew [9:31] right there's Realo which is awesome and [9:34] is you never use it [9:37] correctly right only Marshall and I were [9:41] the two guys who know how to use it okay [9:45] um we're talking about this it's kind of [9:48] funny but essentially operator new was [9:50] designed to work with Malo uh but you [9:53] know it [9:54] didn't I have you know like in that [9:57] movie like [10:00] I [10:01] protest this is not a good thing [10:04] operator knew what happened and I I'll [10:06] tell you what I think um but it's okay [10:09] that it's adds type information is great [10:11] I love it uh there are a few syntactic [10:14] Oddities um the most interesting of [10:17] which is um I was just talking about [10:20] this like int is a type right int is a [10:23] type do we agree so far I have have your [10:26] approval okay thanks beon then int open [10:31] square bracket uh 100 closed square [10:34] bracket is a type is it is it not it's a [10:37] it's a fixed size array of 100 integers [10:40] all right will you with me nothing up my [10:45] sleeves okay new int I get a pointer to [10:49] an INT I get I allocate a new integer [10:52] one integer and I get it a pointed to it [10:54] back new [10:56] int open square bracket 100 close square [11:00] bracket what do I [11:02] get uh there's something wrong here [11:04] ladies and [11:06] gentlemen uh there's a syntactic uh [11:09] weirdness because even though in of 100 [11:12] is a type in and of itself when you put [11:15] it at the tail of new it's going you [11:18] know the the syntax of the operator new [11:20] is going to interpret a completely [11:21] different way it's going to say oh this [11:23] guy wanted not a fixed size array of 100 [11:26] integers and give me a pointer to that [11:28] guy but he want to dynamically size the [11:31] array of integers and I'm going to give [11:33] him a point of TR because why do I care [11:36] about that guy [11:38] right so that's The syntactic Oddities [11:41] that is kind of [11:43] weird um there's no communication [11:46] between new and the Constructor which [11:47] everybody who's done tried to do [11:49] anything with regard to like class level [11:52] operator new and Constructors and stuff [11:54] knows painfully well like people do all [11:56] these kind of crazy tricks and I think [11:58] the best trick you can do with class [12:00] level operator new is to forget all [12:03] about it and never use [12:05] it and like really pretend it doesn't [12:08] exist in the language and then you'll be [12:10] happy you you lead a better life [12:15] right there's you know there's OA do you [12:18] know OA operator new [12:21] Anonymous you could should join the [12:23] meetings Okay so the great AR rate [12:26] distraction okay so we have the great AR [12:28] rate distraction t GA I love acronyms [12:30] right so the great array distraction [12:32] goes like this well operator new is kind [12:35] of weird and uh but you know it had to [12:37] be [12:39] there well how about the race well we [12:41] got to have an operator new with [12:42] brackets at the end and that's going to [12:44] allocate the race and arace only and by [12:46] the way you should be uh sure you should [12:48] make sure that whenever you call New U [12:51] the normal new call delete the normal [12:53] delete but whenever I call new the array [12:55] new you should remember to call the [12:58] delete array delete [13:01] you know the rule this is [13:03] old you know why you need to know the [13:10] rule [13:14] huh okay I want like a coherent so you [13:17] guys talk you guys talk you guys talk [13:20] three votes here what [13:27] yes yeah so there's a there's a thing [13:29] with a race and you know you delete with [13:31] the non race syntax and it's just going [13:33] to call the first Destructor it finds on [13:34] the way and that kind of stuff but In [13:37] fairness there's no there's no reason [13:40] for it to be honest there's absolutely [13:42] no reason and it should have been not [13:44] existent at all it should not have [13:46] existed it's just to make life difficult [13:48] for everybody I un reccommend operator [13:51] new for any serious use of memory [13:53] allocation you can quote me on that the [13:55] guy with the camera you should have me [13:57] there right okay so follow me I'd be [14:00] like if this were a jail and I want to [14:02] escape this would be awful because he's [14:04] following me I couldn't okay [14:09] so and then you know as the history goes [14:12] along KMD allocator and you know the [14:14] funniest thing about STD allocator is [14:16] it's not for memory [14:17] allocation and I'm not kidding what is [14:20] STD allocator [14:22] for construction [14:25] no pointer types Pablo okay yes he's the [14:28] guy so it was [14:31] for uh where are we near and far [14:36] pointers who's old enough to remember [14:40] near and far [14:42] pointers okay great so you turn to your [14:46] left and explain to the younger [14:48] generation what we've been going through [14:51] they better off not knowing you're [14:53] better of not knowing yes that's right [14:56] every knowledge about near and far ends [14:58] up on your face okay okay so not good un [15:03] reccommended so well one question is [15:06] like is STD allocator bad or not and [15:09] this is a legitimate question to ask [15:11] because I've talked to people who claim [15:13] it's a non problem allocator Works to it [15:17] we've been carrying it in standards like [15:18] through C++ 17 it's still there it [15:21] didn't [15:22] die it's like the you know the square [15:25] wheel of on on the car so this is It's a [15:29] truck has many wheels and everything one [15:30] of them is square and that's the delator [15:33] we've been carrying [15:34] it and uh one great thing that uh I [15:38] should bring to your attention is the [15:40] alies there m this talk of last year [15:43] here at this [15:45] conference and it's entitled making [15:47] allocators [15:50] work and there's two issues with that [15:53] talk number one the [15:57] title what I mean you know let's do a [15:59] bit of you know [16:02] linguistic analysis on the [16:04] title did you ever see like it's 2014 so [16:08] it was like last year and stud allocator [16:11] has been finished before 1994 so talk [16:15] like 20 years later like in the book [16:18] right 20 years [16:21] later and 20 years later we have and it [16:24] was a double whing it was like 180 [16:27] minutes talk it was like two talks back [16:29] to back 90 minutes plus 90 minutes and I [16:31] sat through most of [16:33] it [16:35] and it was 20 years after the fact and [16:39] it was making allocators [16:41] work there's no talk like making sort [16:47] work I I didn't go to a talk like making [16:51] making STD Vector work let's let's get [16:54] that soccer working [16:56] okay so the title already kind of is the [17:00] Smoking Gun here ladies and gentlemen we [17:02] have a problem here that right and the [17:06] second matter I have with that talk so [17:08] you know I sat through it and was like [17:10] okay so let me see where's the [17:11] allocation [17:13] happening there's not one slide on it [17:16] that's dedicated to memory [17:19] allocation which is very funny because [17:21] allocators is in the title and [17:23] linguistic analysis making allocators [17:26] work I would expect there be very good [17:27] at allocations [17:29] because they're [17:31] allocators and right that kind of stuff [17:34] so I'm an immigrant but you you can't [17:37] fool me with that stuff okay it's still [17:42] I can figure out the [17:43] basics all right so then I emailed Alis [17:47] there and said you know could you please [17:48] email me the slides because I want to [17:49] quote them and kind of use them as a [17:51] reference and guess what he said [17:54] nothing he didn't reply because he knew [17:57] I'm going to make fun of him [18:01] so all right what's good and what could [18:03] be done be better in St [18:06] allocator I told you I did not come in [18:09] peace friends did not come in peace so [18:16] okay the short story here is that stud [18:19] allocator is not a great allocator [18:21] because it was not designed for that [18:23] purpose it was designed for near and far [18:25] pointers near and far far pointers had [18:27] to do with 16 bit systems and you had [18:31] like pointers that were 20 bit with [18:33] segment and offset again you don't want [18:35] to know that stuff because it ends up on [18:37] your face right so it was kind of a [18:39] weird kind of thing that was essentially [18:42] historically um it was a blip on the in [18:45] the history of computer science that has [18:47] since long subsided it's nobody cares [18:50] about that stuff anymore and to Wi uh [18:53] the the committee the C committee [18:55] actually removed everything related to [18:58] near and far point us from St the [18:59] allocator essentially removing willfully [19:03] its initial [19:04] purpose which is very interesting and [19:07] very telling so okay so there are few [19:09] obvious mistakes that I noticed one is [19:11] that you know the first thing is like [19:13] the type is a parameter in the [19:15] allocator and that's a weird thing [19:21] because you know allocation should not [19:24] have to do much with type at [19:26] all it cares about two things one is the [19:29] size and the other is the [19:31] alignment do you care if you allocate [19:33] for [19:34] example indulge me for a moment do you [19:38] care to allocate an INT and an unsigned [19:41] INT in different [19:44] ways probably not it's like what do I [19:46] care they're both same size same layout [19:50] even what do I care right taking this [19:53] one step further do you allocate about [19:55] do you care about allocating an object [19:57] of 128 bytes [19:59] of type one T1 and another object of 128 [20:03] bytes of type T2 do you care to allocate [20:06] them in very different [20:07] ways possibly possibly okay so alignment [20:11] is kind of a different topic but you [20:13] know I do care about the alignment but [20:14] the type themel the types themselves is [20:16] like I have much less concern for and [20:19] more often if I use a specialized very [20:21] good allocator for T1 it's going to [20:24] actually be a good allocator for 128 [20:27] bytes it's going to be a very good [20:29] allocate for that specific size and [20:30] Alignment not for that specific type [20:32] alone so you care about size but not [20:34] about the type so this whole thing [20:36] putting a type in the allocator is weird [20:38] it's not weird it's it's great with near [20:40] and far pointers but it's not good for [20:43] allocation so you know it's an allocator [20:46] not a not a factory because the [20:48] allocator knows how to deallocate things [20:50] and you know it's kind of odd it's not a [20:52] it doesn't produce it constructs objects [20:54] it shouldn't be constructing objects it [20:56] should just allocate memory um even [20:59] though it's a factory because it has [21:00] construct and destroy it does have [21:02] construct and Destroy steel Howard [21:03] doesn't [21:04] it they didn't remove them those [21:08] bastards man okay so uh it's in even [21:12] though it claims to have Factory Powers [21:15] it still traic in void star you know so [21:19] it kind of the Mind boggles here so what [21:21] I'm seeing here is allocate us shoot [21:23] trade in memory blocks void star and [21:25] size T that's [21:27] it and just to mention like awful crutch [21:30] rebind you other all that good stuff [21:33] that I'm sure is [21:34] uh intimate knowledge to us [21:38] all if you don't you don't you don't [21:40] again you don't want to okay great [21:44] another batch of obvious mistakes [21:47] there's an assumption that allocators [21:49] are [21:51] stateless which is I picture the the [21:54] oxymoron [21:56] here A Memory allocator that giving me [21:59] memory has no [22:03] state right this is very interesting [22:05] it's like the The Empty Bottle that you [22:06] keep on drinking water from and it's [22:09] just you know it's like Sinbad you know [22:12] the it's the story it's like magic you [22:15] know it's empty but I'm pouring from it [22:18] you know it's mysticism something is [22:21] really interesting here except it's of [22:24] course not it's like completely crappy [22:26] right so aloc all allocators have state [22:30] some may have shared state it may be a [22:32] global there because Malo that's how it [22:34] works it has like Global State dealing [22:36] with uh some you know some is monate you [22:40] know I love like these complicated words [22:42] you know the monate pattern is like a [22:45] global except it has a great name right [22:49] monate so well they have state man got [22:53] to deal with it and by the way C++ 14 [22:57] does allow allocators to have state and [23:00] you got you got two gentlemen in the [23:01] front to here to thank for or egg their [23:04] faces as you you know depending on your [23:07] preference John leakas and Pablo Halper [23:10] raise a hand please thanks very much [23:13] there they are so you can talk with them [23:16] yeah don't throw the tomatoes right [23:19] now great so we have some state so [23:22] definitely we want to support allocators [23:24] with [23:25] State and but there's something that uh [23:28] public and John did not do they did not [23:31] do the composition thing which I think [23:33] is the first thing you need to do with [23:34] memory allocation if you want to write [23:37] an allocator you got to make it [23:40] composable if you look at any allocator [23:43] documentation you're going to see that [23:45] it is a composition of several [23:48] completely different allocation [23:52] strategies look Google for dogly [23:55] allocator and you're going to say dog [23:56] the first sentence like like one of the [23:59] first is like if the memory isow less [24:02] than one one kilobyte or whatever if the [24:05] memory is less than 1 kilobyte then I'm [24:07] going to do this stuff if it's between 1 [24:10] kilobyte and 64 kilobytes I'm going to [24:12] do some completely different strategy [24:15] and if it's beyond that I'm just going [24:16] to use M map or whatever the hell [24:18] right so it's a composition of [24:21] strategies depending on the size for [24:23] example in this case if you turn to the [24:26] windows allocator you're going to see [24:27] the same thing oh for small objects is [24:29] going to do this stuff and for medium [24:30] object is going to do completely [24:31] different stuff and for large object is [24:33] going to say out of memory and you know [24:36] that kind of [24:38] stuff my point is it's different [24:41] right so and you're going to see that [24:44] even like for for one allocate that [24:46] small size allocat like up to 64 [24:49] kilobytes for example that guy is inside [24:52] its belly it's you're going to find an [24:53] array of little [24:56] allocators each of whom is specialized [24:59] for a specific cus range and there [25:01] buckets and all these good [25:04] things so if you want to design an [25:06] allocator you got to make composition [25:09] the first thing in your design the first [25:14] concern getting composition right is [25:16] getting the allocators right so the rest [25:18] of the stock is going to do composable [25:23] allocators [25:25] great um [25:29] close your eyes for a second because I I [25:30] want to go one slide ahead and back then [25:33] I want to ask you a question okay close [25:34] your eyes ah okay new design this so [25:39] question for you what is the simplest [25:42] composable allocator that you can [25:45] imagine the null allocator excellent is [25:49] the simplest allocator it's actually a [25:50] useful allocator the null allocator what [25:52] does it [25:54] do nothing it is a show about nothing [25:57] okay it does nothing but it does it in [26:01] style it's always returning null [26:03] whenever you try to allocate from it [26:05] it's going to you know when you try to [26:07] free things what's it's going to [26:09] do it's going to assert that the input [26:11] is null what you pass to the null [26:14] allocator for freeing it's got to be [26:16] null because you're not supposed to pass [26:18] anything else because null you pass to [26:20] the allocator what it passed to [26:23] before right so the argument to free [26:25] must be a previous return of new of [26:27] alloc okay right so [26:30] interesting um does it how how much [26:32] memory does he have was it infinite or [26:36] zero I mean it choose but my point you [26:39] know there's Freedom here but what I'm [26:41] saying here is n is a great what you'd [26:43] call [26:45] um you know wave guides like in [26:47] electrical engineering it's a great [26:49] stopper for the wave the wave guide how [26:51] do you call that stopper it has a word [26:53] for it Terminator it's a great [26:56] Terminator so you can put put that null [26:58] allocator at the end of a chain of [27:00] attempts of allocating memory at the end [27:02] you say the hell with it I don't what to [27:04] do I'm going to use the null allocator [27:05] here and it's a great composition device [27:09] but you know what that wasn't my next [27:10] slide so can somebody tell me my next [27:12] slide what's the s the next simplest [27:16] comp composable allocator that you can [27:19] imagine huh slab that's very Advanced so [27:24] simpler I have huh pass through pass [27:27] through [27:28] what does it do it passes through the [27:30] back it passes okay so let's let's kind [27:33] of get a bit just a bit more [27:35] sophisticated so this is a let's say [27:37] it's a bit too simple now so a stack [27:41] yeah but here's what I think we can do [27:46] fallback fallback is this guy has two [27:49] allocators anything any allocators [27:51] including the N locator and that the [27:54] nullator is going to give you the pass [27:55] through John so the fallback alloc is [27:59] going to try the allocator number one [28:01] and if that doesn't work it's going to [28:02] try allocator number [28:04] two the simplest composable allocator it [28:07] knows how to try one and then try the [28:10] next great let's design this guy and see [28:13] what [28:14] happens and you'll see that once we [28:16] start with composability the rest of the [28:19] Primitives are going to write themselves [28:21] they're going to Define them that we [28:23] will have no choice we will not have to [28:26] be intelligent [28:28] and that's a good [28:30] sign right I'm not kidding actually to [28:33] not have to be smart is a great thing in [28:35] software engineering right because it [28:37] means you you have the right frame of [28:39] thinking and the right frame of [28:41] reference and there's only one way to [28:43] follow the [28:44] tunnel right when I have too many [28:46] choices like oh I could use this I could [28:48] do that I could do that let me be smart [28:49] about it [28:52] right so let's pull let's start pulling [28:54] that string and see what unravels okay [28:56] so I have a an allocator as primary and [28:59] fullback and they know how to allocate [29:00] and de allocate [29:02] memory and I'm going to use private [29:04] inheritance because kind of it's got a [29:06] feel onal slides I'm just going to kind [29:08] of use private inheritance here although [29:10] I could use membership and stuff but [29:12] actually inheritance is nice because it [29:14] gives me that empty base optimization [29:16] thing so I I I shall use it and I have [29:21] two Primitives allocating the allocate [29:22] this is all I know how to [29:24] do who can Implement allocate for me [29:29] it's easy right so let me [29:33] try pullback allocator allocate give me [29:36] the size I'll give you a block back I'm [29:39] trying the first allocator primary and I [29:43] if I didn't get the poter I'm going to [29:45] try the fall [29:47] back okay I'm going to return the [29:51] Block it's a three [29:54] liner do we know how to deallocate [30:03] this is like you know do these pants [30:05] look my butt big right it's it's a trick [30:07] questions friend it's a trick question [30:10] friends [30:11] it's It's Tricky how can I implement the [30:14] allocation with this [30:18] setup [30:20] huh [30:22] th [30:25] th you can't you can't you can't you got [30:28] pass what you got from allocate you [30:29] can't pass something wrong into the [30:32] allocator [30:45] right awesome I I'll repeat yeah I see [30:48] what you're saying so essentially you [30:49] can query with the primary allocator has [30:51] a range of addresses that it can [30:54] possibly return [31:08] yes that kind of stuff [31:10] yes extra crap so I gotta essentially [31:14] like my conclusion is if I want to [31:16] deallocate this is API with alloca and [31:18] deallocate is not enough do you agree [31:20] with me again this is Consequence the [31:23] decision have been made when we we [31:25] wanted to have a composable fallback [31:26] allocator and and now we're dealing with [31:29] what came out of you know we threw the [31:31] Domino on the floor let's see what so we [31:33] got to have a new primitive here which [31:35] is called owns and S could do a range [31:37] check could do whatever ownership test [31:41] but at the end of the day the primary [31:42] allocator must be able to tell whether a [31:45] block came from it or [31:48] not and only with that knowledge I'm [31:51] able to implement deallocate and it goes [31:52] like this if P owns the block then P [31:56] deallocate the block otherwise [31:58] the backup the fullback allocate is [32:00] going to deallocate again a four liner [32:02] very simple function but we added a new [32:04] API uh primitive right so you know the [32:07] primary architect must know how if [32:09] whether it owns memory now question does [32:12] the fallback have to implement owns or [32:19] not not [32:21] necessarily but there's you know it's [32:24] kind of interesting like okay and [32:27] another question is can this fallback [32:29] allocator this composite allocator of [32:31] two can it Implement on itself or [32:35] not only if both so okay so you're doing [32:39] my slides guys because this is part of [32:42] good [32:44] citizenry the fallback allocator is [32:47] going to implement ons if both Implement [32:49] ons and it's going to do this Junction [32:52] between the primary owns it or the [32:54] secondary owns it and there's no other [32:55] way okay and you know that I love [32:59] acronyms there's no there's never too [33:01] many right you know what M def is it's a [33:06] Latin word for uh let me let me see if I [33:09] remember it even uh [33:13] method [33:15] uh ah method definition failure is not [33:18] an [33:19] error I'm not kidding so method it means [33:24] this in C++ if you write a method and if [33:28] uh you never call it in a template class [33:31] the tree is not going to be [33:33] audible right it's not nothing's going [33:35] to happen you write the wrong method you [33:38] don't call it and it's all good between [33:40] friends [33:43] right you got to use the friend keyword [33:45] somewhere [33:47] so yes [34:01] so the question was uh instead instead [34:04] of implementing owns or or as an [34:06] implementation of [34:07] owns right could a fallback allocator [34:10] allocate a bit more memory and put a [34:34] right um let me see if that could even [34:37] work what if the [34:40] primary Al so always both in the primary [34:43] and the secondary allocate the [34:45] Boolean yeah we could do that but that's [34:48] essentially that's time and space it's [34:51] time I'm wasting on this implementation [34:54] and space I'm wasting in the in the [34:55] thing whereas in some Ator owns is a [34:59] trivial oneliner and I'm going to get [35:01] there so the answer is time and space I [35:04] we it would be wasteful to do so for [35:06] many allocators and also let me kind of [35:09] specify here that very often what's the [35:11] fallback allocator it's got to be [35:13] Malo like the last response of of Kings [35:17] is going to be like ah the hell with it [35:18] let's use Malo and call it a day right [35:21] and trying the strategies and stuff and [35:22] the end of the day I'm going to use Malo [35:24] or the N allocat or whatever have have [35:26] you but essentially it's going to be [35:27] alloc that's very dumb and doesn't know [35:29] much about ownership so it's nice that [35:32] the primary knows needs to know but the [35:35] the the the fallback doesn't need to [35:36] know about owns so all [35:39] right [35:44] um so suddenly I have a working system [35:47] because I you know it's funny I wrote [35:49] only two liners three liners four liners [35:51] and suddenly we have something that's [35:52] working because it's trivial to write a [35:55] stack allocator with what we have [35:58] has a con you know has a pool of data it [36:02] just sits on the [36:03] stack and it moves the pointer along [36:06] whenever allocate it bumps the pointer [36:07] whenever the allocate it does nothing or [36:09] bumps the pointer back we'll get that [36:12] and suddenly we have something that [36:13] works because we have a fallback [36:16] allocator that takes a stack allocator [36:17] of 16 kilobytes and if that fills up [36:22] we're going to use the [36:25] malator question [36:28] can I do the allocation in the stack [36:35] allocator who's with [36:39] yes who's qualified yes who's we [36:42] qualified [36:43] yes everybody should be qualified yes [36:46] because depend depends on qual the [36:48] qualifier could be negative always [36:51] right John you're with yes or with no [36:54] I'm with qualified yes [36:57] yes so it all depends on the qualifier [36:59] excellent um cast or volatile who [37:03] qualifier [37:04] so here's where I'm getting with [37:07] this uh yeah here's where I'm going with [37:10] this uh deallocate I'm going to come [37:12] back no worries so the allocate goes [37:15] like this if the the pointer pointing to [37:19] the beginning of the [37:21] block plus the size of the block rounded [37:24] up to the alignment I'm going to explain [37:26] that in a second and if that guy is [37:30] exactly where my stack allocation my [37:33] stack allocator uh pool ends then I can [37:36] do deallocation in other words in plain [37:39] language that means if this deallocation [37:42] is the deallocation of the last [37:44] allocation that happened then I can [37:46] deallocate because I can't roll back the [37:48] pointer but if it's somewhere in the [37:50] middle I [37:52] can't [37:54] okay so with a stack allocator so [37:57] whenever allocate you move the pointer [37:58] forward so behind this allocated space [38:01] in front of is the available free space [38:04] right so if there's it just so happens [38:06] that some guy like way behind you wants [38:09] to deallocate memory it can't be done [38:11] but if this is the last allocation that [38:13] happened I can make a step back right so [38:16] that pointer can move back no problem [38:18] without uh without [38:21] harm okay so we have a conditional [38:25] deallocation which is interesting uh as [38:28] as we talked like ons is Trivial because [38:30] it's like a very simple range test it's [38:33] very [38:34] cheap so uh we also have a new primitive [38:38] which again comes from just looking at [38:40] the the definition there and uh we have [38:42] a deallocate all that's very cheap and [38:45] very risky as it were because it just [38:47] goes resets everything and makes the [38:50] memory usable [38:51] again yes qualified question [39:15] right [39:17] yeah yeah so you don't need to you don't [39:19] need to qualify but essentially here [39:20] here's the deal at this level I'm only [39:23] worrying about void star and size that's [39:25] it you don't a about any more complex no [39:30] rat's but is [39:31] involved that's [39:33] true so at this level and it's kind of [39:37] interesting so there's some there's [39:38] something deep to this because many [39:40] people approach allocation as a kind of [39:42] object and everything matter but [39:45] actually can isolate the two quite [39:47] cleanly it turns out and then you can [39:49] build a type allocators on on top of [39:51] down type allocators so let me give a [39:53] few more explanations about the stack [39:55] allocator because I think it's [39:57] interesting [39:57] thing um well [40:02] let okay you got to be really good with [40:04] this you got to have a degree so [40:07] um it's very easy to use it because it [40:10] just allocate stuff and you use scope [40:12] exit from my previous Talk [40:14] t and you deallocate stuff and you kind [40:17] of use it and all is good and it's this [40:20] is actually a very powerful pattern this [40:22] whole stack allocator backed by Malo [40:25] because it turns out you can accelerate [40:27] your application like 95% of all cases [40:30] by doing stack allocation which is like [40:32] super hot super great super awesome and [40:35] it you know most of the time like you [40:37] know web server whatnot your requests [40:39] are going to be small enough to fit on [40:41] the stack and you're still going to be [40:43] have correct code because whatever [40:46] larger allocation requests come about [40:48] you're going to delegate to [40:50] Malo and this is like a very powerful [40:52] pattern if you can actually make that [40:54] generic this is awesome right um let's [40:58] look at uh the allocate function because [41:00] I promised you a few details about this [41:02] whole round to aligned who can tell me [41:04] what round to line does and what's the [41:07] purpose of it stack allocator is going [41:10] to move the pointer back uh forth in the [41:13] in the buffer and it can't move like it [41:18] let's say I want to allocate one bite [41:21] and I start with a stack like 16 [41:22] kilobytes and let's say it's aligned [41:23] nicely to like word size and I'll get [41:26] one bite so I'm going to move the point [41:28] the point I'm going to move from an [41:29] aligned address to align address plus [41:32] one [41:34] B do you see the tsunami that's being [41:39] initiated here what is it [41:41] please the next allocation is going to [41:44] be unaligned right how do you fix [41:47] that [41:50] yes you can run you know so there's a [41:53] number of solutions and the simplest one [41:55] is to whenever you get a request you run [41:57] it out first [41:58] thing and then you kind of allocate and [42:00] if you start with rounded you you add [42:02] the round number math says it's going to [42:06] work out right yes why not [42:18] LA right so the question was why not [42:21] being lazy about it and instead of [42:23] aligning compulsively at the beginning [42:25] of the allocation you kind of say well [42:27] I'm going to align kind of on a neat [42:29] basis right that's a valid solution I [42:32] got to say that's that's I don't see a [42:34] big downside to it right so it's a good [42:37] solution you got to have the type [42:38] information let's say you can make it [42:40] work so good point great so um in this [42:46] solution particularly I round up [42:48] compulsively and I have a test for did I [42:51] reach the end do I have room for this [42:53] guy and Etc so uh that kind of stuff [42:57] great um notice that my code is like [43:00] null PTR compliant I use null PTR and [43:05] actually I'm using also like the brace [43:07] initializers and everything but I do see [43:09] a bug in the slide which is n PTR is not [43:12] blue which bothers me to no [43:15] end [43:16] okay that's a bit of a bummer okay the [43:20] second um sort of I would say it's yeah [43:24] maybe this [43:27] maybe it's not the second but it's a [43:28] very important pattern in memory [43:29] allocation is a free [43:32] list before looking at the [43:33] implementation who uh who could explain [43:35] what the free list is and does except [43:38] from Fedor whom I [43:40] know he's gonna [43:45] yes it's not a fix size allocator oh it [43:48] is but I mean I misunderstood sorry go [43:50] ahead [43:57] so you make a list of uh fix size chunks [44:00] and you whenever people ask for it if it [44:03] just so happens is the right size you [44:04] just give it from the list um that's a [44:07] list but not a free list the free aspect [44:10] is interesting because the free list has [44:11] almost no overhead in in memory because [44:14] it uses freed memory to hold its [44:17] information [44:20] um free list who [44:22] canive intrusive it's a sort of an [44:25] intrusive list how does it work yes [44:30] yes [44:32] yes the the free list enters in action [44:35] when you deallocate that's why it's free [44:38] list it's right so the funny thing about [44:40] the free list is it starts empty it [44:41] starts with nothing I know nothing but [44:43] it has a backup allocator and the first [44:46] allocations are going to go use the [44:47] backup allocator but whenever deallocate [44:50] the fre wakes up and comes and says Ah [44:53] is this the allocation of a very [44:55] specific size that I care about and if [44:58] so I'm going to thread it through a link [45:00] list to which I only have one pointer [45:03] which is the head of the [45:05] list and whatever an allocation comes [45:07] about later the freist goes huh is this [45:12] allocation for this exact size and is my [45:16] point or Not N has have people freed [45:19] object of the of of the size before and [45:22] if so it's going to give you that object [45:23] in all of one it's a very powerful [45:26] pattern [45:28] what are the downsides of a free [45:31] list it never it's never all advantages [45:33] you [45:37] know you never give memory back to the [45:41] backing story The freelist could go [45:44] forever so if you happen to allocate at [45:46] some point a million objects of size 64 [45:49] whatever you have for the free list and [45:52] then you free them the freeze is going [45:53] to keep them and it's going to fragment [45:54] your memory up to wo right and another [45:58] disadvantage [46:09] Fedor so to summarize freist have I'm [46:13] going to put it nicely have poor thread [46:18] Affinity awesome have poor thread [46:20] Affinity meaning a thread allocates puts [46:22] on a global free list and another thread [46:24] allocates and that the memory that was [46:26] owned by a thread thread just a second [46:27] ago you know nanc ago is going to go to [46:30] a different thread and that's kind of [46:32] weird it's going to have a cold cache [46:33] and everything John ask does [46:40] it what difference does it make if it's [46:43] a different thread [46:47] compc [46:49] um well why don't you answer [46:53] that well it depends whether or not [46:55] you're willing to suffer the global [46:56] renion [47:12] that's why you want to have that's why [47:14] to you have to okay so you're kind of [47:16] you guys are getting to something I was [47:18] getting to anyway although there's no [47:20] slides for it so I have no proof here [47:22] but let me tell you this anybody who [47:26] defines a fre allocator the first thing [47:28] they do they make a thread local free [47:30] list and that's the first line of [47:32] defense [47:33] there so you have like R you know n [47:36] threads and each is going to have a mini [47:38] free list and if those kind of don't [47:41] work you may have a global free list or [47:43] not as it's your choice so I'm I'm [47:46] giving you the engine to do that I'm not [47:48] telling you how to do it right uh but [47:51] this is an important Point freelists are [47:53] contention intensive when you want to [47:56] share them what's another disadvantage [47:58] with relase subtle [48:00] freist another disadvantage [48:02] yes memory fragmentation we talked about [48:05] that [48:23] yes oh yeah so it's intrusive and [48:27] therefore if you mess with a already [48:29] freed memory you're going to mess your [48:30] free list yes agreed um okay [48:56] right so [49:03] it right so okay let me summarize you [49:07] guys got to ask like you know give like [49:09] shorter um so in short it's complicated [49:14] right I'm not kidding I'm not kidding so [49:17] essentially freelists have like really [49:19] weird Behavior depending on what's going [49:22] on in your system and sort of where what [49:24] I want wanted to uh to uh to say say [49:26] here the very solid disadvantage of [49:28] freas which is nonintuitive [49:30] is they are very cash [49:36] adverse and people at when you first [49:39] look at them they look great because [49:40] like fre it's hot memory because I'm [49:42] kind of getting from the freelist I'm [49:44] releasing memory and getting back and [49:45] it's kind of a great thing and actually [49:48] that's not the case often very often [49:51] freelists are actually adverse to to to [49:54] cash they're unfriendly to the cash and [49:56] and uh one one subtle aspect that [49:58] happens is because of all those pointers [50:00] that you have to write is you write in [50:03] cold memory very often when you free [50:05] memory so it's it's not it's not the [50:08] best thing because many applications [50:10] free memory that's already called and um [50:13] a good allocator tries to not write to [50:17] cold memory it turns out memory that's [50:19] being freed is very often cold right [50:23] okay uh nevertheless freas do have um [50:26] kind of they're great for small objects [50:28] they they do have great advantages so I [50:31] kind of feel bummed a bit that we kind [50:32] of dwell on the disadvantage so much uh [50:35] they're actually very fast if you use [50:36] them [50:38] right um so let's do it so I'm going to [50:43] template on a what's Class A the first [50:46] template [50:48] parameter it's my parent allocator the [50:51] the allocator that I'm I'm U sitting on [50:54] top of with my free list right and this [50:57] is generic so it could be any other [50:59] allocator including Malo including a [51:00] stack allocator whatever right and we [51:04] have a specific size that the list is [51:05] interested in and the allocate function [51:08] actually you can see the cursor if you [51:09] squint really hard it's next to public [51:11] right now you see it who's got good eyes [51:13] right so he's next to public right now [51:15] so I'm going to move it slowly and I'm [51:16] going to show you this is [51:17] allocate um so if it just so happens [51:21] that the size is the size I'm interested [51:23] in and if the root is not null then I'm [51:26] going to to be able to return the next [51:28] item in the list and this is a really [51:31] quick operation it's all one it's nice [51:33] otherwise I'm going to allocate from the [51:35] parent so this is my allocation [51:37] function the deallocation function [51:41] is [51:43] um if well if if the length of the block [51:46] is not my interesting size then I'm [51:48] going to deallocate from the parent [51:49] because there's nothing to do but [51:51] otherwise I'm going to essentially I do [51:54] that trigger with writing at the in the [51:56] first uh word of the memory that's being [51:58] deallocated I'm going to write my next [52:00] node and I'm going to maintain the [52:01] singly list uh singly L list that way [52:05] which is great again it's a quick [52:08] operation all right so there are a few [52:11] obvious improvements to the freelist [52:13] allocator such as I want a tolerance [52:15] between minimum and maximum for example [52:18] if my free list is very good at [52:19] allocating one kilobytes one kilobyte [52:22] chunks right and you're asking for 900 [52:25] bytes [52:27] should I give you a little leeway [52:31] there maybe I should right so I should [52:33] have a tolerance I you know anything [52:35] between uh 512 and 1 kilobyte is going [52:39] to go in the free list of 1 kilobyte and [52:41] people do that actually right so this is [52:44] great it's very easy to add too [52:46] allocating batches what's that [52:49] idea allocate in [52:53] batches yes [53:05] right so instead of allocating one [53:08] object at a time I allocate a bunch of [53:09] them and I'm threading the frees through [53:12] them uh even though it's contiguous and [53:14] then I'm going to have a bunch of [53:16] objects and it's going to cost me a lot [53:18] less what's the problem there it's hard [53:21] to detect when you can deallocate that [53:22] big block right so that's it's kind of [53:24] more difficult and and uh very [53:27] interesting optimization is add an upper [53:29] bound no more than top elements to the [53:32] freist because you don't want it to grow [53:34] infinitely so this is kind of a very [53:36] good line of defense against that kind [53:38] of freas that go so you can say you know [53:41] what I'm going to fix my free list to [53:43] 1,000 elements no more 1,24 whatever and [53:46] then if you go beyond that you saturate [53:48] the freelist and you're kind of done uh [53:51] accumulating in it and that works great [53:54] and it's simple to implement [53:56] okay [53:59] um so with all you know with everything [54:02] that that's been said here's what the [54:04] realistic freas looks looks like it has [54:07] a parent allocator it has a minimum size [54:10] uh in this case 17 it has a maximum size [54:13] 32 so that's my tolerance between 78 and [54:15] 32 I'm going to keep the objects in the [54:18] free list and I'm going to allocate [54:20] eight objects at a time because I so I [54:22] so can and I'm not going to remember [54:24] more than 1,24 1 Kil element or one kilo [54:27] element as it were and that's my [54:32] allocator [54:35] uh how do you free the individual [54:38] elements in the [54:42] list when you [54:45] allocate you're [54:47] allocating yes how do you one of them oh [54:51] you you put it back in the list you're [54:53] never free it what does it mean that is [54:57] 32 uh it means for any requests between [55:00] 17 and 32 I'm going to allocate 32 [55:03] bytes and oh okay okay the other upper [55:07] bound you may be referring to is the [55:08] 1024 which is the top the maximum length [55:11] of the free list right okay so this sort [55:15] of more realistic free list and it's not [55:17] difficult to impl but it's it's a good [55:19] chunk of work especially this whole [55:21] allocate at a time it's kind of a it's [55:23] difficult it's not easy it's not trivial [55:25] but it's feasible and the nice thing is [55:27] now you have a free list in your hands [55:29] that you can use with any other [55:31] allocator question can I layer um a free [55:34] list on top of a stack [55:37] allocator yes and it's going to kind of [55:39] be an interesting device because it's [55:41] going to give me the ability to free [55:44] things uh in the middle of the allocator [55:46] if they are the right size so there's [55:50] the there's compound interesting here [55:54] right I'm gaining composition [55:56] strength here right [56:00] great how about an AIX [56:06] allocator this is an absolute classic in [56:08] memory allocation whenever you uh [56:10] whenever people do like kind of advanced [56:11] allocators they they kind of um parasite [56:15] another allocator and they kind of add a [56:16] bit of a few bits of data before and [56:19] after the block what's a good use of [56:22] that electric fence like you put like [56:25] you put a signature here put a signature [56:27] here and when you deallocate if the [56:28] signatures are not correct you kind of [56:31] say oh there's a corruption there right [56:34] so you can check you can insert like [56:36] safeguards you can you can put one [56:38] kilobyte before and one kilobyte after [56:40] large chunks and verify that when did [56:43] the allocate they didn't write to those [56:45] chunks right so people do that so you [56:49] have an allocator that knows how to [56:52] parasite a so they're going to sit on [56:54] top of a and they're going to allocate [56:58] before every allocation they're going to [57:00] put a prefix object and at the end of [57:03] the every allocation they're going to [57:04] put a suffix object which by default is [57:06] void meaning [57:07] nothing and that's a great allocator to [57:10] have as a composition [57:14] device so you can use it for debug studs [57:16] info actually what I've seen people use [57:18] this for very nice um is whenever you [57:22] allocate [57:23] memory you store in the prefix of the [57:26] block the file and the line that [57:29] requested [57:30] memory this is awesome did you see like [57:32] chandel's talk this morning right the [57:35] plary talk that's awesome because it [57:37] showed all these great things with perf [57:39] you can do even though people who use [57:42] OSX they don't have it they're going to [57:44] kind of right on Windows I'm sure they [57:46] have like good tools for that yes yes [57:50] you work for Microsoft don't you yes [57:53] that was an affirmative so Microsoft [57:56] tools and probably they're more visual [57:57] as well Unix has this perf tool OSX has [58:00] nothing which is a bummer but anyhow you [58:04] know all of these great things you can [58:06] see with perf you can't see who's asking [58:08] for memory in perf you can't it's dark [58:11] matter you can't see [58:13] it with this kind of stuff you can so [58:16] you can actually insert you can plop the [58:17] file in line the requested memory and [58:19] then when you free oh so this guy this [58:22] line in the this line in my 1 million [58:24] lines program is the top Contender for [58:27] memory allocation this is awesome it's [58:30] great information AIX [58:33] allocator [58:35] and to Wi a very simple use of AIX [58:39] allocator is an allocator that collects [58:41] statistics and I capacity flag what [58:43] statistics I'm interested in and it's [58:45] going to collect statistics for me so [58:47] it's going to be how many calls do I [58:49] have to allocate how many call to De [58:50] allocate and all of these nice uh pieces [58:53] of information [58:56] I can have par allocation data as we [58:57] just talked I could have Global [58:59] Information and I can slice and dice it [59:01] after the program has ended any way I [59:03] want [59:06] it [59:08] um okay how how long do we [59:11] have oh what happened to the five now [59:14] telling me where were the 15 [59:16] minutes five minutes dude [59:19] okay um the most interesting part of [59:22] this talk you're not going to hear it [59:28] okay thanks five [59:29] minutes so after this's a break can I [59:32] eat 5 minutes into your [59:35] break okay I'm seeing like a couple of [59:38] folks are literally sleeping so like I [59:40] don't you know I don't have an opinion [59:42] you know I'm going to sleep anyway so um [59:46] okay [59:47] so very nice trick and I can't I can't [59:51] not even though I'm not going to go [59:52] through the slides don't mess up my [59:54] review because this is nice thing even [59:56] though it's not in the slides here's how [59:58] you can greatly collect information [60:00] about memory allocation you write a [60:03] macro you know that's going to you know [60:05] allocate with all capitals or whatever [60:07] and inside the macro you put static Char [60:11] cons [60:11] charar this file static U unsigned int [60:16] sorry int long [60:19] line okay I'm trying to be politically [60:21] correct here and then you put a pointer [60:24] next [60:27] static [60:29] data okay and then whenever you uh you [60:32] allocate and stuff you're going to [60:33] update some statistics all of which you [60:35] keep in a static structure this is my [60:37] point it's all static but and you have a [60:40] pointer to sort of the next static piece [60:43] of [60:44] information and here's the thing you [60:46] build a you compile the [60:48] program and at the end of the program [60:52] essentially have file and line [60:53] information and a singly link list [60:56] that spans all of these places where [60:59] information has been stored which is [61:01] static data in your program and you can [61:03] iterate it and get information with no [61:06] Dynamic allocation at [61:09] all so again just to clarify so actually [61:13] it don't say static you define a little [61:15] struct and you make a static in [61:17] instantiation of that whenever we [61:18] allocate memory and you collect [61:20] information in there and these guys are [61:23] going to assemble in a in a singly link [61:25] list and the the the whole thing [61:26] pointers and everything they all point [61:28] in static storage and without allocating [61:31] one bite of memory of memory dynamically [61:34] you collect at the end of the run you [61:35] have a pointer to the beginning of the [61:37] list and you thre you have all of your [61:39] data write me email if you want details [61:41] about this because it's awesome okay so [61:44] now how many minutes [61:46] 10 [61:48] okay all right beatmap block this is a [61:52] very good allocator because it stores [61:54] one bite per block oh one bit per block [61:56] of information so you have a hunk of [61:59] memory and you say I'm going organizing [62:01] this guy in blocks of 64 bytes for every [62:04] 64 bytes you're going to have at the [62:06] beginning of the block one [62:08] bit which tells you whether the block is [62:11] occupied or not so you have metadata one [62:14] bit per block and then you have the [62:16] blocks contiguous right what's the great [62:19] advantage of the [62:21] scheme yes all disant get rid of of all [62:26] the disadvantage of free list you can [62:28] you know you can allocate any size [62:29] because you allocate several blocks you [62:32] store the metadata in a separate place [62:34] which is always going to be hot this is [62:37] a recipe for great things in memory [62:39] allocation store metadata elsewh than [62:42] the [62:43] data so you have the bits and there's [62:46] beat oper like CPU uh Constructors have [62:49] been working hard on bit operations that [62:51] are fast and you can do like simd and [62:53] all these good things on the bits there [62:56] so it's pretty [62:57] awesome so this I highly recommend this [63:00] what are the disadvantages because [63:01] there's never like only advantages [63:05] disadvantages [63:07] huh yeah so the block size is going to [63:10] be like a sort of a it's going to have [63:12] internal fragmentation right it's going [63:13] to allocate more than needed right [63:15] because of that so um there's never a [63:18] thing with only advantages [63:20] right and also like multi-threading is [63:23] weird you can't really those bits are [63:25] kind of messed up if you try to share [63:27] like there's all this staring and stuff [63:29] going on so um anyhow I highly recommend [63:33] this and uh there are a few more exotic [63:37] allocators which I think uh I think you [63:41] going [63:42] like so cascading [63:47] allocator consider I have a beatmap [63:49] block but a big block sits on top of [63:51] like one megabyte [63:53] block so I allocate one megabyte and I'm [63:55] sitting I'm I'm plopping a bit one of [63:58] these guys on it and he knows how to [63:59] organize that that [64:01] megabyte but then I'm running out of I'm [64:03] I'm done I'm allocating more than one [64:05] megabyte what should I do [64:07] next allocate another megabyte and [64:10] organize that guy as a bit map block and [64:12] continue this is the cascading allocator [64:15] it's going to allocate lazily as many as [64:17] you [64:18] need right and it's going to cleverly [64:21] deallocate when you no longer use them [64:23] and this there's a fair amount of logic [64:25] going there but it's very uh it's very [64:28] coherent uh not difficult to [64:32] do another very useful allocator is the [64:39] segregator what does it do quick without [64:42] with zero [64:48] minutes [64:50] what parti who who said that thanks [64:53] Marshall oh Marshall so we should you [64:56] guys can't have Marshall answer every [64:58] question here [65:00] so everything less than or equal to the [65:02] threshold goes to a small allocator [65:04] everything greater than the threshold [65:06] goes to large allocator period done any [65:09] two allocators will work awesome this is [65:12] composition for you and these are each [65:15] they take like 50 lines each maximum [65:17] except for the he you know that bit [65:19] block thing is like much bigger but [65:21] these are simple and then you compose [65:23] them in ways that you couldn't imagine [65:26] so with this guy this is like an [65:28] absolute classic memory allocation small [65:30] objects go for this this allocator large [65:32] objects go to this guy and this is how [65:34] the history writes itself right [65:36] so um this is a very good do I need owns [65:40] for implementing this [65:42] guy no because the size tell me [65:45] everything size tells me what I need to [65:47] do where it goes right so no problem [65:50] there segregator is self-c composable it [65:52] can compose with itself with another [65:56] segregator and here's how it goes I have [65:59] a segregator everything smaller than 4 [66:01] kilobytes goes to another segregator [66:03] which in turn uses a freeas for [66:06] everything smaller than 128 bytes and [66:08] some medium allocator that I so Define [66:11] or everything bigger than 4 kilobytes [66:13] goes to the [66:14] allocator in fact if you compose [66:16] actually a good allocator is maybe 16 of [66:19] these [66:20] aggregators and you know how you [66:21] organize them you organize like in a [66:23] binary tree binary search kind of thing [66:26] you don't search for linearly forze [66:28] you're clever about it you put the you [66:30] know you lay out the code by the binary [66:34] search so you look at the code and you [66:36] see oh this is this is optimized for [66:38] these sizes right and actually so [66:42] there's more philosophy to this [66:43] sometimes you do want the linear search [66:45] because of smaller sizes are both more [66:47] frequent and they're also like it's you [66:50] know if you allocate a big hunk of [66:52] memory you're going to do a lot of work [66:53] with it so you can't afford to be slower [66:56] so there's a lot of philosophy that goes [66:58] in there but it's up to you that's what [67:00] I'm giving you the tools you use [67:03] them bucke Tizer and probably this is [67:06] the last one that we're going to talk [67:08] about what's the bucke Tizer do just by [67:10] looking at the [67:12] definition bucke [67:14] Tizer I'm organizing yeah use buckets [67:18] yes thank you very [67:20] much all right so between sizes minimum [67:24] maximum no that during [67:27] compilation with a specific step I'm [67:30] going to for each category of size I'm [67:32] going to have a separate [67:34] allocator right so I'm bucketization [67:46] a very simple version of the bucket is [67:49] one that does logarithmic growth this [67:52] linear logarithmic everything like the [67:55] first part two second power of two and [67:56] so on [67:57] right all right with this we're done [68:00] friends because most everything you see [68:03] about memory allocation is covered by [68:05] these simple [68:07] patterns almost [68:10] everything I'm not kidding so this kind [68:12] of we're done with the [68:14] subject which is very interesting [68:16] because we didn't do a lot of work and [68:18] you know we like being lazy like you [68:20] know feder say why don't you do it [68:22] lazily yes let's be lazy all the way [68:25] right so we're kind of done here with [68:27] the domain of memory [68:29] allocation with remarkably little code [68:32] and remarkably little smarts we just [68:35] wanted to do composition and we followed [68:37] the [68:38] thread so let me um uh tell you a bit [68:42] more [68:43] about the entire this is a realistic [68:46] example by the way so as I said you know [68:48] there's a realistic alate is going to [68:49] have a bunch of these strategies [68:51] composed [68:52] together um but uh let me give you you [68:55] the complete API just a [68:57] minute so this is the API that I found [69:00] works for everything we talked about and [69:02] more actually so we have a static Conex [69:05] for this is my alignment the alignment [69:08] of the [69:09] allocator um we have a primitive called [69:12] good size which I give you a number and [69:15] you give me the size that you are going [69:17] to allocate anyway usually is the number [69:19] rounded up to the next allocation size [69:22] very useful because I can actually when [69:24] you do Vector stuff you can say oh let [69:27] me allocate the good size for this guy [69:28] anyway so I can maximize my is of memory [69:31] right so we have good size very useful [69:34] primitive we have the allocate and uh we [69:37] have the allocate primitive we have an [69:39] new primitive called allocate all which [69:42] is great for stack allocator yeah you [69:44] don't use that with [69:47] Malo actually the malator doesn't [69:50] doesn't implement it doesn't Define it [69:52] right it's not there but with a stack [69:55] can say allocate all and then organize [69:57] the free list or whatever on top of it [69:59] um expand I want to expand the Block in [70:01] place reallocate I want to do what [70:04] realog does owns we talked about it [70:07] deallocate obvious deallocate all which [70:09] is like de allocate everything in all [70:14] one we ah there's the alignment thing [70:17] you got to do it people do that you need [70:20] it for games who doesn't play games [70:23] right yes so you got to have aligned [70:26] allocate so therefore it's in the API [70:29] it's a necessary evil people need [70:31] that um you have Alon you have [70:34] Primitives on post and windows you just [70:36] rra them and you're [70:38] done [70:39] finally getting there are you bored okay [70:43] it doesn't want to go [70:45] forward all right so what we talked [70:47] about here first so first of all we [70:49] destroyed the allocator because it [70:51] doesn't no composition whatever they do [70:54] with it has knows nothing about [70:57] composition right and as I argued [71:00] composition is everything you should [71:02] care about in memory allocation writing [71:05] good allocators is all about good [71:07] composition okay so Fresh Approach from [71:11] first principle would be to design an [71:13] API for composable allocators that are [71:16] simple and work together well in any way [71:19] you want with what you saw you can [71:22] Implement pretty much any you can you [71:24] can do anything thing with regard to [71:27] memory allocation you can't fly right so [71:29] don't try at home right you know there a [71:32] professional driver close course in [71:34] commerci right you can't do anything [71:37] really so understanding history is great [71:40] because I couldn't figure out why [71:41] allocator was a fail until I understood [71:45] its [71:46] history because you look at it you know [71:49] has allocated has deallocated has [71:50] construct has destroyed it has [71:53] everything and it was difficult for me [71:56] to get away from the frame of mind in [71:58] which it exists therefore it must be [72:01] good to the point where you know this [72:03] may be a mistake and let me see if I [72:06] kind of clean my mind off of everything [72:09] let me see if I can do better right and [72:11] better means [72:14] composable okay with this I'm seeing [72:17] people are like okay the hell with it [72:18] I'm going so thanks very much I'll be [72:20] here for a few more minutes