]> git.ipfire.org Git - thirdparty/gcc.git/blob - libphobos/src/std/experimental/allocator/building_blocks/free_tree.d
d: Import dmd b8384668f, druntime e6caaab9, phobos 5ab9ad256 (v2.098.0-beta.1)
[thirdparty/gcc.git] / libphobos / src / std / experimental / allocator / building_blocks / free_tree.d
1 // Written in the D programming language.
2 /**
3 Source: $(PHOBOSSRC std/experimental/allocator/building_blocks/_free_tree.d)
4 */
5 module std.experimental.allocator.building_blocks.free_tree;
6
7 import std.experimental.allocator.common;
8
9 //debug = std_experimental_allocator_free_tree;
10
11 /**
12
13 The Free Tree allocator, stackable on top of any other allocator, bears
14 similarity with the free list allocator. Instead of a singly-linked list of
15 previously freed blocks, it maintains a binary search tree. This allows the
16 Free Tree allocator to manage blocks of arbitrary lengths and search them
17 efficiently.
18
19 Common uses of `FreeTree` include:
20
21 $(UL
22 $(LI Adding `deallocate` capability to an allocator that lacks it (such as simple regions).)
23 $(LI Getting the benefits of multiple adaptable freelists that do not need to
24 be tuned for one specific size but insted automatically adapts itself to
25 frequently used sizes.)
26 )
27
28 The free tree has special handling of duplicates (a singly-linked list per
29 node) in anticipation of large number of duplicates. Allocation time from the
30 free tree is expected to be $(BIGOH log n) where `n` is the number of
31 distinct sizes (not total nodes) kept in the free tree.
32
33 Allocation requests first search the tree for a buffer of suitable size
34 deallocated in the past. If a match is found, the node is removed from the tree
35 and the memory is returned. Otherwise, the allocation is directed to $(D
36 ParentAllocator). If at this point `ParentAllocator` also fails to allocate,
37 `FreeTree` frees everything and then tries the parent allocator again.
38
39 Upon deallocation, the deallocated block is inserted in the internally
40 maintained free tree (not returned to the parent). The free tree is not kept
41 balanced. Instead, it has a last-in-first-out flavor because newly inserted
42 blocks are rotated to the root of the tree. That way allocations are cache
43 friendly and also frequently used sizes are more likely to be found quickly,
44 whereas seldom used sizes migrate to the leaves of the tree.
45
46 `FreeTree` rounds up small allocations to at least $(D 4 * size_t.sizeof),
47 which on 64-bit system is one cache line size. If very small objects need to
48 be efficiently allocated, the `FreeTree` should be fronted with an
49 appropriate small object allocator.
50
51 The following methods are defined if `ParentAllocator` defines them, and forward to it: `allocateAll`, `expand`, `owns`, `reallocate`.
52 */
53 struct FreeTree(ParentAllocator)
54 {
55 static assert(ParentAllocator.alignment % size_t.alignof == 0,
56 "FreeTree must be on top of a word-aligned allocator");
57
58 import std.algorithm.comparison : min, max;
59 import std.algorithm.mutation : swap;
60 import std.traits : hasMember;
61
62 // State
63 static if (stateSize!ParentAllocator) private ParentAllocator parent;
64 else private alias parent = ParentAllocator.instance;
65 private Node* root; // that's the entire added state
66
67 private struct Node
68 {
69 Node*[2] kid;
70 Node* sibling;
71 size_t size;
72 ref Node* left() { return kid[0]; }
73 ref Node* right() { return kid[1]; }
74 }
75
76 // Removes "which" from the tree, returns the memory it occupied
77 private void[] remove(ref Node* which)
78 {
79 assert(which);
80 assert(!which.sibling);
81 auto result = (cast(ubyte*) which)[0 .. which.size];
82 if (!which.right) which = which.left;
83 else if (!which.left) which = which.right;
84 else
85 {
86 // result has two kids
87 static bool toggler;
88 // Crude randomization: alternate left/right choices
89 toggler = !toggler;
90 auto newRoot = which.kid[toggler], orphan = which.kid[!toggler];
91 which = newRoot;
92 for (Node* n = void; (n = newRoot.kid[!toggler]) !is null; )
93 {
94 newRoot = n;
95 }
96 newRoot.kid[!toggler] = orphan;
97 }
98 return result;
99 }
100
101 private void[] findAndRemove(ref Node* n, size_t s)
102 {
103 if (!n) return null;
104 if (s == n.size)
105 {
106 if (auto sis = n.sibling)
107 {
108 // Nice, give away one from the freelist
109 auto result = (cast(ubyte*) sis)[0 .. sis.size];
110 n.sibling = sis.sibling;
111 return result;
112 }
113 return remove(n);
114 }
115 return findAndRemove(n.kid[s > n.size], s);
116 }
117
118 debug(std_experimental_allocator_free_tree)
119 private void dump()
120 {
121 import std.stdio : writef, writefln, writeln;
122 writeln(typeof(this).stringof, "@", &this, " {");
123 scope(exit) writeln("}");
124
125 if (!root) return;
126
127 static void recurse(Node* n, uint indent = 4)
128 {
129 if (!n)
130 {
131 writefln("%*s(null)", indent, "");
132 return;
133 }
134 for (auto sis = n; sis; sis = sis.sibling)
135 {
136 writef("%*s%x (%s bytes) ", indent, "",
137 cast(void*) n, n.size);
138 }
139 writeln;
140 if (!n.left && !n.right) return;
141 recurse(n.left, indent + 4);
142 recurse(n.right, indent + 4);
143 }
144 recurse(root);
145 }
146
147 private string formatSizes()
148 {
149 string result = "(";
150 void recurse(Node* n)
151 {
152 if (!n)
153 {
154 result ~= "_";
155 return;
156 }
157 import std.conv : to;
158 result ~= to!string(n.size);
159 for (auto sis = n.sibling; sis; sis = sis.sibling)
160 {
161 result ~= "+moar";
162 }
163 if (n.left || n.right)
164 {
165 result ~= " (";
166 recurse(n.left);
167 result ~= ' ';
168 recurse(n.right);
169 result ~= ")";
170 }
171 }
172 recurse(root);
173 return result ~= ")";
174 }
175
176 private static void rotate(ref Node* parent, bool toRight)
177 {
178 assert(parent);
179 auto opposing = parent.kid[!toRight];
180 if (!opposing) return;
181 parent.kid[!toRight] = opposing.kid[toRight];
182 opposing.kid[toRight] = parent;
183 parent = opposing;
184 }
185
186 // Inserts which into the tree, making it the new root
187 private void insertAsRoot(Node* which)
188 {
189 assert(which);
190 debug(std_experimental_allocator_free_tree)
191 {
192 assertValid;
193 scope(exit) assertValid;
194 }
195
196 static void recurse(ref Node* where, Node* which)
197 {
198 if (!where)
199 {
200 where = which;
201 which.left = null;
202 which.right = null;
203 which.sibling = null;
204 return;
205 }
206 if (which.size == where.size)
207 {
208 // Special handling of duplicates
209 which.sibling = where.sibling;
210 where.sibling = which;
211 which.left = null;
212 which.right = null;
213 return;
214 }
215 bool goRight = which.size > where.size;
216 recurse(where.kid[goRight], which);
217 rotate(where, !goRight);
218 }
219 recurse(root, which);
220 }
221
222 private void assertValid()
223 {
224 debug(std_experimental_allocator_free_tree)
225 {
226 static bool isBST(Node* n, size_t lb = 0, size_t ub = size_t.max)
227 {
228 if (!n) return true;
229 for (auto sis = n.sibling; sis; sis = sis.sibling)
230 {
231 assert(n.size == sis.size);
232 assert(sis.left is null);
233 assert(sis.right is null);
234 }
235 return lb < n.size && n.size <= ub
236 && isBST(n.left, lb, min(ub, n.size))
237 && isBST(n.right, max(lb, n.size), ub);
238 }
239 if (isBST(root)) return;
240 dump;
241 assert(0);
242 }
243 }
244
245 /**
246 The `FreeTree` is word aligned.
247 */
248 enum uint alignment = size_t.alignof;
249
250 /**
251 The `FreeTree` allocator is noncopyable.
252 */
253 this(this) @disable;
254
255 /**
256 The destructor of `FreeTree` releases all memory back to the parent
257 allocator.
258 */
259 static if (hasMember!(ParentAllocator, "deallocate"))
260 ~this()
261 {
262 clear;
263 }
264
265 /**
266 Returns $(D parent.goodAllocSize(max(Node.sizeof, s))).
267 */
268 static if (stateSize!ParentAllocator)
269 size_t goodAllocSize(size_t s)
270 {
271 return parent.goodAllocSize(max(Node.sizeof, s));
272 }
273 else
274 static size_t goodAllocSize(size_t s)
275 {
276 return parent.goodAllocSize(max(Node.sizeof, s));
277 }
278
279 /**
280
281 Allocates `n` bytes of memory. First consults the free tree, and returns
282 from it if a suitably sized block is found. Otherwise, the parent allocator
283 is tried. If allocation from the parent succeeds, the allocated block is
284 returned. Otherwise, the free tree tries an alternate strategy: If $(D
285 ParentAllocator) defines `deallocate`, `FreeTree` releases all of its
286 contents and tries again.
287
288 TODO: Splitting and coalescing should be implemented if `ParentAllocator` does not defined `deallocate`.
289
290 */
291 void[] allocate(size_t n)
292 {
293 assertValid;
294 if (n == 0) return null;
295
296 immutable s = goodAllocSize(n);
297
298 // Consult the free tree.
299 auto result = findAndRemove(root, s);
300 if (result.ptr) return result.ptr[0 .. n];
301
302 // No block found, try the parent allocator.
303 result = parent.allocate(s);
304 if (result.ptr) return result.ptr[0 .. n];
305
306 // Parent ran out of juice, desperation mode on
307 static if (hasMember!(ParentAllocator, "deallocate"))
308 {
309 clear;
310 // Try parent allocator again.
311 result = parent.allocate(s);
312 if (result.ptr) return result.ptr[0 .. n];
313 return null;
314 }
315 else
316 {
317 // TODO: get smart here
318 return null;
319 }
320 }
321
322 // Forwarding methods
323 mixin(forwardToMember("parent",
324 "allocateAll", "expand", "owns", "reallocate"));
325
326 /** Places `b` into the free tree. */
327 bool deallocate(void[] b)
328 {
329 if (!b.ptr) return true;
330 auto which = cast(Node*) b.ptr;
331 which.size = goodAllocSize(b.length);
332 // deliberately don't initialize which.left and which.right
333 assert(which.size >= Node.sizeof);
334 insertAsRoot(which);
335 return true;
336 }
337
338 @system unittest // test a few simple configurations
339 {
340 import std.experimental.allocator.gc_allocator;
341 FreeTree!GCAllocator a;
342 auto b1 = a.allocate(10000);
343 auto b2 = a.allocate(20000);
344 auto b3 = a.allocate(30000);
345 assert(b1.ptr && b2.ptr && b3.ptr);
346 () nothrow @nogc { a.deallocate(b1); }();
347 () nothrow @nogc { a.deallocate(b3); }();
348 () nothrow @nogc { a.deallocate(b2); }();
349 assert(a.formatSizes == "(20480 (12288 32768))", a.formatSizes);
350
351 b1 = a.allocate(10000);
352 assert(a.formatSizes == "(20480 (_ 32768))", a.formatSizes);
353 b1 = a.allocate(30000);
354 assert(a.formatSizes == "(20480)", a.formatSizes);
355 b1 = a.allocate(20000);
356 assert(a.formatSizes == "(_)", a.formatSizes);
357 }
358
359 @system unittest // build a complex free tree
360 {
361 import std.experimental.allocator.gc_allocator, std.range;
362 FreeTree!GCAllocator a;
363 uint[] sizes = [3008,704,1856,576,1632,672,832,1856,1120,2656,1216,672,
364 448,992,2400,1376,2688,2656,736,1440];
365 void[][] allocs;
366 foreach (s; sizes)
367 allocs ~= a.allocate(s);
368 foreach_reverse (b; allocs)
369 {
370 assert(b.ptr);
371 () nothrow @nogc { a.deallocate(b); }();
372 }
373 a.assertValid;
374 allocs = null;
375 foreach (s; sizes)
376 allocs ~= a.allocate(s);
377 assert(a.root is null);
378 a.assertValid;
379 }
380
381 /** Defined if `ParentAllocator.deallocate` exists, and returns to it
382 all memory held in the free tree. */
383 static if (hasMember!(ParentAllocator, "deallocate"))
384 void clear()
385 {
386 void recurse(Node* n)
387 {
388 if (!n) return;
389 recurse(n.left);
390 recurse(n.right);
391 parent.deallocate((cast(ubyte*) n)[0 .. n.size]);
392 }
393 recurse(root);
394 root = null;
395 }
396
397 /**
398
399 Defined if `ParentAllocator.deallocateAll` exists, and forwards to it.
400 Also nullifies the free tree (it's assumed the parent frees all memory
401 stil managed by the free tree).
402
403 */
404 static if (hasMember!(ParentAllocator, "deallocateAll"))
405 bool deallocateAll()
406 {
407 // This is easy, just nuke the root and deallocate all from the
408 // parent
409 root = null;
410 return parent.deallocateAll;
411 }
412 }
413
414 version (StdUnittest)
415 @system unittest
416 {
417 import std.experimental.allocator.gc_allocator;
418 testAllocator!(() => FreeTree!GCAllocator());
419 }
420
421 // https://issues.dlang.org/show_bug.cgi?id=16506
422 @system unittest
423 {
424 import std.experimental.allocator.gc_allocator : GCAllocator;
425 import std.experimental.allocator.mallocator : Mallocator;
426
427 static void f(ParentAllocator)(size_t sz)
428 {
429 static FreeTree!ParentAllocator myAlloc;
430 byte[] _payload = cast(byte[]) myAlloc.allocate(sz);
431 assert(_payload, "_payload is null");
432 _payload[] = 0;
433 () nothrow @nogc { myAlloc.deallocate(_payload); }();
434 }
435
436 f!Mallocator(33);
437 f!Mallocator(43);
438 f!GCAllocator(1);
439 }
440
441 // https://issues.dlang.org/show_bug.cgi?id=16507
442 @system unittest
443 {
444 static struct MyAllocator
445 {
446 byte dummy;
447 static bool alive = true;
448 void[] allocate(size_t s) { return new byte[](s); }
449 bool deallocate(void[] ) { if (alive) assert(false); return true; }
450 enum alignment = size_t.sizeof;
451 }
452
453 FreeTree!MyAllocator ft;
454 void[] x = ft.allocate(1);
455 () nothrow @nogc { ft.deallocate(x); }();
456 ft.allocate(1000);
457 MyAllocator.alive = false;
458 }
459
460 @system unittest // "desperation mode"
461 {
462 uint myDeallocCounter = 0;
463
464 struct MyAllocator
465 {
466 byte[] allocation;
467 void[] allocate(size_t s)
468 {
469 if (allocation.ptr) return null;
470 allocation = new byte[](s);
471 return allocation;
472 }
473 bool deallocate(void[] )
474 {
475 ++myDeallocCounter;
476 allocation = null;
477 return true;
478 }
479 enum alignment = size_t.sizeof;
480 }
481
482 FreeTree!MyAllocator ft;
483 void[] x = ft.allocate(1);
484 () nothrow @nogc { ft.deallocate(x); }();
485 assert(myDeallocCounter == 0);
486 x = ft.allocate(1000); // Triggers "desperation mode".
487 assert(myDeallocCounter == 1);
488 assert(x.ptr);
489 void[] y = ft.allocate(1000); /* Triggers "desperation mode" but there's
490 nothing to deallocate so MyAllocator can't deliver. */
491 assert(myDeallocCounter == 1);
492 assert(y.ptr is null);
493 }
494
495 @system unittest
496 {
497 import std.experimental.allocator.gc_allocator;
498 FreeTree!GCAllocator a;
499
500 assert((() nothrow @safe @nogc => a.goodAllocSize(1))() == typeof(*a.root).sizeof);
501 }
502
503 @system unittest
504 {
505 import std.experimental.allocator.building_blocks.region : Region;
506
507 auto a = FreeTree!(Region!())(Region!()(new ubyte[1024 * 64]));
508 auto b = a.allocate(42);
509 assert(b.length == 42);
510 assert((() pure nothrow @safe @nogc => a.expand(b, 22))());
511 assert(b.length == 64);
512 assert((() nothrow @nogc => a.reallocate(b, 100))());
513 assert(b.length == 100);
514 assert((() nothrow @nogc => a.deallocateAll())());
515 }