Skip to content

Commit f47492e

Browse files
committed
mm: retain repeatedly reused small-block pools
1 parent 257eedc commit f47492e

4 files changed

Lines changed: 112 additions & 8 deletions

File tree

‎qualification/suite/scripts/mm/qualify_current_mm.sh‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ compile_test standalone small-finalize memory_small_last_free_finalize.dpr \
9595
compile_test product small-pool-leak-report memory_small_pool_last_free_finalize.dpr \
9696
-dFPCMM_REPORTMEMORYLEAKS -dFPCMM_MEDIUMLASTFREE_TEST
9797
compile_test product memory-large-boundary memory_large_boundary.dpr
98+
compile_test product memory-hot-small-pool memory_hot_small_pool.dpr \
99+
-dFPCMM_SMALLPOOL_REUSE_TEST
98100
compile_test product memory-mega memory_mega.dpr
99101
compile_test product memory-massive memory_massive.dpr \
100102
-Fu"$perf_common" -dFPCMM_SMALLLASTFREE_TEST -dFPCMM_MEDIUMLASTFREE_TEST
@@ -127,6 +129,10 @@ run_one memory-large-boundary \
127129
"$build/memory-large-boundary/memory-large-boundary"
128130
grep -q 'MEMORY_LARGE_BOUNDARY_PASS' \
129131
"$results/memory-large-boundary/run.log"
132+
run_one memory-hot-small-pool \
133+
"$build/memory-hot-small-pool/memory-hot-small-pool"
134+
grep -q 'MEMORY_HOT_SMALL_POOL_PASS' \
135+
"$results/memory-hot-small-pool/run.log"
130136
run_one memory-mega-full "$build/memory-mega/memory-mega" full "$seed"
131137
grep -q 'MEMORY_MEGA_PASS' "$results/memory-mega-full/run.log"
132138
run_one memory-massive-quick \

‎qualification/suite/tests/memory/README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ must still report zero failed assertions.
4141
|---|---|
4242
| Deferred finalization | `memory_small_pool_last_free_finalize`, `memory_small_last_free_finalize`, and `memory_medium_last_free_finalize` each pass 100 isolated processes with no failed run. |
4343
| Product boundaries | `memory_large_boundary` checks allocation capacity and first/last-byte access around the large-block transitions. |
44+
| Hot small-pool reuse | `memory_hot_small_pool` proves that a larger small-block class releases cold empty pools, activates reuse only after repeated churn, and then returns the retained block without changing live-block accounting. |
4445
| Deterministic breadth | `memory_mega full` checks zero-size and realloc contracts, size classes, every configured realloc transition, pool lifecycle, a shadow-model fuzz pass, cross-thread ownership transfer, and saturation. |
4546
| Concurrent composition | `memory_massive quick` runs a five-thread ownership pipeline, remote realloc/free, forced `GetMem` contention, both deferred-free lists, and managed-value COW/refcount/unwind. It runs once normally and once with diagnostics enabled. |
4647
| Randomized composition | `memory_chaos all` mixes raw blocks, RTL-managed values, cross-thread release, realloc, and valid exit-time finalization in release and diagnostic profiles. |
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
program memory_hot_small_pool;
2+
3+
{$mode delphi}
4+
{$H+}
5+
6+
uses
7+
mormot.core.fpcx64mm;
8+
9+
const
10+
ColdFreeCount = 8;
11+
TestSize = 1024;
12+
13+
procedure Require(Condition: boolean; const MessageText: string);
14+
begin
15+
if not Condition then
16+
begin
17+
WriteLn('MEMORY_HOT_SMALL_POOL_FAIL ', MessageText);
18+
Halt(1);
19+
end;
20+
end;
21+
22+
var
23+
BeforeStatus, AfterStatus: TMMStatus;
24+
BlockType: pointer;
25+
P, Reused: pointer;
26+
I: integer;
27+
28+
begin
29+
BeforeStatus := CurrentHeapStatus;
30+
BlockType := nil;
31+
for I := 1 to ColdFreeCount do
32+
begin
33+
P := GetMem(TestSize);
34+
BlockType := Fpcx64mmTestSmallBlockType(P);
35+
FreeMem(P);
36+
Require(Fpcx64mmTestSmallEmptyPoolReuseScore(BlockType) = cardinal(I),
37+
'cold empty-pool score mismatch');
38+
Require(Fpcx64mmTestSmallRetainedPool(BlockType) = nil,
39+
'cold size class retained a pool too early');
40+
end;
41+
P := GetMem(TestSize);
42+
BlockType := Fpcx64mmTestSmallBlockType(P);
43+
FreeMem(P);
44+
Require(Fpcx64mmTestSmallEmptyPoolReuseScore(BlockType) = ColdFreeCount,
45+
'hot empty-pool score exceeded its saturation threshold');
46+
Require(Fpcx64mmTestSmallRetainedPool(BlockType) <> nil,
47+
'hot size class did not retain a reusable pool');
48+
Reused := GetMem(TestSize);
49+
Require(Reused = P, 'retained hot pool did not return its free block');
50+
FreeMem(Reused);
51+
AfterStatus := CurrentHeapStatus;
52+
Require(AfterStatus.SmallBlocks = BeforeStatus.SmallBlocks,
53+
'hot-pool reuse changed the live small-block count');
54+
Require(AfterStatus.SmallBlocksSize = BeforeStatus.SmallBlocksSize,
55+
'hot-pool reuse changed the live small-byte count');
56+
WriteLn('MEMORY_HOT_SMALL_POOL_PASS score=',
57+
Fpcx64mmTestSmallEmptyPoolReuseScore(BlockType));
58+
end.

‎runtime/mm/mormot.core.fpcx64mm.pas‎

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,12 @@ function Fpcx64mmTestSmallLastFreeCount(P: pointer): cardinal;
343343
procedure Fpcx64mmTestCorruptSmallLastFreeHead(P: pointer);
344344
{$endif FPCMM_SMALLLASTFREE_TEST}
345345

346+
{$ifdef FPCMM_SMALLPOOL_REUSE_TEST}
347+
function Fpcx64mmTestSmallBlockType(P: pointer): pointer;
348+
function Fpcx64mmTestSmallEmptyPoolReuseScore(BlockType: pointer): cardinal;
349+
function Fpcx64mmTestSmallRetainedPool(BlockType: pointer): pointer;
350+
{$endif FPCMM_SMALLPOOL_REUSE_TEST}
351+
346352
{$ifdef FPCMM_MEDIUMLASTFREE_TEST}
347353
procedure Fpcx64mmTestLockMedium(P: pointer; Locked: boolean);
348354
function Fpcx64mmTestMediumLastFree(P: pointer): pointer;
@@ -1075,6 +1081,7 @@ procedure NotifyMediumLargeFree(var Arena: TMMStatusArena; Size: PtrUInt);
10751081
SmallBlockDownsizeCheckAdder = 64;
10761082
SmallBlockUpsizeAdder = 32;
10771083
SmallBlockTypePO2 = 6; // SizeOf(TSmallBlockType)=64
1084+
SmallBlockHotPoolThreshold = 8;
10781085

10791086
MediumBlockPoolSizeMem = 20 * 64 * 1024;
10801087
MediumBlockPoolSize = MediumBlockPoolSizeMem - 16;
@@ -1159,11 +1166,12 @@ procedure NotifyMediumLargeFree(var Arena: TMMStatusArena; Size: PtrUInt);
11591166
NextSequentialFeedBlockAddress: pointer;
11601167
MaxSequentialFeedBlockAddress: pointer;
11611168
CurrentSequentialFeedPool: PSmallBlockPoolHeader;
1162-
GetmemCount: cardinal;
1163-
FreememCount: cardinal;
1164-
LastFreeLocked: boolean;
1165-
Padding: array[1 .. 3] of byte;
1166-
LastFreeCount: cardinal;
1169+
GetmemCount: cardinal;
1170+
FreememCount: cardinal;
1171+
LastFreeLocked: boolean;
1172+
EmptyPoolReuseScore: byte;
1173+
Padding: array[1 .. 2] of byte;
1174+
LastFreeCount: cardinal;
11671175
end;
11681176
PSmallBlockType = ^TSmallBlockType;
11691177

@@ -3997,14 +4005,24 @@ function _FreeMem(P: pointer): PtrUInt;
39974005
{$endif NOSFRAME}
39984006
{$ifdef FPCMM_MOONSHARD}
39994007
@EmptySequentialFeedPool:
4000-
// Keep one reusable first block for the hottest tiny size classes
4008+
// Keep one empty pool after repeated single-block churn. Tiny classes
4009+
// retain it immediately; larger small classes first prove reuse.
4010+
// Those larger classes are global, not per-thread/per-arena: retaining
4011+
// every one is bounded to about 1.2 MiB with the current pool table.
40014012
{$ifdef MSWINDOWS}
40024013
cmp word ptr [rbx].TSmallBlockType.BlockSize, 256
4014+
jbe @StoreFreeBlock
4015+
cmp byte ptr [rbx].TSmallBlockType.EmptyPoolReuseScore, SmallBlockHotPoolThreshold
4016+
jae @StoreFreeBlock
4017+
inc byte ptr [rbx].TSmallBlockType.EmptyPoolReuseScore
40034018
{$else}
40044019
cmp word ptr [rsi].TSmallBlockType.BlockSize, 256
4005-
{$endif MSWINDOWS}
40064020
jbe @StoreFreeBlock
4007-
jmp @IsSequentialFeedPool
4021+
cmp byte ptr [rsi].TSmallBlockType.EmptyPoolReuseScore, SmallBlockHotPoolThreshold
4022+
jae @StoreFreeBlock
4023+
inc byte ptr [rsi].TSmallBlockType.EmptyPoolReuseScore
4024+
{$endif MSWINDOWS}
4025+
jmp @IsSequentialFeedPool
40084026
{$endif FPCMM_MOONSHARD}
40094027
@ProcessPendingBin:
40104028
// Release the next SmallLastFree list block while we own the lock
@@ -5378,6 +5396,27 @@ procedure Fpcx64mmTestCorruptSmallLastFreeHead(P: pointer);
53785396
end;
53795397
{$endif FPCMM_SMALLLASTFREE_TEST}
53805398

5399+
{$ifdef FPCMM_SMALLPOOL_REUSE_TEST}
5400+
function Fpcx64mmTestSmallBlockType(P: pointer): pointer;
5401+
begin
5402+
result := PSmallBlockPoolHeader(
5403+
PPtrUInt(PByte(P) - BlockHeaderSize)^ and DropSmallFlagsMask)^.BlockType;
5404+
end;
5405+
5406+
function Fpcx64mmTestSmallEmptyPoolReuseScore(BlockType: pointer): cardinal;
5407+
begin
5408+
result := PSmallBlockType(BlockType)^.EmptyPoolReuseScore;
5409+
end;
5410+
5411+
function Fpcx64mmTestSmallRetainedPool(BlockType: pointer): pointer;
5412+
begin
5413+
if PSmallBlockType(BlockType)^.MaxSequentialFeedBlockAddress <> nil then
5414+
result := PSmallBlockType(BlockType)^.CurrentSequentialFeedPool
5415+
else
5416+
result := nil;
5417+
end;
5418+
{$endif FPCMM_SMALLPOOL_REUSE_TEST}
5419+
53815420
{$ifdef FPCMM_MEDIUMLASTFREE_TEST}
53825421
function TestMediumInfo(P: pointer): PMediumBlockInfo;
53835422
begin

0 commit comments

Comments
 (0)