-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathallocator_prv.h
More file actions
3213 lines (3072 loc) · 172 KB
/
Copy pathallocator_prv.h
File metadata and controls
3213 lines (3072 loc) · 172 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/***************************************************************************
Copyright (C) 2002-2026 Kentaro Kitagawa
kitag@issp.u-tokyo.ac.jp
This file is dual-licensed under your choice of EITHER:
* Apache License, Version 2.0
(http://www.apache.org/licenses/LICENSE-2.0, or see
LICENSE-APACHE-2.0 in this directory)
-- OR --
* GNU General Public License, version 2 of the License,
or (at your option) any later version
(http://www.gnu.org/licenses/old-licenses/gpl-2.0.html,
or see LICENSE-GPL-2.0 in this directory).
Pick whichever license suits your project. Unless required
by applicable law or agreed to in writing, this file is
distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied
***************************************************************************/
#ifndef ALLOCATOR_PRV_H_
#define ALLOCATOR_PRV_H_
#ifndef USE_STD_ALLOCATOR
#include <new>
#include <cstdint>
#include <stdint.h>
#include <stdlib.h>
#include <stddef.h>
#include <atomic>
#include <limits>
#include <type_traits>
// ===== MSVC compatibility shim for the live pool =====
// The live pool is ON by default on MSVC (opt OUT via
// KAME_DISABLE_POOL_MSVC → USE_STD_ALLOCATOR, in which case this header
// isn't included). The pool core is written for GCC/Clang; map the
// GCC-isms MSVC lacks. Placed here (the pool-private header) rather than
// allocator.h so that ANY includer — incl. tests that include
// allocator_prv.h directly — gets the shim. (The 5 __sync_* atomic
// wrappers below carry their own _Interlocked* branch; TLS already falls
// to thread_local on MSVC.)
#if defined(_MSC_VER) && !defined(__GNUC__)
#include <intrin.h>
// noinline/cold/used/tls_model are codegen hints — safe to drop on
// MSVC. `section` is macOS-only; `constructor` is handled via static-
// init, not the attribute. So strip every __attribute__.
#define __attribute__(x)
#ifndef __builtin_expect
#define __builtin_expect(expr, c) (expr)
#endif
// constexpr bit-scan (C++17 relaxed constexpr) — _BitScan* aren't
// constexpr, but these feed constexpr ladder-bucket math. Branch/shift
// binary search (O(log width), ~6 ops) rather than an O(width) bit-walk:
// the per-call constexpr step count matters because kame_ladder_bucket
// runs inside the compile-time bucket_for_size LUT build AND its
// EXHAUSTIVE 369..32768 KAME_LUT_PROOF sweeps — an O(width) walk there
// blew MSVC's default `/constexpr:steps` (2^20) ceiling once the LUT was
// extended to the full 32 KiB range (C2131 at the LUT/proofs).
constexpr int kame_msvc_ctzll(unsigned long long v) noexcept {
if(!v) return 64; int n = 0;
if(!(unsigned)(v)) { n += 32; v >>= 32; }
if(!(unsigned short)(v)) { n += 16; v >>= 16; }
if(!(unsigned char)(v)) { n += 8; v >>= 8; }
if(!(v & 0xfull)) { n += 4; v >>= 4; }
if(!(v & 0x3ull)) { n += 2; v >>= 2; }
if(!(v & 0x1ull)) { n += 1; }
return n;
}
constexpr int kame_msvc_ctz(unsigned int v) noexcept {
return v ? kame_msvc_ctzll((unsigned long long)v) : 32;
}
constexpr int kame_msvc_clzll(unsigned long long v) noexcept {
if(!v) return 64; int n = 0;
if(!(v >> 32)) { n += 32; v <<= 32; }
if(!(v >> 48)) { n += 16; v <<= 16; }
if(!(v >> 56)) { n += 8; v <<= 8; }
if(!(v >> 60)) { n += 4; v <<= 4; }
if(!(v >> 62)) { n += 2; v <<= 2; }
if(!(v >> 63)) { n += 1; }
return n;
}
#define __builtin_ctzll(x) kame_msvc_ctzll(x)
#define __builtin_ctz(x) kame_msvc_ctz(x)
#define __builtin_clzll(x) kame_msvc_clzll(x)
// __builtin_thread_pointer: only expanded inside #if KAME_FAST_TSD (macOS only).
// Provided here for completeness; on Windows the code is dead.
#ifdef _WIN64
#define __builtin_thread_pointer() ((void *)__readgsqword(0x30)) // x64 TEB self-ptr
#else
#define __builtin_thread_pointer() ((void *)__readfsdword(0x18)) // x86 TEB self-ptr
#endif
static inline bool kame_msvc_mul_ovf(std::size_t a, std::size_t b, std::size_t *out) noexcept {
#ifdef _WIN64
// x64: _umul128 gives the high 64 bits; overflow if non-zero.
unsigned long long hi; *out = (std::size_t)_umul128(a, b, &hi); return hi != 0ull;
#else
// x86: size_t is 32-bit; promote to 64-bit, overflow if high 32 bits set.
unsigned long long r = (unsigned long long)a * (unsigned long long)b;
*out = (std::size_t)(r & 0xFFFFFFFFULL); return (r >> 32) != 0ULL;
#endif
}
#define __builtin_mul_overflow(a, b, outp) kame_msvc_mul_ovf((a), (b), (outp))
#endif
// Forced-inline / no-inline that survive MSVC. The generic
// `#define __attribute__(x)` above strips EVERY GNU attribute on MSVC —
// fine for pure codegen hints (cold/used/tls_model), but it would also
// silently drop the `always_inline` / `noinline` that pin the `deallocate`
// hot/cold split (the lean FS=true free MUST inline into operator delete /
// free; deallocate_cold / deallocate_fs_false_owner MUST stay out-of-line,
// else the cold bulk bloats the hot frame back to the 6-stp prologue this
// split removes). Map to MSVC's `__forceinline` / `__declspec(noinline)`
// so the split is honoured there too — not just on GCC/Clang/llvm-mingw.
// (`cold` has no MSVC analogue and is a layout-only hint, so it is dropped.)
#if defined(_MSC_VER) && !defined(__GNUC__)
#define KAME_ALWAYS_INLINE __forceinline
#define KAME_NOINLINE __declspec(noinline)
#define KAME_NOINLINE_COLD __declspec(noinline)
#else
#define KAME_ALWAYS_INLINE __attribute__((always_inline)) inline
#define KAME_NOINLINE __attribute__((noinline))
#define KAME_NOINLINE_COLD __attribute__((noinline, cold))
#endif
// Cache-line size, architecture-dependent. Kept as a fixed macro
// (std::hardware_destructive_interference_size is ABI-fragile across
// libc++/libstdc++ and warns under GCC). Duplicated from kamestm's
// transaction_detail.h to keep kamepoolalloc standalone-buildable.
// Documents the per-target line sizes that motivate the
// PoolAllocatorBase deallocate-fast-path hot block being held off
// chunk_header's cache line (Apple-aarch64 / PPC 128 B; Fujitsu 256 B).
// NOTE: the hot block itself is `alignas(64)`, not this value — the
// embedded object is only 64-aligned (chunk_base+ALLOC_CHUNK_HEADER), so
// a larger alignas would be UB; 64 still clears chunk_header's line for
// the 64/128 B targets because the object already starts at +64. See
// PoolAllocatorBase::m_owner_id.
#ifndef KAME_CACHE_LINE
#if defined(__APPLE__) && defined(__aarch64__)
#define KAME_CACHE_LINE 128
#elif defined(__powerpc64__) || defined(__POWERPC__)
#define KAME_CACHE_LINE 128
#elif defined(__aarch64__) && (defined(__FUJITSU) || defined(__CLANG_FUJITSU))
#define KAME_CACHE_LINE 256
#else
#define KAME_CACHE_LINE 64
#endif
#endif
// Portable atomic primitives for the custom pool allocator (formerly
// x86-only inline asm in atomic_prv_x86.h, then inline templates in
// allocator.cpp; hoisted here so header-inlined PoolAllocator member
// templates — `batch_clear_impl` etc. — can use them). GCC/Clang
// __sync builtins work on every arch the pool supports.
//! Bit count / population count for 32bit. Hoisted from allocator.cpp
//! so header-inlined FS=false bucket-freelist push can call it.
template <typename T>
inline typename std::enable_if<sizeof(T) == 4, unsigned int>::type count_bits(T x) {
x = x - ((x >> 1) & 0x55555555u);
x = (x & 0x33333333u) + ((x >> 2) & 0x33333333u);
x = (x + (x >> 4)) & 0x0f0f0f0fu;
x = x + (x >> 8);
x = x + (x >> 16);
return x & 0xffu;
}
//! Bit count / population count for 64bit.
template <typename T>
inline typename std::enable_if<sizeof(T) == 8, unsigned int>::type count_bits(T x) {
x = x - ((x >> 1) & 0x5555555555555555uLL);
x = (x & 0x3333333333333333uLL) + ((x >> 2) & 0x3333333333333333uLL);
x = (x + (x >> 4)) & 0x0f0f0f0f0f0f0f0fuLL;
x = x + (x >> 8);
x = x + (x >> 16);
x = x + (x >> 32);
return x & 0xffu;
}
//! \return one bit at the first zero from the LSB in \a x.
template <typename T>
inline T find_zero_forward(T x) {
return (( ~x) & (x + 1u));
}
#if defined(_MSC_VER) && !defined(__GNUC__)
// MSVC: the GCC __sync_* builtins don't exist. Map to _Interlocked*
// (full-barrier, same ordering as the legacy __sync_*), dispatched by
// width. Only 4- and 8-byte targets occur (ints, FUINT, pointers).
template <typename T>
inline typename std::enable_if<std::is_integral<T>::value || std::is_pointer<T>::value, bool>::type
atomicCompareAndSet(T oldv, T newv, T *target) noexcept {
if constexpr (sizeof(T) == 8) {
long long o = (long long)(std::intptr_t)oldv;
return _InterlockedCompareExchange64((long long volatile *)target,
(long long)(std::intptr_t)newv, o) == o;
} else {
long o = (long)(std::intptr_t)oldv;
return _InterlockedCompareExchange((long volatile *)target,
(long)(std::intptr_t)newv, o) == o;
}
}
template <typename T>
inline void atomicInc(T *target) noexcept {
if constexpr (sizeof(T) == 8) _InterlockedIncrement64((long long volatile *)target);
else _InterlockedIncrement((long volatile *)target);
}
template <typename T>
inline void atomicDec(T *target) noexcept {
if constexpr (sizeof(T) == 8) _InterlockedDecrement64((long long volatile *)target);
else _InterlockedDecrement((long volatile *)target);
}
template <typename T>
inline bool atomicDecAndTest(T *target) noexcept {
if constexpr (sizeof(T) == 8) return _InterlockedDecrement64((long long volatile *)target) == 0;
else return _InterlockedDecrement((long volatile *)target) == 0;
}
//! Atomic fetch-and-AND. Returns the OLD value (before AND).
template <typename T>
inline T atomicFetchAnd(T *target, T value) noexcept {
if constexpr (sizeof(T) == 8)
return (T)_InterlockedAnd64((long long volatile *)target, (long long)value);
else
return (T)(long)_InterlockedAnd((long volatile *)target, (long)value);
}
//! Atomic fetch-and-OR. Returns the OLD value (before OR).
template <typename T>
inline T atomicFetchOr(T *target, T value) noexcept {
if constexpr (sizeof(T) == 8)
return (T)_InterlockedOr64((long long volatile *)target, (long long)value);
else
return (T)(long)_InterlockedOr((long volatile *)target, (long)value);
}
#else
template <typename T>
inline typename std::enable_if<std::is_integral<T>::value || std::is_pointer<T>::value, bool>::type
atomicCompareAndSet(T oldv, T newv, T *target) noexcept {
return __sync_bool_compare_and_swap(target, oldv, newv);
}
template <typename T>
inline void atomicInc(T *target) noexcept {
__sync_fetch_and_add(target, 1);
}
template <typename T>
inline void atomicDec(T *target) noexcept {
__sync_fetch_and_sub(target, 1);
}
template <typename T>
inline bool atomicDecAndTest(T *target) noexcept {
return __sync_sub_and_fetch(target, 1) == 0;
}
//! Atomic fetch-and-AND. Returns the OLD value (before AND) so the
//! caller can compute the resulting bit pattern. Used by an earlier change
//! BIT_OWNED clear to detect "I brought m_flags_packed to 0" → I'm
//! the unique releaser.
template <typename T>
inline T atomicFetchAnd(T *target, T value) noexcept {
return __sync_fetch_and_and(target, value);
}
//! Atomic fetch-and-OR. Returns the OLD value (before OR).
template <typename T>
inline T atomicFetchOr(T *target, T value) noexcept {
return __sync_fetch_and_or(target, value);
}
#endif
#if defined(__GNUC__) || defined(__clang__)
#define ALLOC_TLS __thread //TLS for allocations, could be better for NUMA.
// Hot-path TLS variables: marked initial-exec to bypass libc's
// `__tls_get_addr` thunk (~15% of total runtime under the
// global-dynamic default on shared libraries). IE lowers each
// access to a single `mov %fs:offset(,%idx,8),%reg` op.
//
// Cost: each IE TLS variable claims space in the program's static
// TLS block at load time, so the library can ONLY be loaded at
// process start (LD_PRELOAD or normal -l link) — NOT `dlopen`'d
// after startup, since dlopen has limited surplus static-TLS
// budget. We restrict the IE marking to the small hot variables
// (g_thread_freelist_ptr 384 B + s_tls_owner_id 4 B + s_alloc_tls_off
// 1 B ≈ 400 B); the larger cold TLS (tls_cross_dealloc_batch 16 KiB)
// stays on global-dynamic. Total static-TLS demand of the IE
// variables fits in the default Linux surplus budget (~4 KiB).
//
// Windows carve-out: `tls_model("initial-exec")` targets the
// ELF/Mach-O `__tls_get_addr` thunk, which doesn't exist on the
// Windows ABI — TLS access there goes through the FS/GS segment
// pointer + `_tls_index` machinery (real `.tls` section) or
// `__emutls_get_address` (MinGW gcc emulated TLS) instead. MinGW
// gcc + lld either silently ignores the attribute or emits a broken
// section layout across the EXE/DLL boundary, with the symptom that
// IE-marked TLS reads return garbage in modules / dlopen'd DLLs
// (observed: kame.exe's allocator activates correctly under inline-
// compile, but `operator new` from a module crashes the moment it
// reads `g_thread_freelist_ptr`). Drop the attribute on Windows;
// the cost is one `_tls_index`-indirect read per hot-path TLS access
// (or one `__emutls_get_address` call on emutls builds) — measurable
// but tiny compared to the cost of a corrupted pool state.
#if defined(_WIN32) || defined(__WIN32__) || defined(WINDOWS)
#define ALLOC_TLS_IE __thread
#else
#define ALLOC_TLS_IE __thread __attribute__((tls_model("initial-exec")))
#endif
#else
#define ALLOC_TLS thread_local
#define ALLOC_TLS_IE thread_local
#endif
//! (§33/§34) "1-back skip": when the producer's chunk-search
//! (`allocate_chunk_path` Phase 2 DLL walk) looks for an allocatable
//! chunk, skip not only the pinned chunk but also the one that was
//! pinned immediately before ("1-back"). In a producer→consumer
//! hand-off the consumer's cross-thread frees concentrate on the chunk
//! the producer JUST finished, so that chunk's `m_flags` cacheline is
//! hot with the consumer's CAS-clears; re-pinning it to CAS-SET the
//! same word ping-pongs the cacheline producer↔consumer. Skipping it
//! for one pin cycle keeps the line in the consumer's cache.
//!
//! DEFAULT ON (since §34). The skip forces the producer to find a
//! DIFFERENT chunk (2-back+, or acquire a new one) instead of re-pinning
//! 1-back. Before §34 that re-acquire was an mmap + page-fault (or
//! bitmap-claim + madvise on release), so the skip cost more than the
//! contention it dodged — measured -10..0 % on a single-socket 4-core
//! box, hence the original default-OFF. §34's unified large-recycle
//! cache made chunk acquire/release a warm LRC pop/push (no syscall, no
//! refault), so the re-acquire is now cheap and the false-sharing
//! avoidance turns net-positive even on a single socket:
//!
//! bench_xthread_pool -w2 (M free/sec, §34 base, OFF → ON):
//! 8 B 28.6 → 29.9 (+5 %) 256 B 16.6 → 17.6 (+6 %)
//! 64 B 49.8 → 50.6 (+2 %) 1024 B 10.4 → 11.1 (+6 %)
//! 4096 B 11.0 → 13.0 (+18 %)
//! single-thread alloc_minimal_bench: parity (~232 M ops/s, the hot
//! freelist loop never re-enters the chunk-search path).
//!
//! Net win at every size, memory-neutral, and the rationale is only
//! stronger on multi-socket NUMA (cross-socket `m_flags` bounce is far
//! costlier than the shared-L3 case measured here). Build with
//! `-DKAME_POOL_ONEBACK_SKIP=0` to turn it back off.
//!
//! Chunk release is unaffected either way — the empty-neighbour
//! `owner_release` path is separate from this allocate-search walk and
//! still visits every chunk. Memory footprint (`chunks_live`) is
//! identical with and without the skip (verified time-bounded in both).
#ifndef KAME_POOL_ONEBACK_SKIP
#define KAME_POOL_ONEBACK_SKIP 1
#endif
//! (Orphan-chain) The orphan reclaim mechanism: a TLA+-verified
//! atomic_shared_ptr-refcounted intrusive orphan chain (push at owner-exit,
//! scrub-reclaim drained orphans, adopt survivors with a chunk self-ref
//! owner-ref) — see design/ORPHAN_CHAIN_INTEGRATION.md and
//! tests/tlaplus/OrphanChain_*.tla. (§S7) It is the sole orphan mechanism: the
//! §36 orphan Treiber stack (s_orphan_head / orphan_push / orphan_pop) is
//! retired and the former KAME_ORPHAN_CHAIN opt-in flag is removed — the chain
//! code below is UNCONDITIONAL. Verified race-free on Linux (TSan/ASan) with
//! churn plateau; the regression guard is the TLA model
//! (tests/tlaplus/run_orphan_chain.sh) — the owner-free vs scrub-pin race it
//! caught (Inv_NoBadOwnerFree) is not reproducible by runtime stress.
#include "atomic_smart_ptr.h"
//! (Orphan-chain) The chunk's embedded PoolAllocator IS the intrusive
//! node of the lock-free orphan chain — it uses the intrusive
//! atomic_shared_ptr path (Ref = T, custom disposer) WITHOUT a sizeof
//! completeness probe — the type is self-referential (holds an
//! atomic_shared_ptr<PoolAllocator>) hence incomplete at first use. Mirrors
//! tests/atomic_intrusive_chain_test.cpp's Node. (Forward decl carries NO
//! default args — they live on the primary template below.)
template <unsigned int ALIGN, bool FS, bool DUMMY> class PoolAllocator;
template <unsigned int ALIGN, bool FS, bool DUMMY>
struct force_intrusive_ref<PoolAllocator<ALIGN, FS, DUMMY> > : std::true_type {};
//! Allocation unit (1 chunk = N × this). every mmap region is
//! a uniform 32 MiB block carved into 128 fixed-size 256 KiB "units".
//! A chunk = 1, 2, or 4 contiguous units depending on the per-template
//! `CHUNK_UNITS` (= 1 for ALIGN < 256, = 2 for ALIGN < 1024, = 4 for
//! ALIGN ≥ 1024 = 1024). The unit size matches the previous an earlier change
//! minimum so cross-thread chunk residency under lazy commit stays
//! tight; the buddy approach replaces the previous 2× growth ladder.
//!
//! O(1) chunk_base lookup from any slot uses `s_back_offset[]` (one
//! byte per unit, per region) — see `PoolAllocatorBase::s_back_offset`
//! below. `back_off = u - base_u`; a reader does `base_u = u -
//! back_off` and then `chunk_base = region + base_u * 256K`.
#define ALLOC_MIN_CHUNK_SIZE (1024 * 256) //256 KiB unit
//! log2(ALLOC_MIN_CHUNK_SIZE) — used for fast unit-index extraction
//! `unit_idx = pdiff >> ALLOC_MIN_CHUNK_SHIFT`. Compile-time constant.
#define ALLOC_MIN_CHUNK_SHIFT 18 //log2(256 KiB)
//! Max chunk = 16 units = 4 MiB. The compile-time bucket templates only
//! ever use CHUNK_UNITS ∈ {1,2,4} (≤ 1 MiB — see the CHUNK_UNITS constexpr
//! below); the larger 8/16-unit chunks are claimed only by the *runtime*
//! N-unit path used for dedicated single-slot large allocations
//! (allocate_dedicated_chunk). A 16-unit chunk still fits within a 32 MiB
//! region's 128-unit bitmap and within one BitmapWord's claim CAS (≤ 64
//! units/word on 64-bit, ≤ 32 on 32-bit), and back_off (uint8) covers it.
#define ALLOC_MAX_CHUNK_UNITS 16
#define ALLOC_MAX_CHUNK_SIZE (ALLOC_MIN_CHUNK_SIZE * ALLOC_MAX_CHUNK_UNITS)
// OS page size — relevant for the `madvise()` granularity on chunk
// release. All chunk sizes are multiples of ALLOC_MIN_CHUNK_SIZE
// (= 256 KiB) which auto-satisfies every supported arch's page size.
#if defined(__APPLE__) && defined(__aarch64__)
#define ALLOC_PAGE_SIZE 16384 // 16 KiB
#elif defined(__powerpc64__) || defined(__POWERPC__)
#define ALLOC_PAGE_SIZE 65536 // 64 KiB
#else
#define ALLOC_PAGE_SIZE 4096 // 4 KiB
#endif
//! regions are uniform 32 MiB — no ladder, no growth. The
//! an earlier change growth-cap macro `GROW_CHUNK_SIZE` is removed; chunk size
//! is now a per-template constant (`PoolAllocator<...>::CHUNK_SIZE`).
//! `NUM_ALLOCATORS_IN_SPACE == 128` matches the bit count of the per-
//! region claim bitmap (BitmapWord bits × BITMAP_WORDS_PER_REGION =
//! UNITS_PER_BITMAP_WORD × BITMAP_WORDS_PER_REGION = 128 units). Every
//! region is 32 MiB = 128 × 256 KiB regardless of host word size.
//!
//! `ALLOC_MAX_REGIONS` is the VA cap — each region is mmap'd
//! `PROT_READ | PROT_WRITE` upfront (switches the release path
//! from `mprotect(PROT_NONE)` to `madvise(MADV_FREE/DONTNEED)` so
//! reclaim is RSS-cheap without protection toggling). Total
//! reservation = 32 MiB × N entries.
//!
//! host | N | total VA cap
//! -------------+------+--------------------
//! 64-bit | 3200 | 100 GiB
//! 32-bit | 96 | 3 GiB (= full Linux 3G/1G user VA;
//! | | lazy — only mmap'd-on-demand regions
//! | | count against the actual user VA budget,
//! | | so the cap is a ceiling not a reservation)
//! Windows | 3200 | (same as 64-bit; pool path opt-in via dylib)
//!
//! Sizing rationale: a general-purpose allocator must accommodate
//! workloads that stretch into the multi-GiB user-heap range without
//! catastrophically aborting (the earlier change's 3 GiB cap was first to expose
//! this — `alloc_only` at 8192 B for 500K iters needs ~4.5 GiB pool
//! and would hit `# of chunks exceeds the limit`). 64-bit hosts have
//! 128 TiB of user VA on macOS / Linux / Windows so a 100 GiB cap is
//! 0.08 % of available VA — trivial in reservation cost. 32-bit
//! matches the practical user-VA ceiling on Linux (3G/1G split).
//!
//! Lazy mmap: `allocate_chunk` only mmaps a region when the previous
//! ones are full. A workload using 5 regions walks 5 entries in
//! `deallocate`'s region-loop and pays RSS only for those 5 × 32 MiB.
//! Bumping the cap doesn't accelerate steady-state allocation.
//!
//! Cache impact for `s_back_offset[]` (only the COLD chunk-claim /
//! deallocate-region-miss paths touch unused regions' table entries):
//! * 64-bit: 3200 × 128 × 8 = 3.1 MiB BSS. Unused entries stay
//! zero-filled in BSS pages, never paged in. Hot subset =
//! populated_regions × 1 KiB; for typical KAME workloads (1-5
//! populated regions) the working set fits in a few KiB.
//! * 32-bit: 96 × 128 × 8 = 96 KiB BSS.
//!
//! Cache impact for `s_claim_bitmap[]`:
//! * 64-bit: 3200 × 2 × 8 = 50 KiB. L2 only on full scan
//! (allocate_chunk cold path); steady-state ops touch one word.
//! * 32-bit: 96 × 4 × 4 = 1.5 KiB. L1d.
//!
//! `s_mmapped_spaces[]` (the region-base array walked by every
//! deallocate / lookup_chunk):
//! * 64-bit: 3200 × 8 = 25 KiB. First N entries (= populated
//! regions) hot in L1d; rest unused.
//! * 32-bit: 96 × 8 = 768 B. L1d.
#define ALLOC_MIN_MMAP_SIZE (1024 * 1024 * 32) //32 MiB = 256 KiB × 128
//! log2(ALLOC_MIN_MMAP_SIZE). Used as the "region index shift" for the
//! 2-level radix lookup (§13): any pointer's owning region is identified
//! by its upper `64 - ALLOC_MIN_MMAP_SHIFT` bits.
#define ALLOC_MIN_MMAP_SHIFT 25
static_assert((1ull << ALLOC_MIN_MMAP_SHIFT) == (unsigned)ALLOC_MIN_MMAP_SIZE,
"ALLOC_MIN_MMAP_SHIFT must equal log2(ALLOC_MIN_MMAP_SIZE)");
//! (§13.3) Region-count ceiling. No longer an array bound — the
//! per-region-index globals (`s_mmapped_spaces[]`, `s_region_has_free[]`)
//! are retired; regions live on a push-only list and their metadata
//! inside themselves. This constant is now ONLY the default (uncapped)
//! value of the runtime `s_max_regions_cap`, set to the radix tree's full
//! VA coverage so the pool is effectively VA-limited (not array-capped).
#if defined __LP64__ || defined __LLP64__ || defined(_WIN64) || defined(__MINGW64__)
//! 1 << RADIX_REGION_BITS = 8388608 regions × 32 MiB = 256 TiB — the
//! 48-bit user-VA range the radix covers (static_assert'd below).
#define ALLOC_MAX_REGIONS (1 << 23)
#else
//! 32-bit host: 96 regions = 3 GiB, matching the Linux 3G/1G user-VA
//! ceiling (mmap fails past this anyway). Kept small so the
//! `regions × 32 MiB` byte math in kame_pool_get_max_bytes can't
//! overflow a 32-bit size_t.
#define ALLOC_MAX_REGIONS 96
#endif
//! 2-level radix tree for O(1) pointer-to-region lookup (§13,
//! tests/CHUNK_CLAIM_TLA_NOTES.md). Replaces the O(N) linear walk of
//! `s_mmapped_spaces[]` that every `lookup_chunk(p)` / `deallocate(p)` /
//! `size_of(p)` used to do.
//!
//! Encoding: region index = `p >> ALLOC_MIN_MMAP_SHIFT` (upper bits of
//! the pointer); split into RADIX_L1_BITS + RADIX_L2_BITS. Picked 12+11
//! = 23-bit region index, which covers 23 + 25 = 48-bit user VA. This
//! spans every known 64-bit user-VA layout the allocator's own NULL-hint
//! mmaps can land in: x86-64 macOS/Linux/Windows (47-bit), Apple Silicon
//! arm64 (47-bit; PAC reserves the high bits), AND 48-bit aarch64 Linux
//! (where mmap(NULL) may legitimately return [2^47, 2^48) with no hint —
//! the case a 22-bit / 47-bit radix would silently fail to register,
//! later mis-routing that region's frees to libc). Pointers above 2^48
//! (rare: 52-bit ARM64 LVA or 5-level/57-bit paging — both opt-in only
//! via an explicit mmap hint, never requested here) fall back via the
//! defensive bound check in `radix_lookup` (returns -1, treated as
//! foreign — same outcome as today's "not in `s_mmapped_spaces[]`").
//!
//! Storage:
//! L1: fixed `[1<<12 = 4096]` `atomic<RadixL2Node*>` in BSS (32 KiB).
//! Top level is hot-in-L1d on the lookup path.
//! L2: each populated node is `[1<<11 = 2048]` `atomic<uint32_t>`
//! slots (8 KiB = 2 pages). Slot value 0 = unpopulated; non-zero
//! = present (a pure presence token since §13.3 — the region base
//! is derived from the pointer, not a stored index). L2 nodes
//! are allocated
//! LAZILY via mmap from `radix_alloc_l2()` (NOT through the
//! interposed libc malloc — that would recurse). Each L2 covers
//! `2^11 × 32 MiB = 64 GiB` of VA, so a 1-TiB workload populates
//! ~16 L2 nodes (128 KiB committed total).
//!
//! Concurrency: L1 slot install is one CAS (loser munmap's its loser
//! leaf). L2 slot store is `release`-paired with the reader's
//! `acquire` load on the L1 entry that brought it into view (the L1
//! load synchronizes-with the L2 init store). No reclamation needed
//! (slots are written once and live for the process lifetime — regions
//! are never unmapped in the current design; §13.2 may revisit).
//!
//! Pointer decomposition (64-bit; ALLOC_MIN_MMAP_SHIFT = 25 ⇒ 32 MiB region):
//!
//! 63 48 47 36 35 25 24 0
//! ┌───────────┬────────────┬────────────┬─────────────────┐
//! │ must be 0 │ L1 index │ L2 index │ in-region off │
//! │ (16 b) │ (12 b) │ (11 b) │ (25 b) │
//! └───────────┴────────────┴────────────┴─────────────────┘
//! bound \____ region_idx = p >> 25 (23 b) ____/
//! check (the 32-MiB region's identity; base = region_idx << 25)
//!
//! region_idx = p >> 25; l1 = region_idx >> 11; l2 = region_idx & 0x7FF
//!
//! 2-level walk (radix_lookup):
//!
//! s_radix_l1[4096] BSS, 32 KiB, atomic<RadixL2Node*>
//! │ [l1] null leaf ⇒ ABSENT
//! ▼
//! RadixL2Node lazily mmap'd on first insert, 8 KiB
//! entries[2048] atomic<uint32_t>, one slot per 32-MiB region
//! │ [l2] 0 slot ⇒ ABSENT
//! ▼
//! RadixKind: 0 ABSENT foreign pointer → libc free/size
//! 1 POOL base → RegionMeta (see region-header diagram)
//! 2 LARGE base → LargeAllocMeta (see region-header diagram)
//!
//! Coverage: each L2 spans 2^11 × 32 MiB = 64 GiB; L1 spans 2^23 × 32 MiB
//! = 256 TiB (the 48-bit user VA). bits [63..48] set ⇒ ABSENT (foreign).
//!
constexpr int RADIX_L1_BITS = 12;
constexpr int RADIX_L2_BITS = 11;
constexpr unsigned RADIX_L1_SIZE = 1u << RADIX_L1_BITS;
constexpr unsigned RADIX_L2_SIZE = 1u << RADIX_L2_BITS;
constexpr int RADIX_REGION_BITS = RADIX_L1_BITS + RADIX_L2_BITS; // 23
//! (§35) Exclusive upper bound on a radix-registrable region base. A base
//! at or above this has region index ≥ 1<<RADIX_REGION_BITS, which the radix
//! cannot index (`radix_insert`'s `l1 >= RADIX_L1_SIZE` skips it, and
//! `radix_lookup`'s bound check returns ABSENT). = 2^48 with the 23-bit
//! region index. `mmap_new_region` / `large_va_raw_map` reject any base ≥
//! this and fall back to libc, so an out-of-window kernel placement (only
//! possible via an explicit >window mmap hint — never requested here)
//! degrades gracefully instead of mis-routing the region's later frees.
//! For multi-region huge spans only the HEAD base must clear this bound
//! (tail slots are never standalone lookup targets).
//!
//! Width-aware: the radix spans `RADIX_REGION_BITS + ALLOC_MIN_MMAP_SHIFT`
//! (= 48) VA bits. On a platform whose pointers are NARROWER than that — ILP32
//! (`sizeof(uintptr_t)*8 == 32 < 48`) — the radix already covers the ENTIRE
//! address space (every 32-bit base has region index `p>>25` < 2^7, well within
//! the 2^23 table — sparse but correct), so the bound is the whole space and
//! nothing is ever out-of-window. Computing `1 << 48` directly would be UB on
//! a 32-bit `uintptr_t` (shift count ≥ width), so clamp to `~0` there.
constexpr unsigned RADIX_VA_BITS = RADIX_REGION_BITS + ALLOC_MIN_MMAP_SHIFT; // 48
#if defined(_MSC_VER) && !defined(__GNUC__)
#pragma warning(push)
#pragma warning(disable: 4293) // MSVC warns on the dead ILP32 shift arm (1<<48); clamped to ~0 below
#endif
constexpr uintptr_t RADIX_VA_LIMIT =
(RADIX_VA_BITS >= sizeof(uintptr_t) * 8u)
? ~(uintptr_t)0 // pointers ⊆ radix span (ILP32)
: ((uintptr_t)1 << RADIX_VA_BITS); // 2^48 on LP64/LLP64
#if defined(_MSC_VER) && !defined(__GNUC__)
#pragma warning(pop)
#endif
#if defined __LP64__ || defined __LLP64__ || defined(_WIN64) || defined(__MINGW64__)
// 64-bit: the region-count ceiling equals the radix's full VA coverage.
static_assert(ALLOC_MAX_REGIONS == (1 << RADIX_REGION_BITS),
"ALLOC_MAX_REGIONS must equal the radix VA coverage "
"(1 << RADIX_REGION_BITS)");
#endif
//! (§19) Radix slot value semantics — what kind of allocation lives at
//! the 32-MiB-aligned base of a present slot. Lookup returns this
//! directly; 0 means "absent, foreign pointer".
enum RadixKind : uint32_t {
KAME_RADIX_ABSENT = 0u, // no allocation at this base
KAME_RADIX_POOL = 1u, // a PoolAllocatorBase::RegionMeta lives here
KAME_RADIX_LARGE = 2u, // a LargeAllocMeta lives here (§19 large-alloc)
};
struct RadixL2Node {
// Member name is `entries`, NOT `slots` — Qt defines `slots` as an
// empty preprocessor token in <QtCore>, so `T slots[N];` at class
// scope becomes `T [N];`, which Apple Clang then mis-parses as a
// structured binding (illegal at class scope). Renamed to keep the
// header includable from Qt-built TUs (`kame/main.cpp`, etc.)
// without `#undef slots` gymnastics.
std::atomic<uint32_t> entries[RADIX_L2_SIZE]; // (§19) RadixKind values
};
static_assert(sizeof(RadixL2Node) == RADIX_L2_SIZE * 4u,
"RadixL2Node must be exactly RADIX_L2_SIZE * 4 bytes "
"(atomic<uint32_t>) for a clean 8 KiB layout");
//! Reserved bytes at the head of every chunk. Layout:
//! [ 0 .. 7]: chunk-wide SIZE info — `uint64_t`:
//! FS=true (fixed-size chunk): low 32 bits = slot
//! size in bytes (= ALIGN; same for every
//! slot in the chunk). Non-zero ⇒ "jump
//! straight to bucket-driven dispatch
//! without a per-slot header read".
//! FS=false (variable-size): 0. Distinct from FS=true
//! and from chunk-released (palloc==0); the
//! dealloc path reads the per-slot
//! `{bucket, SIZE}` header at `p - 8`
//! instead.
//! High 32 bits: ALIGN (always — for non-templated
//! dispatchers; see `chunk_header_size_info()`).
//! [ 8 .. 15]: `PoolAllocatorBase *` palloc (chunk owner).
//! [16 .. 23]: `DeallocateFn` — non-virtual static trampoline
//! (per-template) that dispatches the dealloc body.
//! [24 .. 31]: `SizeOfFn` — slot-size lookup trampoline.
//! FS=false: reads SIZE from the per-slot
//! `{bucket, SIZE}` header at `p - 8`.
//! [32 .. 63]: pad (the low 8 B, [32..39], double as the dedicated-chunk
//! byte size when size_info low-32 == the DEDICATED sentinel).
//!
//! FS=false slot 0 (bit 0 of m_flags[0]) is the only slot with no predecessor
//! whose last 8 B can host its `{uint32 local_id, uint32 SIZE}` borrow prefix.
//! `allocate_pooled` reaches its prefix via the SAME uniform `slot_start - 8`
//! math used for every slot — with no special-case branch. Since the §15
//! K_MAX forward-shift, slot 0 (`mempool()`) sits at `chunk_base + K_MAX`, so
//! `slot_start - 8` = `chunk_base + K_MAX - 8` (= +4088): the LAST 8 bytes of
//! the metadata region (reserved tail of the pad after m_flags[]), NOT the
//! [56..63] chunk-header pad (that was the pre-§15 home, when mempool was at
//! +ALLOC_CHUNK_HEADER). For FS=true and FS=false-m_sizes chunks this tail is
//! just unused pad (neither has a p-8 per-slot header).
//! Slot region (`mempool()`) starts at `chunk_base + ALLOC_CHUNK_K_MAX`
//! (= +4096; §15 forward-shift). The former `m_mempool` field is retired —
//! the start is derived from `this` (`(char*)this + (K_MAX - HEADER)`).
//!
//! Visual chunk layout (regular vs dedicated, byte offsets from `chunk_base`,
//! which is 256 KiB-aligned). Metadata occupies [0, K_MAX); the slot region
//! begins at the 256 KiB unit boundary `chunk_base + K_MAX` — the metadata
//! physically lives in the PREVIOUS unit's last page (see §15 below).
//!
//! The physical layout below is SHARED by four slot/metadata schemes; they
//! differ only in three fields — size_info[0..7], the hot-block `m_sizes`
//! pointer, and where each slot's `{local-id, SIZE}` comes from:
//!
//! scheme size_info[0..7] m_sizes per-slot local-id / SIZE source
//! ---------------- --------------- -------- ---------------------------------
//! FS=true ALIGN (≠ 0) null implicit: local-id 0, SIZE = ALIGN
//! (one size per chunk; no per-slot
//! data, no p-8 read)
//! FS=false borrow 0 null `{uint32 local_id, uint32 SIZE}`
//! (ALIGN < 1024) at `p - 8`; slot 0's prefix lives
//! in hdr[56..63], every other slot
//! borrows its predecessor's last 8 B
//! FS=false m_sizes 0 non-null `m_sizes[bit] = (N<<8)|local_id`,
//! ("full-usable", `bit = (p-mempool) >> m_align_shift`;
//! ALIGN ≥ 1024) slots are FULL N*ALIGN bytes — no
//! p-8 borrow theft (page-aligned
//! requests don't round up a class)
//! DEDICATED 0xFFFFFFFF (n/a) whole chunk = one slot; byte size
//! at hdr[32..39]; m_owner_id stamped
//! 0 → dealloc takes the bit-7 cold path
//!
//! (size_info high-32 = ALIGN for all three bucket schemes; n/a for DEDICATED.)
//!
//! REGULAR (FS=true / FS=false bucket chunk; FS=false has the two
//! sub-modes — borrow vs m_sizes — tabulated above)
//! =======================================================
//! +0 ┌────────────────────────────────────────────┐
//! │ chunk_header (ALLOC_CHUNK_HEADER = 64 B): │
//! │ [0..7] size_info [8..15] palloc │ <- chunk_header
//! │ [16..23] DeallocateFn [24..31] SizeOfFn │ (NOT touched on
//! │ [32..39] dedicated_size / pad │ the dealloc
//! │ [40..63] pad │ fast path)
//! +64 ├────────────────────────────────────────────┤
//! │ PoolAllocator embed object (placement-new │
//! │ at +64): vptr + cold fields (m_chunk_size │
//! │ …); alignas(64) pads so the hot block │
//! │ starts at +128. │
//! +128 ├────────────────────────────────────────────┤
//! │ hot block (cache-line-isolated "1b"): │ <- dealloc
//! │ m_owner_id, m_fs_flag, m_align_shift, │ fast-path line
//! │ m_base_bucket, m_sizes, │
//! │ m_freelist_head[KAME_LOCAL_BUCKETS] │
//! ├────────────────────────────────────────────┤
//! │ m_flags[] claim bitmap + padding │
//! │ (all metadata fits within [0, K_MAX); │
//! │ last 8 B [K_MAX-8 .. K_MAX) = FS=false- │
//! │ borrow slot-0 {local_id,SIZE} prefix) │
//! +K_MAX ├──────────── 256 KiB unit boundary ─────────┤
//! (4096) │ slot region = mempool() = chunk_base+K_MAX │ <- user pointers
//! │ FS=true : ALIGN-stride slots │
//! │ FS=false borrow: {local_id,SIZE} at p-8 │
//! │ FS=false m_sizes: full slots; id/SIZE in │
//! │ m_sizes[] (see table) │
//! +chunk_size └────────────────────────────────────────────┘
//!
//! DEDICATED (single-slot large allocation, bit-7 set in back_off)
//! =======================================================
//! +0 ┌────────────────────────────────────────────┐
//! │ chunk_header: size_info low-32 = DEDICATED │ <- line 0
//! │ sentinel (0xFFFFFFFF), palloc, │
//! │ [32..39] dedicated total byte size │
//! +64 ├────────────────────────────────────────────┤
//! │ K_MAX gap (metadata region; NOT user- │
//! │ writable). At +128 the would-be hot-block │
//! │ m_owner_id is stamped to 0 by │
//! │ allocate_dedicated_chunk, so the dealloc │
//! │ fast-path owner-id compare never matches → │
//! │ it naturally falls to the bit-7 cold path. │
//! +K_MAX ├──────────── 256 KiB unit boundary ─────────┤
//! (4096) │ user data: one contiguous slot, up to │ <- user pointer
//! │ chunk_size - K_MAX bytes │ (= chunk_base+K_MAX)
//! +chunk_size └────────────────────────────────────────────┘
#define ALLOC_CHUNK_HEADER 64
//! (§15) Forward-shift reservation: every chunk's first byte sits
//! K_MAX bytes BEFORE its first claimed unit's boundary. Slot region
//! therefore starts at the unit boundary (256 KiB-aligned), giving
//! deterministic page / huge-page alignment for slot data.
//!
//! chunk_base = unit_boundary[base_unit] - ALLOC_CHUNK_K_MAX
//! slot region = [chunk_base + K_MAX, chunk_base + chunk_size)
//! = [unit_boundary[base_unit],
//! unit_boundary[base_unit + CHUNK_UNITS] - K_MAX)
//!
//! The K_MAX bytes of metadata (chunk_header + PoolAllocator object +
//! m_flags + padding) live in the PREVIOUS unit's last page. Adjacent
//! chunks tile end-to-end: chunk N's last K_MAX bytes are reserved for
//! chunk N+1's metadata (if the next position is later claimed). This
//! means EVERY chunk's effective slot region is `chunk_size - K_MAX`
//! bytes; one chunk's tail K_MAX is always next-chunk's metadata slot.
//!
//! The first chunk in a region (base_unit = 1) has its metadata in
//! unit 0 (the RegionMeta unit) — unit 0's RegionMeta lives at the
//! start (~150 B), and only the last K_MAX bytes are reserved for
//! chunk 1's metadata, so no collision.
//!
//! K_MAX is sized so that the largest template (smallest ALIGN, biggest
//! count) fits its PoolAllocator object + m_flags array within
//! `K_MAX - ALLOC_CHUNK_HEADER` bytes. Verified at compile time in the
//! per-template `create()`:
//! - ALIGN=16, CHUNK_UNITS=1: PoolAllocator (~200 B) + m_flags (~2 KiB)
//! + padding < 4 KiB ✓
//! - ALIGN=256, CHUNK_UNITS=2: PoolAllocator + m_flags (~256 B) ≪ 4 KiB
//! - Dedicated chunks: chunk_header only (~64 B) ≪ 4 KiB
//!
//! 4 KiB = one OS page on every supported target. Convenient because
//! the entire metadata block sits in ONE page (the previous unit's
//! last page), so dealloc's hot-block read touches only that one
//! extra page beyond the slot region's pages.
#define ALLOC_CHUNK_K_MAX 4096
#define ALLOC_CHUNK_HEADER_SIZE_INFO_OFFSET 0 // [ 0.. 7]: chunk SIZE info
#define ALLOC_CHUNK_HEADER_PALLOC_OFFSET 8 // [ 8..15]: palloc
#define ALLOC_CHUNK_HEADER_FN_OFFSET 16 // [16..23]: DeallocateFn
#define ALLOC_CHUNK_HEADER_SIZEOF_FN_OFFSET 24 // [24..31]: SizeOfFn
// [32..39]: dedicated-chunk total byte size (only when SIZE_INFO low-32 ==
// ALLOC_CHUNK_DEDICATED_SIZEINFO). Reuses the field freed by the retired
// recycle epoch. A "dedicated" chunk is a single large allocation that
// occupies a whole N-unit chunk (no sub-slot bitmap); deallocate() detects
// the sentinel BEFORE bucket_for_size and releases the whole chunk.
#define ALLOC_CHUNK_HEADER_DEDICATED_SIZE_OFFSET 32 // [32..39]: dedicated chunk bytes
//! SIZE_INFO low-32 sentinel marking a dedicated single-slot large chunk.
//! Distinct from any real ALIGN (≤ 1024) and from 0 (the FS=false marker).
#define ALLOC_CHUNK_DEDICATED_SIZEINFO 0xFFFFFFFFu
// [32..55] free (was recycle epoch, retired — DLL/lookup safety comes
// from BIT_OWNED gating + live-slot invariant, not an epoch)
// [56..63] = slot-0 header (= ALLOC_CHUNK_HEADER - 8). No constant
// needed — `allocate_pooled` reaches it via the uniform
// `slot_start - 8` math (slot 0's slot_start == m_mempool ==
// chunk_base + ALLOC_CHUNK_HEADER).
static_assert(ALLOC_CHUNK_HEADER >= ALLOC_CHUNK_HEADER_SIZEOF_FN_OFFSET + 8 + 8,
"chunk header must have >= 8 B of pad between SizeOfFn "
"and the slot-0 reservation at chunk_header[-8..-1].");
#define ALLOC_ALIGNMENT 16 //bytes, not 8 but 16 for compatibility
#define ALLOC_MAX_CHUNKS_OF_TYPE \
(ALLOC_MIN_MMAP_SIZE / ALLOC_MIN_CHUNK_SIZE * ALLOC_MAX_REGIONS)
//! Max distinct sizes a single chunk hands out — depth of its (ALIGN,FS)
//! template's size set; sizes the compact per-chunk freelist
//! `PoolAllocatorBase::m_freelist_head[]` (follow-up "(1b)" §12.3).
//! FS=true chunks serve exactly 1 size (local id 0); the widest FS=false
//! tier is ALIGN=256, which serves buckets {16, 32..39} = 9 sizes
//! (id 0..8). Defined here (before PoolAllocatorBase) since the class
//! needs it for the array bound; `kBucketLocalId[]` (below) and a
//! BucketTraits-derived static_assert (allocator.cpp) verify no
//! (ALIGN,FS) tier exceeds this and that ids are collision-free.
constexpr int KAME_LOCAL_BUCKETS = 9;
class PoolAllocatorBase;
//! Cross-dealloc batch entry — paired chunk + slot pointers. Defined
//! here so `PoolAllocatorBase::batch_return_to_bitmap` and
//! `CrossDeallocBatch::buf[]` (in allocator.cpp) share the exact same
//! layout — no per-chunk slot-pointer copy on flush, no SoA/AoS
//! translation; `batch_return_to_bitmap` reads `entries[k].slot`
//! directly from the caller's buffer.
//!
//! `CrossDeallocBatch` keeps a sentinel `{nullptr, nullptr}` entry at
//! the position one past the live count, so the chunk-side walker
//! only needs `while(entries[k].chunk == this)` — no `k < n_max` test
//! in the inner loop. Trailing sentinel is invariant by flush
//! contract (any non-trivial chunk pointer `this` differs from
//! nullptr, so the walk always terminates at the boundary).
struct CrossDeallocEntry {
PoolAllocatorBase *chunk;
void *slot;
};
//! (§hot-tls) Unified per-thread hot TLS page — forward declaration.
//! Defined fully after AllocSlot/ALLOC_NUM_BUCKETS below.
//! All hot alloc/dealloc state in one struct so a single fast-TSD read
//! (macOS: mrs TPIDRRO_EL0 + one load) or one initial-exec TLS access
//! (Linux: mov %fs:offset) covers everything.
struct KameTlsPage;
//! Sentinel for an empty radix cache slot — guaranteed not to match any
//! real region base (real bases are 32 MiB-aligned user-space addresses;
//! all-ones is the kernel-space top address with low bits set).
static constexpr uintptr_t RADIX_CACHE_EMPTY = ~(uintptr_t)0;
//! Forward declaration of the hot TLS page accessor. Full definition
//! (and KameTlsPage struct) follows after AllocSlot/ALLOC_NUM_BUCKETS.
//! Used by PoolAllocatorBase::radix_lookup and ::deallocate inlines.
KameTlsPage *kame_page() noexcept;
class PoolAllocatorBase {
public:
//! Signature of the per-chunk dealloc trampoline stored in the
//! chunk header at offset `ALLOC_CHUNK_HEADER_FN_OFFSET` (= 8).
//! Set by `allocate_chunk` to `&PoolAllocator<ALIGN,FS,DUMMY>::
//! deallocate_pooled_static`, a non-virtual static wrapper that
//! casts `base` to the bound derived type and calls its inline
//! `deallocate_pooled_impl`. Replaces vtable dispatch on the
//! `deallocate_<>` hot path: 1 load (function pointer, same cache
//! line as `palloc`) + 1 indirect branch, vs. the previous
//! 2 loads (vtable + slot) + 1 indirect branch. Saving on macOS
//! arm64 with cache-hot vtable: ~1-2 cycles per dealloc; on
//! NUMA / cache-cold vtable: more.
using DeallocateFn = bool (*)(PoolAllocatorBase *base, char *slot);
//! Signature of the per-chunk slot-size trampoline stored at chunk-
//! header offset `ALLOC_CHUNK_HEADER_SIZEOF_FN_OFFSET` (= 16).
//! Used by `pool_slot_size(p)` / `realloc()` to recover the size of
//! an allocated slot without a vtable call. Per-(ALIGN,FS,DUMMY)
//! instantiation:
//! - FS=true → return ALIGN (compile-time constant)
//! - FS=false → decode N from `m_sizes[idx]>>sidx`, return N*ALIGN
using SizeOfFn = std::size_t (*)(PoolAllocatorBase *base, char *slot);
virtual ~PoolAllocatorBase() = default;
//! regions are uniform 32 MiB, so `deallocate_<>` no longer
//! needs the per-level compile-time CHUNK_SIZE template parameter.
//! Collapsed to a single non-template function with a runtime
//! region-walk loop — eliminates the 24/96-level template recursion
//! that previously generated one inlined copy of the body per
//! ladder level (icache bloat scaling with ALLOC_MAX_REGIONS).
//! Free `p` (pool slot or, for a foreign pointer, via libsystem).
//! VOID + self-contained: the foreign fallback lives INSIDE
//! `deallocate_cold`, so every caller is a pure tail-call and the lean
//! FS=true hot path needs NO stack frame (no prologue spill, no `bl`).
//! `always_inline` (see the def) expands it into free / operator
//! delete / kame_pool_free.
static inline void deallocate(void *p) noexcept;
//! Cold off-ramp for `deallocate` — VOID + self-contained: a foreign /
//! released pointer is libsystem-freed in place (no caller-side
//! fallback, no wrapper hop). Handles every case the lean hot path
//! does NOT inline: region-cache miss (foreign / large / first-touch),
//! and owner-mismatch (cross-thread / released / dedicated /
//! post-teardown). noinline+cold so its calls (deallocate_large_va,
//! the per-template DeallocateFn, deallocate_chunk, large_recycle_push,
//! libsystem_free_for_pool) never spill into the hot path.
static void deallocate_cold(void *p) noexcept;
//! FS=false owner-free helper for `deallocate`. Split out (noinline)
//! so the FS=true 64 B hot path in `deallocate` stays lean/inlinable.
//! `chunk_base` is pre-resolved by the caller; only this-thread-owned
//! FS=false chunks reach here. Void: a garbage local-id tail-calls
//! `deallocate_cold`.
static void deallocate_fs_false_owner(char *chunk_base, void *p) noexcept;
//! Look up the slot size (bytes) for a pointer. Returns 0 if `p`
//! is not a pool slot (foreign / libsystem-malloc'd / null). Uses
//! the same chunk-header pattern as `deallocate` and dispatches
//! the slot-size lookup through the chunk's `SizeOfFn`.
static inline std::size_t size_of(void *p);
//! Dedicated single-slot large allocation (sizes between
//! ALLOC_MAX_BUCKETED_SIZE and a 16-unit chunk's payload = 4 MiB − 64 B).
//! Claims a whole N-unit chunk (no sub-slot bitmap) and returns
//! chunk_base + ALLOC_CHUNK_HEADER; freed via the s_back_offset bit7
//! path in deallocate() / size_of(). Returns nullptr (caller falls
//! through to std::malloc) when the payload is too large or the region
//! cap is hit.
static void *allocate_dedicated_chunk(std::size_t size) noexcept;
//! (§22) Public forwarder to the protected `deallocate_chunk` (the
//! N-bit bitmap-CAS claim-clear + madvise that truly releases a
//! dedicated chunk). Exists so the namespace-level large-recycle
//! cache (allocator.cpp) can release a recycled dedicated chunk on
//! eviction / thread-exit. The evicting thread is the chunk's unique
//! owner (the claim bits stay set while cached, so no other thread can
//! re-claim it; the release is a single-winner CAS), hence race-free.
static void recycle_release_chunk(char *chunk_base,
std::size_t chunk_size) noexcept;
//! Address-only chunk lookup. Returns nullptr if `p` does not
//! belong to any pool chunk (or the chunk has been released).
//! Used by `drain_thread_slot_freelists` to handle the case where
//! `m_slots[bucket].freelist_head` holds slots from multiple
//! chunks of the same PoolType (e.g. FS=false sizes 96/128/160/192
//! all share `PoolAllocator<32, false>`; a chunk transition triggered
//! by one bucket can leave another bucket's freelist pointing into the
//! old chunk, yet both old- and new-chunk slots land on the same
//! freelist through the shared `s_tls.my_chunk == this` owner check). Implemented
//! as a for-loop walk of `s_mmapped_spaces[]` — each region is a
//! uniform 32 MiB, and `s_back_offset[]` maps any claimed unit back
//! to its chunk base in O(1).
static inline PoolAllocatorBase *lookup_chunk(void *p) noexcept;
//! Total live chunks across all regions, summed from
//! `s_claim_bitmap[]` (popcount of set bits). Diagnostic probe for
//! tests that want to verify release paths actually fire — leak in
//! the chunk-release path would show as monotonic growth across
//! repeated alloc/free cycles. Relaxed loads (rare path, hint
//! only; the snapshot races against concurrent
//! claim / release CAS but each bit is consistent at the moment of
//! its read).
static int count_live_chunks() noexcept;
//! Null out this thread's `s_my_chunk` for this chunk's ALIGN type.
//! Called from `AllocThreadExitCleanup` after freelist flush, before pin
//! count decrement. Prevents stale `s_my_chunk` from pushing to
//! a dead freelist when later TLS destructors (e.g.
//! `RunnerCounterRegistration` via `pthread_key`) do heap
//! alloc/dealloc after `AllocThreadExitCleanup` has already run.
virtual void clear_owner_tls() noexcept {}
//! Batch return of a contiguous run of CrossDeallocEntries whose
//! `chunk == this` to the bitmap. Each override walks
//! `entries[k]` while `entries[k].chunk == this` (terminating on
//! the trailing `{nullptr, nullptr}` sentinel or the next chunk's
//! group), merges adjacent same-m_flags-word slots into one CAS
//! per word, and returns the number of entries it consumed so the
//! caller can advance past them. Pure virtual — each
//! `PoolAllocator<ALIGN, FS, DUMMY>` supplies its own ALIGN /
//! per-FS-variant counter logic.
//!
//! Caller contract:
//! * `entries[0].chunk == this` on entry (else returns 0);
//! * `entries[k].chunk` for k ≥ count past the buffer is
//! `nullptr` (the sentinel) so the inner loop's chunk
//! comparison terminates without needing a count test.
virtual int batch_return_to_bitmap(
const CrossDeallocEntry *entries) noexcept = 0;
//! Adaptive holding hint: last `batch_return_to_bitmap` call's
//! coalescing factor for this chunk, in fixed-point ×16
//! (16 = 1.0× = no coalescing benefit, 24 = 1.5× = 33 % CAS
//! saved, 32 = 2.0× = 50 % saved, etc.). Updated `relaxed` on
//! each batch — it's a hint, not authoritative; races are
//! benign (next push reads slightly stale value, no
//! correctness impact). Read by `CrossDeallocBatch::
//! push_direct` to decide adaptively whether to hold (route to
//! the per-thread holding buf for further coalescing
//! accumulation) or dispatch immediately. Epsilon-greedy
//! explore in the caller occasionally force-holds regardless,
//! so a chunk whose factor dropped below threshold can be
//! re-evaluated.
std::atomic<uint8_t> m_last_coalesce_x16{16};
//! Freelist-miss slow allocate. Called from `new_redirected`'s
//! cold path through this chunk's vtable; runs the bitmap-CAS /
//! chunk-claim / create_allocator path with this template
//! instantiation's compile-time ALIGN. `bucket` is the table
//! index of the freelist that missed, used to refresh
//! `m_slots[bucket].freelist_head` to the (possibly advanced)
//! `s_tls.my_chunk`'s freelist cell. Pure virtual
//! so the dispatch is per-(ALIGN,FS) without a separate
//! function-pointer table.
virtual void *slow_allocate(unsigned bucket, std::size_t size) noexcept = 0;
//! Public read-only accessor for `m_chunk_size` — used by
//! anonymous-namespace helpers (e.g. `drain_thread_slot_freelists`)
//! that need to compute `chunk_base = head & ~(chunk_size - 1)`
//! without a per-template dispatch (the helper iterates all
//! buckets and slots from all chunk-template instantiations may
//! coexist on its freelists). Returns the chunk-size stamped by
//! `allocate_chunk()` at chunk-claim time.
std::size_t chunk_size() const noexcept { return m_chunk_size; }