Repository navigation
page: the first purge_holes_large_floor bytes of free blocks in large pages stay - #48
Jarred-Sumner wants to merge 1 commit into
Conversation
… pages stay After the hold of two epochs, an idle sweep took every free block of a large page. A server with a request now and then (5 requests a second, bodies of 128 KiB to 512 KiB) faulted its buffers in again on every request: twice the median latency for 4 MiB of memory. Now the first purge_holes_large_floor bytes (8 MiB, 0 for none) stay, a whole page at a time. The count is process-wide so that a pool of threads keeps 8 MiB together and not each. A tld gives its share back when its next sweep begins and when it goes away.
…lone for a while Since the idle sweep discards the free blocks of large pages (#46), a thread that handles a request now and then takes every buffer of 96 KiB and up out of discarded memory again: once the two epochs of the hold are over the blocks go, and a server with five requests a second pays a page fault for every OS page of every buffer on every request. purge_holes_large_floor (4 MiB) bytes of such blocks, process-wide, are now left where they are while their page was allocated from in the last purge_holes_large_floor_epochs (256) epochs of the sweep. - The budget is one atomic sum. A thread gives its share back when its next sweep decides again, when it exits, and in the child of a fork; what it kept and has used since counts until then (stale on the high side only). - The pages of one sweep are decided together at the end of its pass over the thread's own pages, the ones that were used last first. Abandoned pages come after that for what is left; one that stays says in swept_state which thread counts it, so that the other threads that sweep its heap leave it alone and do not count it again. - Only blocks on the free list count: the part of a page that was never formed was never written to, and it is discarded as before. - The age of a page is the epoch of its last allocation, which is in swept_state already. An epoch ends only when a thread is swept, so there is no clock in the page and nothing is woken for this: a process in which no thread parks any more keeps those bytes until one does. There is no floor without an interval to count epochs in. - A thread that stays parked is looked at again by the scavenger once what it kept is old enough to go, which is when the sweeps of other threads have moved the epoch that far: it does not hold the floor against the threads that run. The idea of a floor under what the sweeps take is from #48 (purge_holes_large_floor, there a flat number of bytes that stay for good). This one counts what was written to, prefers what was used last, and lets go of it.
…lone for a while Since the idle sweep discards the free blocks of large pages (#46), a thread that handles a request now and then takes every buffer of 96 KiB and up out of discarded memory again: once the two epochs of the hold are over the blocks go, and a server with five requests a second pays a page fault for every OS page of every buffer on every request. purge_holes_large_floor (4 MiB) bytes of such blocks, process-wide, are now left where they are while their page was allocated from in the last purge_holes_large_floor_epochs (256) epochs of the sweep. - The budget is one atomic sum. A thread gives its share back when its next sweep decides again, when it exits, and in the child of a fork; what it kept and has used since counts until then (stale on the high side only). - The pages of one sweep are decided together at the end of its pass over the thread's own pages, the ones that were used last first. Abandoned pages come after that for what is left; one that stays says in swept_state which thread counts it, so that the other threads that sweep its heap leave it alone and do not count it again (until a thread that had a share ends: then the first sweeper to pass counts it). - Only blocks on the free list count: the part of a page that was never formed was never written to, and it is discarded as before. - The age of a page is the epoch of its last allocation, which is in swept_state already. An epoch ends only when a thread is swept, so there is no clock in the page and nothing is woken for this: a process in which no thread parks any more keeps those bytes until one does. There is no floor without an interval to count epochs in. - A thread that stays parked is looked at again by the scavenger once what it kept is old enough to go, which is when the sweeps of other threads have moved the epoch that far: it does not hold the floor against the threads that run. The idea of a floor under what the sweeps take is from #48 (purge_holes_large_floor, there a flat number of bytes that stay for good). This one counts what was written to, prefers what was used last, and lets go of it.
|
#50 takes the idea of this PR (a floor under what the idle sweeps take from large pages) further, and is meant to replace it. What it does differently, from measuring this branch in a real server (
Numbers and the rule are in #50. |
|
Closing as superseded by #50 (merged; its message credits this PR for the idea). #50 keeps the same thing this did, a small budget of large-page memory that the hole purge leaves alone, but ages it in sweep epochs (no timer), prefers the most recently used pages, counts only blocks that are actually on a free list, and covers wholly free large pages, which is where most of the refaults came from. Measured on a 5 req/s server with 256 KiB bodies: 124 -> 2.5 page faults per request with #50, against about a 20% reduction with this version. The branch is left in place. |
After #46, an idle sweep takes every free block of a large page two epochs after the last allocation from it. A server that gets a request now and then takes those buffers again after that, and faults every OS page of them in again, on every request.
Now the first
purge_holes_large_floorbytes of free blocks in large pages stay (default 8 MiB,0= none,MIMALLOC_PURGE_HOLES_LARGE_FLOORin KiB). What is above the floor goes as before.holes_sweep_deferred, so nothing comes back for it.The cost this removes
Bun.serve, body in = body out, 1 connection, Linux x64, bun368a6f728(before #46) vs7d792a538(with #46):Bodies under 128 KiB and of 1 MiB and more are equal at every rate. The memory that #46 gives back in these cases is 3 to 4 MB, which is under the floor.
Tests
test-purge-holes-large: newfloorcase. 60 blocks of 320 KiB, 40 freed, floor 4 MiB: 32 purged and 2.5 MiB kept; a second sweep gives the same; all 40 purged with floor 0; the share of a thread that ends comes back.test-purge-holes-largeandtest-purge-holesset the floor to 0, as they do for the interval.Not measured yet in a bun build: that needs the
MIMALLOC_COMMITbump.