Repository navigation
util: reuse the hash read buffer instead of zeroing 128KB per file - #2939
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #2939 +/- ##
==========================================
+ Coverage 75.66% 75.68% +0.01%
==========================================
Files 74 74
Lines 43955 43975 +20
==========================================
+ Hits 33260 33281 +21
+ Misses 10695 10694 -1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Merging this PR will improve performance by ×2.52 warnings
Performance Changes
Tip Curious why performance improved? Comment Comparing Footnotes
|
|
@codspeedbot fix this regression |
I fixed the memory regression and pushed it to this branch as Cause: the 128KB per-thread heap buffer was never freed, so every thread that hashed a file kept 128KB allocated. That's the 324 B → 131,428 B jump in Fix (
I added Results (all runs in the same environment):
If you'd rather have the full speed on small files and keep the per-thread buffer, comment Summary
Was this response helpful? React with 👍 or 👎 to help us improve CodSpeedBot. |
eca2f55 to
085ad43
Compare
085ad43 to
c7383ef
Compare
3611a4b to
061b1ba
Compare
Digest::reader_sync_with and Digest::update_from_file each zeroed a fresh 128KB buffer for every file they hashed. Direct mode hashes every included header on each cache hit, and those are usually a few KB, so the memset cost more than the hashing: it was ~15% of the server's CPU on direct-mode hits. Keep one buffer per thread and reuse it, falling back to a fresh one if it is already borrowed further up the stack. Server CPU per direct-mode cache hit (perf stat, 200 hits): C: 0.95 ms -> 0.76 ms (-20%) C++ (STL headers): 4.21 ms -> 3.57 ms (-15%) End to end (hyperfine, 100 runs), a C direct-mode hit goes from 2.6 ms to 2.3 ms; on the C++ one the gain is within the run-to-run noise.
The per-thread 128KB buffer stayed allocated for the life of each thread that hashed a file, and changed the heap layout seen by later allocations. Read the first 64KB through a 16KB stack buffer instead, and only switch to the 128KB one for larger inputs. Headers, which direct mode hashes the most, then only zero 16KB, and nothing stays allocated between calls.
The 16KB stack buffer is zeroed for every input hashed, which was 45% of the hash_multiple_files benchmark under CodSpeed's simulation: more than hashing the data itself. std::io::copy reads through a stack buffer that it does not zero for readers able to fill uninitialized memory, such as files and byte slices. Feed the digest (and the time macro finder) through it as a Write, so that nothing is zeroed and nothing stays allocated between calls. Local medians (divan), main / per-thread buffer / 16KB stack buffer / this: hash_multiple_files 35.6 / 20.2 / 20.5 / 19.6 us hash_header_file 7.4 / 5.9 / 5.9 / 5.7 us hash_large_data 867 / 829 / 875 / 836 us time_macro_finder_no_macros 300 / 274 / 299 / 268 us
061b1ba to
0220bb3
Compare
util: reuse the hash read buffer instead of zeroing 128KB per file
Digest::reader_sync_with and Digest::update_from_file each zeroed a fresh
128KB buffer for every file they hashed. Direct mode hashes every included
header on each cache hit, and those are usually a few KB, so the memset cost
more than the hashing: it was ~15% of the server's CPU on direct-mode hits.
Keep one buffer per thread and reuse it, falling back to a fresh one if it is
already borrowed further up the stack.
Server CPU per direct-mode cache hit (perf stat, 200 hits):
C: 0.95 ms -> 0.76 ms (-20%)
C++ (STL headers): 4.21 ms -> 3.57 ms (-15%)
End to end (hyperfine, 100 runs), a C direct-mode hit goes from 2.6 ms to
2.3 ms; on the C++ one the gain is within the run-to-run noise.
util: hash through stack buffers instead of a per-thread heap buffer
The per-thread 128KB buffer stayed allocated for the life of each thread
that hashed a file, and changed the heap layout seen by later allocations.
Read the first 64KB through a 16KB stack buffer instead, and only switch
to the 128KB one for larger inputs. Headers, which direct mode hashes the
most, then only zero 16KB, and nothing stays allocated between calls.
util: hash through std::io::copy, without zeroing a buffer
The 16KB stack buffer is zeroed for every input hashed, which was 45% of
the hash_multiple_files benchmark under CodSpeed's simulation: more than
hashing the data itself.
std::io::copy reads through a stack buffer that it does not zero for
readers able to fill uninitialized memory, such as files and byte slices.
Feed the digest (and the time macro finder) through it as a Write, so that
nothing is zeroed and nothing stays allocated between calls.
Local medians (divan), main / per-thread buffer / 16KB stack buffer / this:
hash_multiple_files 35.6 / 20.2 / 20.5 / 19.6 us
hash_header_file 7.4 / 5.9 / 5.9 / 5.7 us
hash_large_data 867 / 829 / 875 / 836 us
time_macro_finder_no_macros 300 / 274 / 299 / 268 us