Implement a sqrt heap growth curve#1864
Conversation
Heap-pivot sets the point at which growth switches from linear to sqrt
Cli options will still override
Sam May (ag-eitilt)
left a comment
There was a problem hiding this comment.
Looks good to me! I would have expected a more dramatic graph of the savings after the crossover point, but I think that's down to the log scale understating the benefits this provides.
| using type = double; | ||
| using input_type = type; |
There was a problem hiding this comment.
I can understand the type aliasing to make the purpose more clear than just having a random double in the signature, but why the double aliasing through type first? And do we want a more indicative name than "input"?
There was a problem hiding this comment.
Mostly following the pattern established in the file. We have a using type = bool; up above.
| char *tail; | ||
| double v = strtod(clo.heappivot, &tail); | ||
| if (*tail || v < 1.0) { | ||
| std::cerr << "Cannot run with " << clo.heappivot << " heap-pivot (must be >= 1.0 MB)!" |
There was a problem hiding this comment.
Doesn't need to be output to stderr, but maybe add a comment each describing the math that leads to these restrictions?
| static void set_env_var(HeapFactorPolicy& p, const char*) {} | ||
| }; | ||
|
|
||
| struct HeapPivotPolicy { |
There was a problem hiding this comment.
These structs themselves could also be good places to add comments describing their role in the algorithm. At this point, we all know how the heap factor is used in the growth algorithm, but we only know that due to a good while of people picking through the code and explaining to the rest of us; it's still not superficially obvious to anyone coming in fresh. And the pivot is a new concept entirely.
| << " --fatal-warnings Do not execute if there are any warnings" << std::endl | ||
| << " --heap-factor X Heap-size is X * live data after the last GC (default 4.0)" << std::endl | ||
| << " --heap-factor X GC headroom scale factor (default 4.0)" << std::endl | ||
| << " --heap-pivot X Pivot point in MB where GC growth curve transitions (default 64)" << std::endl |
There was a problem hiding this comment.
64 MB is not a lot as a default, I had the assumption this pivot would happen we go into the GB territory , which makes me wonder whats the point of giving a pivot, and not having everything as a sqrt growth? Whats the benefit of having a pivot (the normal growth rate) at all?
There was a problem hiding this comment.
The pivot also influences the sqrt rate growth. It helps bend it per say. So lower pivot point, less aggressive growth upper heap sizes.
pivot 64MB, heap factor 4x
Live (MB) Old heap Old sys (2×) New heap New sys (2×)
----------------------------------------------------------------------
200.0 800.0 1600.0 652.5 1305.1
400.0 1600.0 3200.0 1040.0 2080.0
800.0 3200.0 6400.0 1705.1 3410.2
1600.0 6400.0 12800.0 2880.0 5760.0
3200.0 12800.0 25600.0 5010.2 10020.4
6400.0 25600.0 51200.0 8960.0 17920.0
12800.0 51200.0 102400.0 16420.4 32840.8
25600.0 102400.0 204800.0 30720.0 61440.0
51200.0 204800.0 409600.0 58440.8 116881.5
102400.0 409600.0 819200.0 112640.0 225280.0
```
Pivot 256MB, heap factor 4x
```
Live (MB) Old heap Old sys (2×) New heap New sys (2×)
----------------------------------------------------------------------
200.0 800.0 1600.0 800.0 1600.0
400.0 1600.0 3200.0 1600.0 3200.0
800.0 3200.0 6400.0 2610.2 5220.4
1600.0 6400.0 12800.0 4160.0 8320.0
3200.0 12800.0 25600.0 6820.4 13640.8
6400.0 25600.0 51200.0 11520.0 23040.0
12800.0 51200.0 102400.0 20040.8 40081.5
25600.0 102400.0 204800.0 35840.0 71680.0
51200.0 204800.0 409600.0 65681.5 131363.1
102400.0 409600.0 819200.0 122880.0 245760.0
```
|
I would recommend testing against our internal repo CI, to see if we see much of a timing difference for the rest of pipelines with these changes |
| size_t sqrt_desired = | ||
| live_pads + | ||
| static_cast<size_t>(heap_factor * std::sqrt(static_cast<double>(live_pads) * | ||
| static_cast<double>(heap_pivot_pads))); |
There was a problem hiding this comment.
Maybe not for this PR, but I think its worth following the equation in the paper in the future: https://dl.acm.org/doi/epdf/10.1145/3563323
where the tunable heap_factor and heap_pivot gets reduced into an adaptive runtime measured constant C
M = L + sqrt(L · C)
where the constant C = g / cs (GC duration / bytes allocated between GC's divided by mutator time)
benefit here being automatic adaption instead of tuning the builds all the time. I think we should have access to most of the pieces in the equation
There was a problem hiding this comment.
Yeah, definitely something worth considering. I do plan on more tweaks to this (dealing with the 1.5x blind growth) so can revisit then.
Wake Heap allocation is based on a simple geomtric growth curve:
max (Heap_factor * last_live_heap, current_heap_size)This is a Stop-and-copy GC with 2 semi-spaces, we pay double the cost for the heap. We also have a blind 1.5x multiplier when we actually
malloc/reallocto help avoid having to pay extra malloc costs.This does not scale well or reflect most use cases once it approaches GB heap size. Kilobytes are free, megabytes are cheap, gigabytes of memory gets expensive or unattainable.
This PR updates the growth algorithm to include a Pivot point (
heap_pivot) where the growth goes from an aggressive linear growth to a sqrt growth.last_live_heap + heap_factor* sqrt(last_live_heap * heap_pivot))Heap-factoris still used to impact the growth rate, but it's impact is tied to the sqrt growth. This doesn't address anything about always keeping 2x heap around or switch our GC approach. I decided not to touch the 1.5x overhead malloc in this PR or anything about the heap downsizing (1/3) but I believe it should also follow some growth curve.Performance impacts:
There's a lot of variables in play I tried to consider (fast/slow growth, long/short heap objects, how often GC gets triggered, etc.), but don't have any strong mathematical backing or data to share.
In any real testing example I have, I did not see anything of concern with GC being invoked too aggressively. This mostly helped ensure large heaps don't hit OOM issues unnecessarily.
Here are a couple graphs showing the growth curve before and after change.

Here's some literature i referenced when creating this:
https://dl.acm.org/doi/epdf/10.1145/3563323