Skip to content

rts: Gradually return retained memory to the OS

Matthew Pickering requested to merge wip/return-memory-fd into master

Related to #19381 (closed) #19359 (closed) #14702 (closed)

After a spike in memory usage we have been conservative about returning allocated blocks to the OS in case we are still allocating a lot and would end up just reallocating them. The result of this was that up to 4 * live_bytes of blocks would be retained once they were allocated even if memory usage ended up a lot lower.

For a heap of size ~1.5G, this would result in OS memory reporting 6G which is both misleading and worrying for clients. In long-lived server applications this results in consistent high memory usage when the live data size is much more reasonable (for example ghcide)

Therefore we have a new (2021) strategy which starts by retaining up to 4 * live_bytes of blocks before gradually returning uneeded memory back to the OS on subsequent major GCs which are NOT caused by a heap overflow.

Each major GC which is NOT caused by heap overflow increases the consec_idle_gcs counter and the amount of memory which is retained is inversely proportional to this number. By default the excess memory retained is oldGenFactor (controlled by -F) / 2 ^ (consec_idle_gcs * returnDecayFactor)

On a major GC caused by a heap overflow, the consec_idle_gcs variable is reset to 0 (as we could continue to allocate more, so retaining all the memory might make sense).

Therefore setting bigger values for -Fd makes the rate at which memory is returned slower. Smaller values make it get returned faster. Setting -Fd0 means no additional memory is retained.

The default is -Fd4 which results in the following scaling:

> mapM print [(x, 1/ (2**(x / 4))) | x <- [1 :: Double ..20]]
(1.0,0.8408964152537146)
(2.0,0.7071067811865475)
(3.0,0.5946035575013605)
(4.0,0.5)
(5.0,0.4204482076268573)
(6.0,0.35355339059327373)
(7.0,0.29730177875068026)
(8.0,0.25)
(9.0,0.21022410381342865)
(10.0,0.17677669529663687)
(11.0,0.14865088937534013)
(12.0,0.125)
(13.0,0.10511205190671433)
(14.0,8.838834764831843e-2)
(15.0,7.432544468767006e-2)
(16.0,6.25e-2)
(17.0,5.255602595335716e-2)
(18.0,4.4194173824159216e-2)
(19.0,3.716272234383503e-2)
(20.0,3.125e-2)

So after 13 consecutive GCs only 0.1 of the maximum memory used will be retained.

Further to this decay factor, the amount of memory we attempt to retain is also influenced by the GC strategy for the oldest generation. If we are using a copying strategy then we will need at least 2 * live_bytes for copying to take place, so we always keep that much. If using compacting then we need a lower number, so we just retain at least 1.2 * live_bytes for some protection.

Merge request reports