You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
wc -l appears to be consistently slower than gnu implementation #7929
There is a lot of variability, so sometimes it's better to look at the minimum number in the range (the example below doesn't have this problem), and I sandwiched the rust version between coreutils runs to avoid thermal concerns:
seq 10000000000000 1 inf | head -n 100000000 > seq100M
cargo build -r -p uu_wc && taskset -c 0 hyperfine --warmup 10 -L wc wc,target/release/wc,wc "{wc} -l tmp/seq100M"
Finished `release` profile [optimized] target(s) in 0.10s
Benchmark 1: wc -l tmp/seq100M
Time (mean ± σ): 244.9 ms ± 5.9 ms [User: 44.3 ms, System: 197.8 ms]
Range (min … max): 239.5 ms … 262.3 ms 12 runs
Benchmark 2: target/release/wc -l tmp/seq100M
Time (mean ± σ): 265.1 ms ± 5.2 ms [User: 50.4 ms, System: 211.7 ms]
Range (min … max): 256.9 ms … 278.2 ms 11 runs
Benchmark 3: wc -l tmp/seq100M
Time (mean ± σ): 245.8 ms ± 5.7 ms [User: 42.6 ms, System: 200.5 ms]
Range (min … max): 242.2 ms … 261.7 ms 12 runs
Summary
wc -l tmp/seq100M ran
1.00 ± 0.03 times faster than wc -l tmp/seq100M
1.08 ± 0.03 times faster than target/release/wc -l tmp/seq100M
First, super low hanging fruit, increasing read buffer size from 16kb to 128kb improves performance by about 4%. That's half the difference.
Second, the rest of the performance difference is probably in bytecount's AVX2 code. That library does something a bit complicated:
while haystack.len() >= offset + 32 * 255 {
let mut counts = _mm256_setzero_si256();
for _ in 0..255 {
counts = _mm256_sub_epi8(
counts,
_mm256_cmpeq_epi8(mm256_from_offset(haystack, offset), needles),
);
offset += 32;
}
count += sum(&counts);
}
Basically accumulating counts into each byte of a 256-bit vector, then summing them up.
But GNU's wc does a much tighter loop (just looking at disassembly in samply):
vpcmpeqb ymm0, ymm1, ymmword [rdx] << compares, set all 1 in each byte if equal, 0 if not
vpmovmskb esi, ymm0 << Picks the MSb from each byte
popcnt esi, esi << counts the number of 1
I'll try to modify bytecount and run benchmarks (intuition would be that the simpler code is faster, but we'll need to check of course).
Activity
sylvestre commentedon May 13, 2025
@drinkcat if you want a small challenge ;)
drinkcat commentedon May 13, 2025
That does look fun, I'll take a look!
drinkcat commentedon May 14, 2025
I can reproduce with some simple output of
seq.There is a lot of variability, so sometimes it's better to look at the minimum number in the range (the example below doesn't have this problem), and I sandwiched the rust version between coreutils runs to avoid thermal concerns:
drinkcat commentedon May 14, 2025
Ok, 2 things:
First, super low hanging fruit, increasing
readbuffer size from 16kb to 128kb improves performance by about 4%. That's half the difference.Second, the rest of the performance difference is probably in
bytecount's AVX2 code. That library does something a bit complicated:Basically accumulating counts into each byte of a 256-bit vector, then summing them up.
But GNU's
wcdoes a much tighter loop (just looking at disassembly insamply):I'll try to modify
bytecountand run benchmarks (intuition would be that the simpler code is faster, but we'll need to check of course).drinkcat commentedon May 14, 2025
The second part was a red herring, it seems like both approaches are very similar in performance (once I managed to get Rust to actually emit
popcnt).But! Performance increases significantly if the buffer is aligned on a 32-byte boundary (
#[repr(align(32))]in a wrapping structure).We're on par with GNU with those 2 changes (larger, aligned buffer).
I'll clean up and push later. Nice thing here is that these changes are all contained within
coreutils.jnsgruk commentedon May 14, 2025
Nice! Quick work!
drinkcat commentedon May 15, 2025
@jnsgruk #7934. Might be nice to test on your machine, performance profile might be a bit different on different CPUs.
(I'm running this on the P core of a i7-1370P -- that's a laptop chip)