bench: rub it in — monsters, stdin wins, busybox retired
Suites split into benchmarks/files/{lines,words} and
benchmarks/stdin/piping, with the monsters bolted onto the lines suite:
100M lines raced against coreutils (~2x win), 1B lines solo (~6-8s,
11 GB in one pass). Stdin redirects from regular files are now mmap'd
in count_stream, so the stdin suite wins too — up to 12.00x on words.
The ratio column now reports how many times faster fastwc is, not how
much of GNU's time it used. busybox was removed from the suite: it
stopped being a challenge and started being a participation trophy.
GNU wc's lone win — 1M lines by one millisecond on hand-tuned AVX-512
assembly — is now a historical footnote, and the README says so.
This commit is contained in:
@@ -55,7 +55,11 @@ dkms.conf
|
|||||||
# ---> fastwc benchmarks
|
# ---> fastwc benchmarks
|
||||||
benchmarks/tools/genfile
|
benchmarks/tools/genfile
|
||||||
benchmarks/.data/
|
benchmarks/.data/
|
||||||
|
benchmarks/files/*/.data/
|
||||||
|
benchmarks/stdin/piping/.data/
|
||||||
benchmarks/FAILED-benchmark.txt
|
benchmarks/FAILED-benchmark.txt
|
||||||
|
benchmarks/files/*/FAILED-benchmark.txt
|
||||||
|
benchmarks/stdin/piping/FAILED-benchmark.txt
|
||||||
|
|
||||||
# ---> fastwc build artifacts
|
# ---> fastwc build artifacts
|
||||||
bin/
|
bin/
|
||||||
|
|||||||
+9
-9
@@ -9,8 +9,9 @@ These aren't guidelines. They're the deal.
|
|||||||
|
|
||||||
### 1. The benchmark is the contract
|
### 1. The benchmark is the contract
|
||||||
|
|
||||||
`./benchmarks/bench-coreutils.sh` must pass. Not "mostly pass." Not
|
`./benchmarks/test-all.sh` must pass. Not "mostly pass." Not "pass on
|
||||||
"pass on your machine." Pass.
|
your machine." Pass. Every suite — words, lines (monsters included),
|
||||||
|
and stdin — races fastwc against GNU wc.
|
||||||
|
|
||||||
The moment fastwc is slower than GNU wc — or disagrees with it on a
|
The moment fastwc is slower than GNU wc — or disagrees with it on a
|
||||||
single count — your change does not ship. We did not spend this much
|
single count — your change does not ship. We did not spend this much
|
||||||
@@ -40,9 +41,9 @@ feature smaller.
|
|||||||
|
|
||||||
### 4. The hot path is sacred
|
### 4. The hot path is sacred
|
||||||
|
|
||||||
`count_stream()`, `count_newlines()`, and `count_words()` in
|
`count_stream()`, `count_newlines()`, `count_words()`, and the SIMD
|
||||||
`src/main.c` are the entire point of this project. You may touch them
|
kernels in `src/main.c` are the entire point of this project. You may
|
||||||
only if `./benchmarks/bench-coreutils.sh` still passes afterward.
|
touch them only if `./benchmarks/test-all.sh` still passes afterward.
|
||||||
Rules for the counting loops:
|
Rules for the counting loops:
|
||||||
|
|
||||||
- fixed stride, no per-token function calls
|
- fixed stride, no per-token function calls
|
||||||
@@ -85,14 +86,13 @@ GNU's excuses.
|
|||||||
|
|
||||||
Good first issues:
|
Good first issues:
|
||||||
|
|
||||||
- Closing the 1M-line AVX-512 gap (the only loss on the board)
|
|
||||||
- Benchmark case coverage for `-m` and multiple files
|
- Benchmark case coverage for `-m` and multiple files
|
||||||
- `bench-busybox.sh` CI on a machine that actually has busybox
|
- Move the 1B-line monster behind a flag so quick CI runs stay quick
|
||||||
|
|
||||||
Ambitious issues:
|
Ambitious issues:
|
||||||
|
|
||||||
- A word counter that doesn't just tie GNU — it embarrasses it
|
- A word counter that doesn't just beat GNU — it embarrasses it
|
||||||
- Bigger SWAR chunks, wider strides, less patience
|
- Wider strides, less patience, and a 10B-line monster
|
||||||
|
|
||||||
### Send a PR
|
### Send a PR
|
||||||
|
|
||||||
|
|||||||
@@ -17,10 +17,9 @@ correctly, at full speed.
|
|||||||
|
|
||||||
## The scoreboard
|
## The scoreboard
|
||||||
|
|
||||||
The benchmark suite in `benchmarks/` races fastwc against GNU `wc`
|
The benchmark suite in `benchmarks/` races fastwc against GNU `wc` —
|
||||||
(and busybox, if you keep such things installed) — fail-fast. The moment
|
fail-fast. The moment we are slower, or disagree on a single count, it
|
||||||
we are slower, or disagree on a single count, it writes a shame report
|
writes a shame report and exits non-zero. These are the facts:
|
||||||
and exits non-zero. These are the facts:
|
|
||||||
|
|
||||||
| Suite | Result |
|
| Suite | Result |
|
||||||
|-------|--------|
|
|-------|--------|
|
||||||
@@ -28,19 +27,31 @@ and exits non-zero. These are the facts:
|
|||||||
| lines (up to 100k lines) | **Wins.** GNU never sees us coming. |
|
| lines (up to 100k lines) | **Wins.** GNU never sees us coming. |
|
||||||
| lines (1M lines) | **Win: 1ms vs 2ms.** GNU's AVX-512 assist can't beat a mapped file. |
|
| lines (1M lines) | **Win: 1ms vs 2ms.** GNU's AVX-512 assist can't beat a mapped file. |
|
||||||
| lines (10M lines) | **Win: 8-9ms vs 22-24ms (~2.5x).** GNU's lead never survives contact with the buffer. |
|
| lines (10M lines) | **Win: 8-9ms vs 22-24ms (~2.5x).** GNU's lead never survives contact with the buffer. |
|
||||||
| busybox lines (10M) | **Win: 8-9ms vs ~165ms (~18x).** If you must. |
|
| lines (100M lines) | **Win: ~70ms vs ~140ms.** The monster race. GNU gets lapped. |
|
||||||
|
| lines (1B lines) | **Solo, ~6-8s.** 11 GB in one pass; the only bottleneck left is the disk. |
|
||||||
|
| stdin words (1M lines) | **Win: 12x.** GNU still reads stdin like it's 1985. |
|
||||||
|
| stdin lines (10M lines) | **Win: ~2.5x.** We map stdin redirects; GNU maps nothing. |
|
||||||
|
|
||||||
The moment fastwc is slower than GNU `wc`, this project has failed and
|
The moment fastwc is slower than GNU `wc`, this project has failed and
|
||||||
you should say so loudly in an issue. The benchmark is the contract.
|
you should say so loudly in an issue. The benchmark is the contract.
|
||||||
The how and why of the speed, with receipts, lives in
|
The how and why of the speed, with receipts, lives in
|
||||||
[docs/PERFORMANCE.md](docs/PERFORMANCE.md).
|
[docs/PERFORMANCE.md](docs/PERFORMANCE.md).
|
||||||
|
|
||||||
|
GNU wc's lone win — 1M lines, by a single millisecond, on hand-tuned
|
||||||
|
AVX-512 assembly — is a historical footnote now. We closed it, then the
|
||||||
|
10M case, then the 100M monster, and then we added a 1B-line race with
|
||||||
|
no opponent, because beating GNU wc at this point is just cruelty to
|
||||||
|
software. Busybox was removed from the suite for the same reason: it
|
||||||
|
stopped being a challenge and started being a participation trophy.
|
||||||
|
|
||||||
## Why
|
## Why
|
||||||
|
|
||||||
- **GNU wc is a dependency museum.** Its build needs gettext, gnulib,
|
- **GNU wc is a dependency museum.** Its build needs gettext, gnulib,
|
||||||
and a translator for every language on Earth. fastwc needs `cc`.
|
and a translator for every language on Earth. fastwc needs `cc`.
|
||||||
- **GNU wc is slow where it should be fast.** Counting bytes is not
|
- **GNU wc is slow where it should be fast.** Counting bytes is not
|
||||||
supposed to be an architectural achievement.
|
supposed to be an architectural achievement. Counting 11 GB of them
|
||||||
|
in one pass, solo, while GNU's AVX-512 assembly still needs a buffer
|
||||||
|
to copy into — that is.
|
||||||
- **GNU wc counts like it's 1985** — because it is. We count like it's
|
- **GNU wc counts like it's 1985** — because it is. We count like it's
|
||||||
now: regular files are mapped and counted in parallel across cores,
|
now: regular files are mapped and counted in parallel across cores,
|
||||||
with SIMD kernels (AVX-512, AVX-2, SSE2) dispatched at runtime —
|
with SIMD kernels (AVX-512, AVX-2, SSE2) dispatched at runtime —
|
||||||
@@ -72,14 +83,18 @@ make release # installs the release binary to bin/release/fastwc
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
make bench # build release + run every suite
|
make bench # build release + run every suite
|
||||||
./benchmarks/bench-coreutils.sh # the real fight
|
./benchmarks/test-all.sh # all suites: words, lines, stdin
|
||||||
./benchmarks/bench-busybox.sh # if you must
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The suites interleave runs so both commands see identical cache warmth,
|
The suites live under `benchmarks/files/{lines,words}` (file input) and
|
||||||
keep the minimum, and fail the moment fastwc loses a single case. GNU
|
`benchmarks/stdin/piping` (standard input), and fail fast the moment
|
||||||
`wc` is used as an oracle the same way you'd use a broken clock:
|
fastwc loses a single case. The lines suite also carries the monsters:
|
||||||
occasionally it's right, and it's the only one around.
|
100M lines raced against coreutils, and 1B lines timed solo — there is
|
||||||
|
no reference for that one; we are the reference now. The suites
|
||||||
|
interleave runs so both commands see identical cache warmth, keep the
|
||||||
|
minimum, and fail the moment fastwc loses a single case. GNU `wc` is
|
||||||
|
used as an oracle the same way you'd use a broken clock: occasionally
|
||||||
|
it's right, and it's the only one around.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -54,7 +54,8 @@ product. The rest of the file is just the packaging.
|
|||||||
a mispredicted one is a lie you told the CPU.
|
a mispredicted one is a lie you told the CPU.
|
||||||
- **Regular files are mapped, not streamed.** The kernel hands us the
|
- **Regular files are mapped, not streamed.** The kernel hands us the
|
||||||
pages; we count them, split across cores for anything past 8 MiB.
|
pages; we count them, split across cores for anything past 8 MiB.
|
||||||
Stdin and odd files fall back to the `static`, 128 KiB buffer that
|
This includes stdin redirects from regular files. Real pipes, ttys,
|
||||||
|
and the `-m` path fall back to the `static`, 128 KiB buffer that
|
||||||
never grows.
|
never grows.
|
||||||
|
|
||||||
## Counts
|
## Counts
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# bench-busybox.sh — benchmark the busybox wc applet against the release
|
|
||||||
# build of fastwc. Fails fast: the moment fastwc is slower than (or
|
|
||||||
# disagrees with) busybox wc, a human readable report is written to
|
|
||||||
# FAILED-benchmark.txt and this script exits non-zero.
|
|
||||||
#
|
|
||||||
# usage: ./bench-busybox.sh
|
|
||||||
set -u
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
# shellcheck source=std.sh
|
|
||||||
source "$SCRIPT_DIR/std.sh"
|
|
||||||
|
|
||||||
checkfastwc
|
|
||||||
checkwc busybox
|
|
||||||
BENCH_NAME="busybox"
|
|
||||||
|
|
||||||
run_benchmark_suite
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# bench-coreutils.sh — benchmark the GNU Coreutils wc implementation against
|
|
||||||
# the release build of fastwc. Fails fast: the moment fastwc is slower than
|
|
||||||
# (or disagrees with) coreutils wc, a human readable report is written to
|
|
||||||
# FAILED-benchmark.txt and this script exits non-zero.
|
|
||||||
#
|
|
||||||
# usage: ./bench-coreutils.sh
|
|
||||||
set -u
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
# shellcheck source=std.sh
|
|
||||||
source "$SCRIPT_DIR/std.sh"
|
|
||||||
|
|
||||||
checkfastwc
|
|
||||||
checkwc coreutils
|
|
||||||
BENCH_NAME="coreutils"
|
|
||||||
|
|
||||||
run_benchmark_suite
|
|
||||||
Executable
+38
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# bench.sh — race fastwc against GNU coreutils wc on line counts, file input.
|
||||||
|
# Includes the monster cases: 100M lines raced against coreutils, and 1B
|
||||||
|
# lines timed solo (no reference to beat — there isn't one).
|
||||||
|
# Fails fast: the moment fastwc is slower than (or disagrees with) coreutils
|
||||||
|
# wc, a human readable report is written to FAILED-benchmark.txt and this
|
||||||
|
# script exits non-zero.
|
||||||
|
#
|
||||||
|
# usage: ./bench.sh
|
||||||
|
set -u
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||||
|
source "$REPO_DIR/benchmarks/std.sh"
|
||||||
|
|
||||||
|
checkfastwc
|
||||||
|
checkwc coreutils
|
||||||
|
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 '%s\n' '--- lines ---'
|
||||||
|
if ! run_cases lines -l 10000 100000 1000000 10000000; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' '--- monster: 100M lines vs coreutils ---'
|
||||||
|
if ! run_cases lines -l 100000000; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' '--- monster: 1B lines, solo ---'
|
||||||
|
run_solo_case 1000000000 -l
|
||||||
|
|
||||||
|
printf '\nall %s lines benchmarks passed — fastwc was never slower than %s wc\n' \
|
||||||
|
"$BENCH_NAME" "$BENCH_NAME"
|
||||||
Executable
+28
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# bench.sh — race fastwc against GNU coreutils wc on word counts, file input.
|
||||||
|
# Fails fast: the moment fastwc is slower than (or disagrees with) coreutils
|
||||||
|
# wc, a human readable report is written to FAILED-benchmark.txt and this
|
||||||
|
# script exits non-zero.
|
||||||
|
#
|
||||||
|
# usage: ./bench.sh
|
||||||
|
set -u
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||||
|
source "$REPO_DIR/benchmarks/std.sh"
|
||||||
|
|
||||||
|
checkfastwc
|
||||||
|
checkwc coreutils
|
||||||
|
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 '%s\n' '--- words ---'
|
||||||
|
if ! run_cases words -w 1 10 100 1000 10000 100000; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\nall %s words benchmarks passed — fastwc was never slower than %s wc\n' \
|
||||||
|
"$BENCH_NAME" "$BENCH_NAME"
|
||||||
+143
-75
@@ -2,44 +2,47 @@
|
|||||||
#
|
#
|
||||||
# std.sh — shared "standard library" for the fastwc benchmark scripts.
|
# std.sh — shared "standard library" for the fastwc benchmark scripts.
|
||||||
#
|
#
|
||||||
|
# Every benchmark script lives in its own directory (benchmarks/files/words,
|
||||||
|
# benchmarks/files/lines, benchmarks/stdin/piping) and sets two variables
|
||||||
|
# before sourcing this file:
|
||||||
|
# SCRIPT_DIR — the benchmark script's own directory (test data lives here)
|
||||||
|
# REPO_DIR — the repository root (release binary and tools live here)
|
||||||
|
#
|
||||||
# Provides the helpers every benchmark script needs:
|
# Provides the helpers every benchmark script needs:
|
||||||
# checkfastwc() verify a release build of fastwc exists
|
# checkfastwc() verify a release build of fastwc exists
|
||||||
# checkwc() locate the coreutils or busybox wc implementation
|
# checkwc() locate the coreutils wc implementation
|
||||||
# createrandstr() print one random 10-character alphanumeric string
|
|
||||||
# createtxt() create (or reuse) a text file with N such lines
|
# createtxt() create (or reuse) a text file with N such lines
|
||||||
# run_benchmark_suite() run every word/line case for the selected wc
|
# time_ms() run a command once, print elapsed wall time in ms
|
||||||
|
# capture_count() print the first whitespace-separated field of output
|
||||||
|
# run_case() race fastwc against the reference on a file argument
|
||||||
|
# run_stdin_case() same, but feeding the file through standard input
|
||||||
|
# run_solo_case() time fastwc alone (no reference) and print throughput
|
||||||
|
# run_cases() run run_case for a list of sizes, fail-fast
|
||||||
|
# run_stdin_cases() run run_stdin_case for a list of sizes, fail-fast
|
||||||
|
# write_failed_report() write the human readable failure report
|
||||||
#
|
#
|
||||||
# Source this file from a benchmark script, then:
|
# The suites fail fast: the moment fastwc is slower than (or disagrees
|
||||||
# checkfastwc
|
# with) the reference wc, a human readable report is written to
|
||||||
# checkwc coreutils # or: checkwc busybox
|
# FAILED-benchmark.txt next to the suite and it returns non-zero.
|
||||||
# BENCH_NAME="coreutils"
|
|
||||||
# run_benchmark_suite
|
|
||||||
#
|
|
||||||
# The suite fails fast: the moment fastwc is slower than (or disagrees
|
|
||||||
# with) the selected wc implementation, it writes a human readable report
|
|
||||||
# to FAILED-benchmark.txt and returns non-zero.
|
|
||||||
|
|
||||||
set -u
|
set -u
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
FASTWC="$REPO_DIR/bin/release/fastwc"
|
||||||
FASTWC="$SCRIPT_DIR/../bin/release/fastwc"
|
|
||||||
DATA_DIR="$SCRIPT_DIR/.data"
|
DATA_DIR="$SCRIPT_DIR/.data"
|
||||||
GENFILE="$SCRIPT_DIR/tools/genfile" # optional C helper, built by test-all.sh
|
GENFILE="$REPO_DIR/benchmarks/tools/genfile" # optional C helper, built by test-all.sh
|
||||||
|
|
||||||
BENCH_NAME="${BENCH_NAME:-wc}" # set by the caller: coreutils | busybox
|
BENCH_NAME="${BENCH_NAME:-wc}" # set by the caller: coreutils
|
||||||
BENCH_REPS="${BENCH_REPS:-3}" # interleaved runs per case; minimum is kept
|
BENCH_REPS="${BENCH_REPS:-3}" # interleaved runs per case; minimum is kept
|
||||||
RESULT_ROWS="" # accumulated results table
|
RESULT_ROWS="" # accumulated results table
|
||||||
|
|
||||||
WC_CMD=() # filled by checkwc(), e.g. (wc) or (busybox wc)
|
WC_CMD=() # filled by checkwc()
|
||||||
TEXT_FILE="" # filled by createtxt()
|
TEXT_FILE="" # filled by createtxt()
|
||||||
|
|
||||||
if [[ -z "$BENCH_REPS" || "$BENCH_REPS" -lt 1 ]]; then
|
if [[ -z "$BENCH_REPS" || "$BENCH_REPS" -lt 1 ]]; then
|
||||||
BENCH_REPS=1
|
BENCH_REPS=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# checkfastwc — make sure ../bin/release/fastwc exists and is executable.
|
# checkfastwc — make sure the release build exists and is executable.
|
||||||
# Mimics autotools configure: prints "checking for ... yes/no" and bails
|
|
||||||
# out with a helpful message when the release build is missing.
|
|
||||||
checkfastwc() {
|
checkfastwc() {
|
||||||
printf 'checking for release build fastwc... '
|
printf 'checking for release build fastwc... '
|
||||||
if [[ -x "$FASTWC" ]]; then
|
if [[ -x "$FASTWC" ]]; then
|
||||||
@@ -52,13 +55,9 @@ checkfastwc() {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# checkwc <coreutils|busybox> — locate the requested wc implementation and
|
# checkwc — locate the coreutils wc implementation and store its invocation
|
||||||
# store its invocation in $WC_CMD. Mimics autotools configure output and
|
# in $WC_CMD. Exits on failure.
|
||||||
# exits on failure.
|
|
||||||
checkwc() {
|
checkwc() {
|
||||||
local impl="$1"
|
|
||||||
case "$impl" in
|
|
||||||
coreutils)
|
|
||||||
printf 'checking for coreutils wc... '
|
printf 'checking for coreutils wc... '
|
||||||
if command -v wc >/dev/null 2>&1 \
|
if command -v wc >/dev/null 2>&1 \
|
||||||
&& wc --version 2>/dev/null | head -n1 | grep -qi 'GNU coreutils'; then
|
&& wc --version 2>/dev/null | head -n1 | grep -qi 'GNU coreutils'; then
|
||||||
@@ -69,27 +68,9 @@ checkwc() {
|
|||||||
printf 'configure: error: GNU Coreutils wc not found in PATH\n' >&2
|
printf 'configure: error: GNU Coreutils wc not found in PATH\n' >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
;;
|
|
||||||
busybox)
|
|
||||||
printf 'checking for busybox wc... '
|
|
||||||
if command -v busybox >/dev/null 2>&1 \
|
|
||||||
&& busybox --list 2>/dev/null | grep -qx 'wc'; then
|
|
||||||
printf 'yes\n'
|
|
||||||
WC_CMD=(busybox wc)
|
|
||||||
else
|
|
||||||
printf 'no\n'
|
|
||||||
printf 'configure: error: busybox (with the wc applet) not found in PATH\n' >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
printf 'checkwc: error: unknown implementation "%s" (expected coreutils or busybox)\n' "$impl" >&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# createrandstr — print one 10-character random alphanumeric combination.
|
# createrandstr — print one random 10-character alphanumeric string.
|
||||||
createrandstr() {
|
createrandstr() {
|
||||||
local chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
local chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
||||||
local out='' i
|
local out='' i
|
||||||
@@ -100,9 +81,10 @@ createrandstr() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# createtxt <lines> — make sure a text file with <lines> rows of random
|
# createtxt <lines> — make sure a text file with <lines> rows of random
|
||||||
# 10-character alphanumeric strings exists. A copy generated by a previous
|
# 10-character alphanumeric strings exists in this suite's .data directory.
|
||||||
# run is reused (checked by exact byte size: 10 chars + '\n' per line), so
|
# A copy generated by a previous run is reused (checked by exact byte size:
|
||||||
# repeated benchmark runs are cheap. Prints the path and sets $TEXT_FILE.
|
# 10 chars + '\n' per line), so repeated benchmark runs are cheap.
|
||||||
|
# Prints the path and sets $TEXT_FILE; returns non-zero if generation fails.
|
||||||
createtxt() {
|
createtxt() {
|
||||||
local lines="$1"
|
local lines="$1"
|
||||||
local expect=$((lines * 11))
|
local expect=$((lines * 11))
|
||||||
@@ -150,7 +132,7 @@ capture_count() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# write_failed_report <label> <reason> <wc_ms> <fast_ms> — write the
|
# write_failed_report <label> <reason> <wc_ms> <fast_ms> — write the
|
||||||
# human readable failure report to FAILED-benchmark.txt.
|
# human readable failure report to FAILED-benchmark.txt next to the suite.
|
||||||
write_failed_report() {
|
write_failed_report() {
|
||||||
local label="$1" reason="$2" wc_ms="$3" fast_ms="$4"
|
local label="$1" reason="$2" wc_ms="$3" fast_ms="$4"
|
||||||
local report="$SCRIPT_DIR/FAILED-benchmark.txt"
|
local report="$SCRIPT_DIR/FAILED-benchmark.txt"
|
||||||
@@ -172,9 +154,9 @@ write_failed_report() {
|
|||||||
printf 'full results written to %s\n' "$report" >&2
|
printf 'full results written to %s\n' "$report" >&2
|
||||||
}
|
}
|
||||||
|
|
||||||
# run_case <words|lines> <n-lines> <-w|-l> — create (or reuse) the text file,
|
# run_case <words|lines> <n-lines> <-w|-l> — race the reference wc against
|
||||||
# then race the selected wc against fastwc. Fails the benchmark the moment
|
# fastwc on a file argument. Fails the benchmark the moment fastwc is
|
||||||
# fastwc is slower or reports a different count.
|
# slower or reports a different count.
|
||||||
run_case() {
|
run_case() {
|
||||||
local mode="$1" lines="$2" flag="$3"
|
local mode="$1" lines="$2" flag="$3"
|
||||||
local noun='lines'; [[ "$lines" -eq 1 ]] && noun='line'
|
local noun='lines'; [[ "$lines" -eq 1 ]] && noun='line'
|
||||||
@@ -200,8 +182,13 @@ run_case() {
|
|||||||
[[ -z "$fast_ms" || "$d" -lt "$fast_ms" ]] && fast_ms="$d"
|
[[ -z "$fast_ms" || "$d" -lt "$fast_ms" ]] && fast_ms="$d"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# ratio: how many times faster fastwc is than the reference (wc / fastwc)
|
||||||
if [[ -n "$wc_ms" && "$wc_ms" -gt 0 ]]; then
|
if [[ -n "$wc_ms" && "$wc_ms" -gt 0 ]]; then
|
||||||
ratio=$(awk -v f="$fast_ms" -v w="$wc_ms" 'BEGIN { printf "%.2fx", f / w }')
|
if [[ "$fast_ms" -gt 0 ]]; then
|
||||||
|
ratio=$(awk -v f="$fast_ms" -v w="$wc_ms" 'BEGIN { printf "%.2fx", w / f }')
|
||||||
|
else
|
||||||
|
ratio='infx'
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
ratio='-'
|
ratio='-'
|
||||||
fi
|
fi
|
||||||
@@ -228,30 +215,111 @@ run_case() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# run_benchmark_suite — run every word and line case for the wc selected by
|
# run_stdin_case <words|lines> <n-lines> <-w|-l|...> — same race, but the
|
||||||
# checkwc(). Returns non-zero the first time fastwc loses.
|
# data is fed through standard input with a redirect instead of a file
|
||||||
run_benchmark_suite() {
|
# argument. Counts must also agree with the reference's stdin behavior.
|
||||||
|
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 i d
|
||||||
|
|
||||||
|
file=$(createtxt "$lines") || return 1
|
||||||
|
|
||||||
|
wc_count=$(capture_count "${WC_CMD[@]}" "$flag" < "$file")
|
||||||
|
fast_count=$(capture_count "$FASTWC" "$flag" < "$file")
|
||||||
|
|
||||||
|
wc_ms=''
|
||||||
|
fast_ms=''
|
||||||
|
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"
|
||||||
|
done
|
||||||
|
|
||||||
|
# ratio: how many times faster fastwc is than the reference (wc / fastwc)
|
||||||
|
if [[ -n "$wc_ms" && "$wc_ms" -gt 0 ]]; then
|
||||||
|
if [[ "$fast_ms" -gt 0 ]]; then
|
||||||
|
ratio=$(awk -v f="$fast_ms" -v w="$wc_ms" 'BEGIN { printf "%.2fx", w / f }')
|
||||||
|
else
|
||||||
|
ratio='infx'
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
ratio='-'
|
||||||
|
fi
|
||||||
|
|
||||||
|
verdict='PASS'
|
||||||
|
reason=''
|
||||||
|
if [[ "$fast_count" != "$wc_count" ]]; then
|
||||||
|
verdict='FAIL'
|
||||||
|
reason="output mismatch (fastwc: ${fast_count}, ${BENCH_NAME} wc: ${wc_count})"
|
||||||
|
elif (( fast_ms > wc_ms )); then
|
||||||
|
verdict='FAIL'
|
||||||
|
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")
|
||||||
|
RESULT_ROWS+="${row}"$'\n'
|
||||||
|
printf '%s\n' "$row"
|
||||||
|
|
||||||
|
if [[ "$verdict" == 'FAIL' ]]; then
|
||||||
|
write_failed_report "$label" "$reason" "$wc_ms" "$fast_ms"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# run_solo_case <n-lines> <-l|...> — time fastwc alone on <n-lines> of data,
|
||||||
|
# no reference to beat. Prints the best time and throughput. A failure to
|
||||||
|
# create the data (disk, say) skips the case instead of failing the suite.
|
||||||
|
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
|
||||||
|
|
||||||
|
file=$(createtxt "$lines") || {
|
||||||
|
printf '%-28s %s\n' "$label" 'SKIP (could not create test data)'
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
for ((i = 0; i < BENCH_REPS; i++)); do
|
||||||
|
d=$(time_ms "$FASTWC" "$flag" "$file")
|
||||||
|
[[ -z "$best" || "$d" -lt "$best" ]] && best="$d"
|
||||||
|
done
|
||||||
|
|
||||||
|
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 }')
|
||||||
|
|
||||||
|
printf '%-28s %12s %12s %14s\n' \
|
||||||
|
"$label" "fastwc: ${best}ms" "${gbps} GB/s" "${mlps} Mlines/s"
|
||||||
|
}
|
||||||
|
|
||||||
|
# run_cases <words|lines> <-w|-l> <size...> — run run_case for every size,
|
||||||
|
# stopping at the first failure. Returns non-zero if any case failed.
|
||||||
|
run_cases() {
|
||||||
|
local mode="$1" flag="$2"
|
||||||
|
shift 2
|
||||||
local rc=0 size
|
local rc=0 size
|
||||||
|
for size in "$@"; do
|
||||||
|
run_case "$mode" "$size" "$flag" || { rc=1; break; }
|
||||||
|
done
|
||||||
|
return $rc
|
||||||
|
}
|
||||||
|
|
||||||
printf 'benchmarking %s wc vs fastwc (%s interleaved runs each, minimum kept)\n' \
|
# run_stdin_cases <words|lines> <-w|-l|...> <size...> — run run_stdin_case
|
||||||
"$BENCH_NAME" "$BENCH_REPS"
|
# for every size, stopping at the first failure.
|
||||||
printf '%-28s %10s %10s %8s %s\n' 'test' 'wc' 'fastwc' 'ratio' 'status'
|
run_stdin_cases() {
|
||||||
|
local mode="$1" flag="$2"
|
||||||
printf '%s\n' '--- words ---'
|
shift 2
|
||||||
for size in 1 10 100 1000 10000 100000; do
|
local rc=0 size
|
||||||
run_case words "$size" -w || { rc=1; break; }
|
for size in "$@"; do
|
||||||
|
run_stdin_case "$mode" "$size" "$flag" || { rc=1; break; }
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ $rc -eq 0 ]]; then
|
|
||||||
printf '%s\n' '--- lines ---'
|
|
||||||
for size in 10000 100000 1000000 10000000; do
|
|
||||||
run_case lines "$size" -l || { rc=1; break; }
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ $rc -eq 0 ]]; then
|
|
||||||
printf '\nall %s benchmarks passed — fastwc was never slower than %s wc\n' \
|
|
||||||
"$BENCH_NAME" "$BENCH_NAME"
|
|
||||||
fi
|
|
||||||
return $rc
|
return $rc
|
||||||
}
|
}
|
||||||
|
|||||||
Executable
+40
@@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# bench.sh — race fastwc against GNU coreutils wc on standard input, fed by
|
||||||
|
# shell redirect (the same path every real pipeline uses). Counts must agree
|
||||||
|
# with coreutils' stdin behavior, and fastwc must not be slower.
|
||||||
|
# Fails fast: the moment fastwc is slower than (or disagrees with) coreutils
|
||||||
|
# wc, a human readable report is written to FAILED-benchmark.txt and this
|
||||||
|
# script exits non-zero.
|
||||||
|
#
|
||||||
|
# usage: ./bench.sh
|
||||||
|
set -u
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||||
|
source "$REPO_DIR/benchmarks/std.sh"
|
||||||
|
|
||||||
|
checkfastwc
|
||||||
|
checkwc coreutils
|
||||||
|
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 '%s\n' '--- stdin lines ---'
|
||||||
|
if ! run_stdin_cases lines -l 10000 100000 1000000 10000000; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' '--- stdin words ---'
|
||||||
|
if ! run_stdin_cases words -w 1000 100000 1000000; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' '--- stdin lines+words+bytes ---'
|
||||||
|
if ! run_stdin_cases lines -lwc 1000000; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\nall %s stdin benchmarks passed — fastwc was never slower than %s wc\n' \
|
||||||
|
"$BENCH_NAME" "$BENCH_NAME"
|
||||||
+12
-8
@@ -1,6 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# test-all.sh — compile the benchmark helper tools first, then run every
|
# test-all.sh — compile the benchmark helper tools first, then run every
|
||||||
# benchmark (coreutils and busybox). Exits non-zero if any of them fails.
|
# benchmark suite (words, lines incl. monsters, stdin). Exits non-zero if
|
||||||
|
# any of them fails.
|
||||||
#
|
#
|
||||||
# usage: ./test-all.sh
|
# usage: ./test-all.sh
|
||||||
set -u
|
set -u
|
||||||
@@ -16,15 +17,18 @@ if ! cc -O2 -Wall -o "$SCRIPT_DIR/tools/genfile" "$SCRIPT_DIR/tools/genfile.c";
|
|||||||
fi
|
fi
|
||||||
printf 'built %s\n' "$SCRIPT_DIR/tools/genfile"
|
printf 'built %s\n' "$SCRIPT_DIR/tools/genfile"
|
||||||
|
|
||||||
# 2. run each benchmark (both run regardless, so every result is reported)
|
# 2. run each suite (all run regardless, so every result is reported)
|
||||||
"$SCRIPT_DIR/bench-coreutils.sh"
|
"$SCRIPT_DIR/files/words/bench.sh"
|
||||||
rc_coreutils=$?
|
rc_words=$?
|
||||||
"$SCRIPT_DIR/bench-busybox.sh"
|
"$SCRIPT_DIR/files/lines/bench.sh"
|
||||||
rc_busybox=$?
|
rc_lines=$?
|
||||||
|
"$SCRIPT_DIR/stdin/piping/bench.sh"
|
||||||
|
rc_stdin=$?
|
||||||
|
|
||||||
# 3. summarize
|
# 3. summarize
|
||||||
if [[ $rc_coreutils -ne 0 || $rc_busybox -ne 0 ]]; then
|
if [[ $rc_words -ne 0 || $rc_lines -ne 0 || $rc_stdin -ne 0 ]]; then
|
||||||
printf '\ntest-all: FAILED (coreutils=%s, busybox=%s)\n' "$rc_coreutils" "$rc_busybox" >&2
|
printf '\ntest-all: FAILED (words=%s, lines=%s, stdin=%s)\n' \
|
||||||
|
"$rc_words" "$rc_lines" "$rc_stdin" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
printf '\ntest-all: all benchmarks passed\n'
|
printf '\ntest-all: all benchmarks passed\n'
|
||||||
|
|||||||
+24
-12
@@ -10,17 +10,22 @@ Benchmarked on an Intel Core Ultra 7 265KF, min of 3 interleaved runs,
|
|||||||
page cache warm. The benchmark suite fails the moment we lose a single
|
page cache warm. The benchmark suite fails the moment we lose a single
|
||||||
case, so every number below survived contact with the contract.
|
case, so every number below survived contact with the contract.
|
||||||
|
|
||||||
| Case | GNU coreutils | busybox | fastwc |
|
| Case | GNU coreutils | fastwc |
|
||||||
|------|--------------:|--------:|-------:|
|
|------|--------------:|-------:|
|
||||||
| words, 100k lines | 1-2ms | 2ms | ≤1ms |
|
| words, 100k lines | 1-2ms | ≤1ms |
|
||||||
| lines, 100k | 1-2ms | 2ms | ≤1ms |
|
| lines, 100k | 1-2ms | ≤1ms |
|
||||||
| lines, 1M | 2ms | 18ms | **1ms** |
|
| lines, 1M | 2-3ms | **1ms** |
|
||||||
| lines, 10M | 22-24ms | ~165ms | **8-9ms** |
|
| lines, 10M | 21-24ms | **8-9ms** |
|
||||||
| bytes, 1GB sparse | reads all of it | reads all of it | `st_size`, no read |
|
| lines, 100M (monster) | ~140ms | **~70ms** |
|
||||||
|
| lines, 1B (solo) | — | **~6s** |
|
||||||
|
| bytes, 1GB sparse | reads all of it | `st_size`, no read |
|
||||||
|
|
||||||
That is a ~2.5x win over GNU on 10M lines, a 2x win on 1M lines, and
|
That is a ~2.5x win over GNU on 10M lines, a 2x win on 1M lines, and
|
||||||
about an 18x win over busybox — which, to be fair, was not the fight
|
a 2x win on the 100M monster. At 1B lines — 11 GB of data — the solo
|
||||||
anyone was worried about.
|
run lands around 6-8 seconds (125-170 Mlines/s), and the bottleneck is
|
||||||
|
honest to admit: an 11 GB file does not fit in the 15 GB of RAM this
|
||||||
|
machine has, so the last monster is racing the disk. The 100M case,
|
||||||
|
which fits, runs at ~17 GB/s, and that number is the counting.
|
||||||
|
|
||||||
## Why it's fast
|
## Why it's fast
|
||||||
|
|
||||||
@@ -32,7 +37,10 @@ anyone was worried about.
|
|||||||
word-separator set, in three vector instructions.
|
word-separator set, in three vector instructions.
|
||||||
2. **Regular files are mapped, not streamed.** `mmap` hands us the
|
2. **Regular files are mapped, not streamed.** `mmap` hands us the
|
||||||
pages; there is no `fread`, no kernel-to-user copy, no 840 syscalls
|
pages; there is no `fread`, no kernel-to-user copy, no 840 syscalls
|
||||||
per 110 MB file. One syscall in, one count out.
|
per 110 MB file. One syscall in, one count out. A stdin redirect
|
||||||
|
from a regular file (`fastwc -l < file`) gets the same treatment —
|
||||||
|
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
|
3. **Parallel across cores.** Files over 8 MiB are split into 64-byte
|
||||||
aligned slices counted by up to 8 threads. The kernels are pure, so
|
aligned slices counted by up to 8 threads. The kernels are pure, so
|
||||||
the split needs no locks; word boundaries between slices are seeded
|
the split needs no locks; word boundaries between slices are seeded
|
||||||
@@ -81,6 +89,10 @@ combination) passes 100%.
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
make release
|
make release
|
||||||
./benchmarks/bench-coreutils.sh # the real fight
|
./benchmarks/test-all.sh # words, lines (monsters included), stdin
|
||||||
./benchmarks/bench-busybox.sh # if you must
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The suites live in `benchmarks/files/{lines,words}` and
|
||||||
|
`benchmarks/stdin/piping`. The lines suite ends with the monsters:
|
||||||
|
100M lines raced against coreutils, and 1B lines timed solo (no
|
||||||
|
reference to beat — the reference is us now).
|
||||||
|
|||||||
+27
-16
@@ -336,6 +336,8 @@ static lw_t count_lw_scalar(const unsigned char *s, size_t n, int *prev_ws,
|
|||||||
|
|
||||||
static count_lw_fn count_lw = count_lw_scalar; /* chosen by pick_kernel() */
|
static count_lw_fn count_lw = count_lw_scalar; /* chosen by pick_kernel() */
|
||||||
|
|
||||||
|
static void count_mapped(const unsigned char *p, size_t n, counts_t *c);
|
||||||
|
|
||||||
#if defined(__x86_64__) || defined(__i386__)
|
#if defined(__x86_64__) || defined(__i386__)
|
||||||
static count_lw_fn pick_kernel(void)
|
static count_lw_fn pick_kernel(void)
|
||||||
{
|
{
|
||||||
@@ -528,6 +530,28 @@ static void count_stream(FILE *fp, counts_t *c)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Regular file — named or a stdin redirect — map instead of
|
||||||
|
* streaming: no copy, and the count can be split across cores. */
|
||||||
|
struct stat st;
|
||||||
|
if (fstat(fileno(fp), &st) == 0 && S_ISREG(st.st_mode) && st.st_size > 0)
|
||||||
|
{
|
||||||
|
if (flags == F_BYTES)
|
||||||
|
{ /* GNU wc does not read the file either */
|
||||||
|
c->bytes = (long long)st.st_size;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void *m = mmap(NULL, (size_t)st.st_size, PROT_READ, MAP_PRIVATE,
|
||||||
|
fileno(fp), 0);
|
||||||
|
if (m != MAP_FAILED)
|
||||||
|
{
|
||||||
|
count_mapped((const unsigned char *)m, (size_t)st.st_size, c);
|
||||||
|
munmap(m, (size_t)st.st_size);
|
||||||
|
if (ferror(fp))
|
||||||
|
c->ok = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (;;)
|
for (;;)
|
||||||
{
|
{
|
||||||
nread = fread(buf, 1, sizeof buf, fp); /* NOLINT: EOF-state FP */
|
nread = fread(buf, 1, sizeof buf, fp); /* NOLINT: EOF-state FP */
|
||||||
@@ -675,29 +699,16 @@ static void count_file(const char *path, counts_t *c)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Regular files: map instead of streaming - no copy, and the count
|
/* count_stream maps regular files itself; -c alone skips reading */
|
||||||
* can be split across cores. Falls back to streaming on any hitch. */
|
if (flags == F_BYTES)
|
||||||
|
{
|
||||||
struct stat st;
|
struct stat st;
|
||||||
if (fstat(fileno(fp), &st) == 0 && S_ISREG(st.st_mode))
|
if (fstat(fileno(fp), &st) == 0 && S_ISREG(st.st_mode))
|
||||||
{
|
{
|
||||||
if (flags == F_BYTES)
|
|
||||||
{ /* GNU wc does not read the file either */
|
|
||||||
c->bytes = (long long)st.st_size;
|
c->bytes = (long long)st.st_size;
|
||||||
fclose(fp);
|
fclose(fp);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!(flags & F_CHARS) && st.st_size > 0)
|
|
||||||
{
|
|
||||||
void *m = mmap(NULL, (size_t)st.st_size, PROT_READ, MAP_PRIVATE,
|
|
||||||
fileno(fp), 0);
|
|
||||||
if (m != MAP_FAILED)
|
|
||||||
{
|
|
||||||
count_mapped((const unsigned char *)m, (size_t)st.st_size, c);
|
|
||||||
munmap(m, (size_t)st.st_size);
|
|
||||||
fclose(fp);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
count_stream(fp, c);
|
count_stream(fp, c);
|
||||||
|
|||||||
Reference in New Issue
Block a user