Skip to content

[HEAP.INC TIER 1 EVO] Introduce “huge” chunks.

Like most of the memory managers do, add “huge” chunks beyond a certain threshold (presently 1/2 of GrowHeapSize2 = 512 Kb) that thinly wrap SysOSAlloc/Free/Realloc.

At first glance, you get the same behavior in trunk. But there, huge allocations become “variable” chunks. Ungood consequences are, you can allocate 9.999 Mb + 1 Kb in one OS chunk, preventing simple reallocations of that 9.999-Mb chunk and simple freeing of this entire 10-Mb OS chunk if 1 Kb lingers.

With more effort, they COULD be implemented as a subcase of variable chunks rather than a completely new chunk type, and trunk has the rudiments of this solution taken from oldheap.inc with no ideological changes. Major reasons for a separate type:

  • Assuming SysOSAlloc/Realloc/Free are thread-safe, this allows no thread tracking whatsoever. In trunk, allocating 100 Mb in one thread and reallocating in another thread has the potential to become a nightmare, as variable chunks have thread affinity.

  • TrySysOSRealloc now works only with “huge” chunks. (It was probably introduced for this very case: reallocating huge chunks.) While trunk TrySysOSRealloc supports non-huge chunks, it rarely works at all because of the very stringent and brittle conditions to work; new TrySysOSRealloc has no conditions other than requiring a “huge” chunk. Another thing is, I planned to introduce rbtrees, and trunk TrySysOSRealloc would require even more complex pointer fixups than it does now, I’d rather have all of these eliminated.

Minor reason: this allows uint32 instead of SizeUint for variable chunk sizes.

Synthetic example of the improved scenario:

var
	huge, med: array[0 .. 9] of pointer;
	i: SizeInt;
begin
	for i := 0 to High(huge) do
	begin
		huge[i] := GetMem(10 * 1024 * 1024 - 2000);
		med[i] := GetMem(1000);
	end;
	for i := 0 to High(huge) do
		FreeMem(huge[i]);
	writeln('CurrHeapUsed: ', GetFPCHeapStatus.CurrHeapUsed / (1024 * 1024):0:2, ' Mb');
	writeln('CurrHeapSize: ', GetFPCHeapStatus.CurrHeapSize / (1024 * 1024):0:2, ' Mb');
	for i := 0 to High(med) do
		FreeMem(med[i]);
end.

Output before → after:

CurrHeapUsed: 0.01 Mb   → 0.01 Mb
CurrHeapSize: 100.03 Mb → 0.28 Mb

The bad thing about this proposal is memory usage statistics: it becomes a mix of global (huge chunks) and thread-local (everything else) statistics, and as a natural consequence, Max* values become unreliable: if a thread allocated 100 Mb and they are supposed to be counted in everyone’s statistics, others can’t instantly know that and update their local values. I guess it is acceptable and alternatives (like excluding huge chunks from all statistics or giving them thread affinities, not for freeing but just for counting in someone’s statistics) are worse. Multithreaded statistics (remains plausible and) didn’t make much sense anyway, and single-threaded should remain perfect.

Merge request reports

Loading