Compare commits

..
2 Commits
Author SHA1 Message Date
huntedbytheirs 55aab093e7 bench: report fastwc timing in microseconds
Sub-millisecond runs showed as a flat 0ms, hiding real differences. The
suite now times fastwc with a time_us helper, keeps the minimum reading
in µs, derives ms from it, and prints both (fastwc: 0ms (767µs)). GNU wc
stays at ms precision; solo case shows µs too.
2026-08-29 18:56:50 -04:00
huntedbytheirs af61660c42 fix: cap thread count before slice sizing, scale monsters to ncpu
count_sliced computed the slice size from the requested thread count and
only then clamped nt to MAX_THREADS, so the last slice ended before the
file tail: raising the cap (as pick_threads now does) silently dropped
the tail from every count. Cap nt first, then derive per.

Monsters (>=256 MiB) now get one thread per core (capped at 24) instead
of a hard 16, which measures ~1.17x on the 1B-line case and is flat on
warm files. check_sliced now sweeps the cap boundary (16, 24, 25).
2026-08-29 18:56:48 -04:00
6 changed files with 68 additions and 42 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ BENCH_NAME="coreutils"
printf 'benchmarking %s wc vs fastwc: lines, file input (%s interleaved runs each, minimum kept)\n' \
"$BENCH_NAME" "$BENCH_REPS"
printf '%-28s %10s %10s %8s %s\n' 'test' 'wc' 'fastwc' 'ratio' 'status'
printf '%-28s %10s %22s %8s %s\n' 'test' 'wc' 'fastwc (ms µs)' 'ratio' 'status'
printf '%s\n' '--- lines ---'
if ! run_cases lines -l 10000 100000 1000000 10000000; then
+1 -1
View File
@@ -17,7 +17,7 @@ BENCH_NAME="coreutils"
printf 'benchmarking %s wc vs fastwc: words, file input (%s interleaved runs each, minimum kept)\n' \
"$BENCH_NAME" "$BENCH_REPS"
printf '%-28s %10s %10s %8s %s\n' 'test' 'wc' 'fastwc' 'ratio' 'status'
printf '%-28s %10s %22s %8s %s\n' 'test' 'wc' 'fastwc (ms µs)' 'ratio' 'status'
printf '%s\n' '--- words ---'
if ! run_cases words -w 1 10 100 1000 10000 100000; then
+38 -21
View File
@@ -131,6 +131,17 @@ time_ms() {
printf '%s\n' "$(( (e - s) / 1000000 ))"
}
# time_us <cmd...> — run a command once and print elapsed wall time in µs.
# More precise than time_ms: sub-millisecond runs come out as e.g. 812,
# not 0. The benchmark keeps the µs reading for fastwc and derives the ms.
time_us() {
local s e
s=$(date +%s%N)
"$@" >/dev/null 2>&1
e=$(date +%s%N)
printf '%s\n' "$(( (e - s) / 1000 ))"
}
# capture_count <cmd...> — print the first whitespace-separated field of a
# command's output, i.e. the count reported by `wc -w/-l` or `fastwc -w/-l`.
capture_count() {
@@ -151,7 +162,7 @@ write_failed_report() {
printf 'failure : %s\n' "$reason"
printf '\nresults\n'
printf '%s\n' '-------'
printf '%-28s %10s %10s %8s %s\n' 'test' 'wc' 'fastwc' 'ratio' 'status'
printf '%-28s %10s %22s %8s %s\n' 'test' 'wc' 'fastwc (ms µs)' 'ratio' 'status'
printf '%s' "$RESULT_ROWS"
printf '\nfastwc must never be slower than %s wc — benchmark aborted.\n' "$BENCH_NAME"
} > "$report"
@@ -167,7 +178,7 @@ run_case() {
local mode="$1" lines="$2" flag="$3"
local noun='lines'; [[ "$lines" -eq 1 ]] && noun='line'
local label="${mode} (${lines} ${noun})"
local file wc_count fast_count wc_ms fast_ms ratio verdict reason row
local file wc_count fast_count wc_ms fast_us fast_ms ratio verdict reason row
local i d
file=$(createtxt "$lines") || return 1
@@ -177,16 +188,18 @@ run_case() {
fast_count=$(capture_count "$FASTWC" "$flag" "$file")
# speed: interleaved timing so both commands see identical cache warmth;
# keep the minimum of $BENCH_REPS runs each to reduce noise
# keep the minimum of $BENCH_REPS runs each to reduce noise; fastwc is
# timed in µs so sub-millisecond wins are visible in the report
wc_ms=''
fast_ms=''
fast_us=''
for ((i = 0; i < BENCH_REPS; i++)); do
d=$(time_ms "${WC_CMD[@]}" "$flag" "$file")
[[ -z "$wc_ms" || "$d" -lt "$wc_ms" ]] && wc_ms="$d"
d=$(time_ms "$FASTWC" "$flag" "$file")
[[ -z "$fast_ms" || "$d" -lt "$fast_ms" ]] && fast_ms="$d"
d=$(time_us "$FASTWC" "$flag" "$file")
[[ -z "$fast_us" || "$d" -lt "$fast_us" ]] && fast_us="$d"
done
fast_ms=$(( fast_us / 1000 ))
# ratio: how many times faster fastwc is than the reference (wc / fastwc)
if [[ -n "$wc_ms" && "$wc_ms" -gt 0 ]]; then
@@ -209,8 +222,9 @@ run_case() {
reason="fastwc was slower (fastwc: ${fast_ms}ms vs ${BENCH_NAME} wc: ${wc_ms}ms)"
fi
row=$(printf '%-28s %10s %10s %8s %s\n' \
"$label" "wc: ${wc_ms}ms" "fastwc: ${fast_ms}ms" "$ratio" "$verdict")
row=$(printf '%-28s %10s %22s %8s %s\n' \
"$label" "wc: ${wc_ms}ms" "fastwc: ${fast_ms}ms (${fast_us}µs)" \
"$ratio" "$verdict")
RESULT_ROWS+="${row}"$'\n'
printf '%s\n' "$row"
@@ -228,7 +242,7 @@ run_stdin_case() {
local mode="$1" lines="$2" flag="$3"
local noun='lines'; [[ "$lines" -eq 1 ]] && noun='line'
local label="stdin ${mode} (${lines} ${noun})"
local file wc_count fast_count wc_ms fast_ms ratio verdict reason row
local file wc_count fast_count wc_ms fast_us fast_ms ratio verdict reason row
local i d
file=$(createtxt "$lines") || return 1
@@ -237,14 +251,15 @@ run_stdin_case() {
fast_count=$(capture_count "$FASTWC" "$flag" < "$file")
wc_ms=''
fast_ms=''
fast_us=''
for ((i = 0; i < BENCH_REPS; i++)); do
d=$(time_ms "${WC_CMD[@]}" "$flag" < "$file")
[[ -z "$wc_ms" || "$d" -lt "$wc_ms" ]] && wc_ms="$d"
d=$(time_ms "$FASTWC" "$flag" < "$file")
[[ -z "$fast_ms" || "$d" -lt "$fast_ms" ]] && fast_ms="$d"
d=$(time_us "$FASTWC" "$flag" < "$file")
[[ -z "$fast_us" || "$d" -lt "$fast_us" ]] && fast_us="$d"
done
fast_ms=$(( fast_us / 1000 ))
# ratio: how many times faster fastwc is than the reference (wc / fastwc)
if [[ -n "$wc_ms" && "$wc_ms" -gt 0 ]]; then
@@ -267,8 +282,9 @@ run_stdin_case() {
reason="fastwc was slower (fastwc: ${fast_ms}ms vs ${BENCH_NAME} wc: ${wc_ms}ms)"
fi
row=$(printf '%-28s %10s %10s %8s %s\n' \
"$label" "wc: ${wc_ms}ms" "fastwc: ${fast_ms}ms" "$ratio" "$verdict")
row=$(printf '%-28s %10s %22s %8s %s\n' \
"$label" "wc: ${wc_ms}ms" "fastwc: ${fast_ms}ms (${fast_us}µs)" \
"$ratio" "$verdict")
RESULT_ROWS+="${row}"$'\n'
printf '%s\n' "$row"
@@ -286,7 +302,7 @@ run_solo_case() {
local lines="$1" flag="$2"
local noun='lines'; [[ "$lines" -eq 1 ]] && noun='line'
local label="solo ${lines} ${noun}"
local file ms best='' bytes gbps mlps i d
local file best_us='' best_ms bytes gbps mlps i d
file=$(createtxt "$lines") || {
printf '%-28s %s\n' "$label" 'SKIP (could not create test data)'
@@ -294,16 +310,17 @@ run_solo_case() {
}
for ((i = 0; i < BENCH_REPS; i++)); do
d=$(time_ms "$FASTWC" "$flag" "$file")
[[ -z "$best" || "$d" -lt "$best" ]] && best="$d"
d=$(time_us "$FASTWC" "$flag" "$file")
[[ -z "$best_us" || "$d" -lt "$best_us" ]] && best_us="$d"
done
best_ms=$(( best_us / 1000 ))
bytes=$((lines * 11))
gbps=$(awk -v b="$bytes" -v ms="$best" 'BEGIN { printf "%.2f", b / ms / 1e6 }')
mlps=$(awk -v l="$lines" -v ms="$best" 'BEGIN { printf "%.1f", l / ms / 1e3 }')
gbps=$(awk -v b="$bytes" -v ms="$best_ms" 'BEGIN { if (ms < 1) ms = 1; printf "%.2f", b / ms / 1e6 }')
mlps=$(awk -v l="$lines" -v ms="$best_ms" 'BEGIN { if (ms < 1) ms = 1; printf "%.1f", l / ms / 1e3 }')
printf '%-28s %12s %12s %14s\n' \
"$label" "fastwc: ${best}ms" "${gbps} GB/s" "${mlps} Mlines/s"
printf '%-28s %25s %12s %14s\n' \
"$label" "fastwc: ${best_ms}ms (${best_us}µs)" "${gbps} GB/s" "${mlps} Mlines/s"
}
# run_cases <words|lines> <-w|-l> <size...> — run run_case for every size,
+1 -1
View File
@@ -19,7 +19,7 @@ BENCH_NAME="coreutils"
printf 'benchmarking %s wc vs fastwc: stdin (%s interleaved runs each, minimum kept)\n' \
"$BENCH_NAME" "$BENCH_REPS"
printf '%-28s %10s %10s %8s %s\n' 'test' 'wc' 'fastwc' 'ratio' 'status'
printf '%-28s %10s %22s %8s %s\n' 'test' 'wc' 'fastwc (ms µs)' 'ratio' 'status'
printf '%s\n' '--- stdin lines ---'
if ! run_stdin_cases lines -l 10000 100000 1000000 10000000; then
+6 -5
View File
@@ -43,11 +43,12 @@ which fits, runs at ~17 GB/s, and that number is the counting.
the data comes through stdin, but how we read it is our business.
The stdin suite is why this shows up in the scoreboard too.
3. **Parallel across cores.** Files over 8 MiB are split into 64-byte
aligned slices counted by up to 16 threads (12 past 32 MiB, 16 past
256 MiB). The kernels are pure, so the split needs no locks; word
boundaries between slices are seeded from the byte before the slice,
which makes the split exact. Below 8 MiB the thread spawn would cost
more than the counting, so we don't bother.
aligned slices counted by up to one thread per core (capped at 24)
past 256 MiB, 12 past 32 MiB, 4 past 8 MiB. The kernels are pure, so
the split needs no locks; word boundaries between slices are seeded
from the byte before the slice, which makes the split exact. Below
8 MiB the thread spawn would cost more than the counting, so we don't
bother.
4. **No work that isn't asked for.** `-c` on a regular file is
`st_size` from `fstat` — GNU figured that one out too, so we copied
the good idea. `-l` without `-w` skips the whitespace mask entirely.
+21 -13
View File
@@ -36,6 +36,11 @@ enum
F_BYTES = 1 << 3, /* -c: bytes */
};
enum
{
MAX_THREADS = 24, /* pick_threads() and count_sliced() agree on this */
};
static int flags = 0;
typedef struct
@@ -623,18 +628,15 @@ static void count_sliced(const unsigned char *p, size_t n, int nt,
int need_lines, int need_words, long long *lines,
long long *words)
{
enum
{
MAX_THREADS = 16
};
mjob_t jobs[MAX_THREADS];
pthread_t th[MAX_THREADS];
long long tl = 0, tw = 0;
size_t per = (n + (size_t)nt - 1) / (size_t)nt;
size_t per;
int i;
if (nt > MAX_THREADS)
nt = MAX_THREADS;
nt = MAX_THREADS; /* cap first: per is derived from the real nt */
per = (n + (size_t)nt - 1) / (size_t)nt;
per = (per + 63) & ~(size_t)63;
if (per == 0)
per = 64;
@@ -678,16 +680,18 @@ static int pick_threads(size_t n)
long ncpu = sysconf(_SC_NPROCESSORS_ONLN);
int nt;
if (ncpu <= 0)
ncpu = 1;
if (n >= (size_t)256 << 20)
nt = 16;
nt = (int)ncpu; /* monsters: one thread per core, readahead wins */
else if (n >= (size_t)32 << 20)
nt = 12;
nt = (int)ncpu < 12 ? (int)ncpu : 12;
else if (n >= (size_t)8 << 20)
nt = 4;
nt = (int)ncpu < 4 ? (int)ncpu : 4;
else
nt = 1;
if (ncpu > 0 && nt > ncpu)
nt = (int)ncpu;
if (nt > MAX_THREADS)
nt = MAX_THREADS;
return nt;
}
@@ -1103,13 +1107,16 @@ static int check_sliced(void)
static unsigned char buf[9001];
int fails = 0;
size_t n, k;
static const int tcs[] = {
1, 2, 3, 4, 8, 12, 16, MAX_THREADS, MAX_THREADS + 1};
for (n = 0; n <= 9000; n += (n < 300 ? 1 : 37))
{
for (k = 0; k < n; k++)
buf[k] = (unsigned char)rng32();
for (int nt = 1; nt <= 8; nt++)
for (size_t ti = 0; ti < sizeof tcs / sizeof tcs[0]; ti++)
{
int nt = tcs[ti];
long long tl = 0, tw = 0;
long long want_l = count_newlines(buf, n);
int pw = 1;
@@ -1133,8 +1140,9 @@ static int check_sliced(void)
{
for (k = 0; k < n; k++)
buf[k] = (unsigned char)" \t\n\v\f\r\xa0x"[rng32() % 8];
for (int nt = 1; nt <= 8; nt++)
for (size_t ti = 0; ti < sizeof tcs / sizeof tcs[0]; ti++)
{
int nt = tcs[ti];
long long tl = 0, tw = 0;
long long want_l = count_newlines(buf, n);
int pw = 1;