first: setup
setting up repo.
This commit is contained in:
+23
@@ -52,3 +52,26 @@ Module.symvers
|
|||||||
Mkfile.old
|
Mkfile.old
|
||||||
dkms.conf
|
dkms.conf
|
||||||
|
|
||||||
|
# ---> fastwc benchmarks
|
||||||
|
benchmarks/tools/genfile
|
||||||
|
benchmarks/.data/
|
||||||
|
benchmarks/FAILED-benchmark.txt
|
||||||
|
|
||||||
|
# ---> fastwc build artifacts
|
||||||
|
bin/
|
||||||
|
fastwc
|
||||||
|
|
||||||
|
# ---> autotools generated
|
||||||
|
Makefile
|
||||||
|
Makefile.in
|
||||||
|
aclocal.m4
|
||||||
|
autom4te.cache/
|
||||||
|
compile
|
||||||
|
config.log
|
||||||
|
config.status
|
||||||
|
configure
|
||||||
|
depcomp
|
||||||
|
.deps/
|
||||||
|
install-sh
|
||||||
|
missing
|
||||||
|
|
||||||
|
|||||||
+139
@@ -0,0 +1,139 @@
|
|||||||
|
# Contributing to fastwc
|
||||||
|
|
||||||
|
We're building the word counter GNU wc is still trying to be after forty
|
||||||
|
years. If that sounds like your kind of project, keep reading.
|
||||||
|
|
||||||
|
## The rules
|
||||||
|
|
||||||
|
These aren't guidelines. They're the deal.
|
||||||
|
|
||||||
|
### 1. The benchmark is the contract
|
||||||
|
|
||||||
|
`./benchmarks/bench-coreutils.sh` must pass. Not "mostly pass." Not
|
||||||
|
"pass on your machine." Pass.
|
||||||
|
|
||||||
|
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
|
||||||
|
effort being faster than a forty-year-old dependency museum just so you
|
||||||
|
could add a `strlen()` in the hot loop.
|
||||||
|
|
||||||
|
The one standing exception: the 1M-line case, where GNU wins by exactly
|
||||||
|
one millisecond because they ship hand-tuned AVX-512 assembly. Closing
|
||||||
|
that gap is the project's open goal, not your excuse to be slower
|
||||||
|
anywhere else.
|
||||||
|
|
||||||
|
### 2. C99, or don't bother
|
||||||
|
|
||||||
|
We compile with `-Wall -Wextra` and zero warnings. Zero. If the compiler
|
||||||
|
is complaining, the compiler is right and you are wrong. We target C99
|
||||||
|
with POSIX extensions, not whatever your compiler vendor's imagination
|
||||||
|
happens to ship this quarter.
|
||||||
|
|
||||||
|
### 3. No new dependencies. Ever.
|
||||||
|
|
||||||
|
The C standard library is the only thing we trust. No gettext. No gnulib.
|
||||||
|
No `libutf8pro` with its own translation team. GNU wc needs a build
|
||||||
|
system the size of a small country to count newlines. We need `cc`.
|
||||||
|
|
||||||
|
If your feature needs a library, the feature is too big. Make the
|
||||||
|
feature smaller.
|
||||||
|
|
||||||
|
### 4. The hot path is sacred
|
||||||
|
|
||||||
|
`count_stream()`, `count_newlines()`, and `count_words()` in
|
||||||
|
`src/main.c` are the entire point of this project. You may touch them
|
||||||
|
only if `./benchmarks/bench-coreutils.sh` still passes afterward.
|
||||||
|
Rules for the counting loops:
|
||||||
|
|
||||||
|
- fixed stride, no per-token function calls
|
||||||
|
- no allocation, no locking, no syscalls — the read is the only syscall
|
||||||
|
- branchless where it costs nothing; lookup tables are the foundation,
|
||||||
|
not a hack
|
||||||
|
- if you make it "clearer" but 2x slower, it's not clearer, it's worse
|
||||||
|
|
||||||
|
### 5. Never suppress the truth
|
||||||
|
|
||||||
|
No casts to silence warnings. No `-Wno-*` flags to make the noise go
|
||||||
|
away. No `void*` where a real type exists. If the type system is
|
||||||
|
fighting you, you're fighting the design. Fix the design.
|
||||||
|
|
||||||
|
### 6. The codebase is one file. Keep it that way.
|
||||||
|
|
||||||
|
`src/main.c` is one file and it will stay one file. GNU wc needs
|
||||||
|
forty-two translation units and a build system with its own mailing
|
||||||
|
list. We have one file and we can read all of it in one sitting. If your
|
||||||
|
feature needs a new file, your feature is over-engineered.
|
||||||
|
|
||||||
|
### 7. Tests are the benchmarks
|
||||||
|
|
||||||
|
There is no separate test suite, because the benchmark *is* the test
|
||||||
|
suite — it checks correctness against GNU wc (the only oracle around,
|
||||||
|
and it's wrong often enough to keep us humble) and speed in the same
|
||||||
|
breath. Add a flag? It gets benchmarked. Change the counting? It gets
|
||||||
|
benchmarked. The fail-fast scripts in `benchmarks/` are your tests, and
|
||||||
|
all of them must pass before you open a PR.
|
||||||
|
|
||||||
|
### 8. No AI slop
|
||||||
|
|
||||||
|
If it looks like ChatGPT wrote it, it gets rejected. We can tell. Write
|
||||||
|
code like a human who's been doing this for a decade and is tired of
|
||||||
|
GNU's excuses.
|
||||||
|
|
||||||
|
## How to contribute
|
||||||
|
|
||||||
|
### Pick something
|
||||||
|
|
||||||
|
Good first issues:
|
||||||
|
|
||||||
|
- Closing the 1M-line AVX-512 gap (the only loss on the board)
|
||||||
|
- Benchmark case coverage for `-m` and multiple files
|
||||||
|
- `bench-busybox.sh` CI on a machine that actually has busybox
|
||||||
|
|
||||||
|
Ambitious issues:
|
||||||
|
|
||||||
|
- A word counter that doesn't just tie GNU — it embarrasses it
|
||||||
|
- Bigger SWAR chunks, wider strides, less patience
|
||||||
|
|
||||||
|
### Send a PR
|
||||||
|
|
||||||
|
1. Fork the repo
|
||||||
|
2. Create a branch: `feat/my-thing` or `fix/my-bug`
|
||||||
|
3. Write code that follows the rules above
|
||||||
|
4. Run `./benchmarks/test-all.sh` — everything must pass
|
||||||
|
5. Open a PR against `main`
|
||||||
|
|
||||||
|
### PR requirements
|
||||||
|
|
||||||
|
- Build must pass with zero warnings: `make && make release`
|
||||||
|
- Benchmarks must pass: `./benchmarks/test-all.sh`
|
||||||
|
- Follow [STYLEGUIDE.md](STYLEGUIDE.md). The style is the law.
|
||||||
|
- No commented-out code. No dead code. No TODO without a date.
|
||||||
|
- Commit messages in imperative: `Count newlines in 8-byte strides` not
|
||||||
|
`Added newline counting optimization`
|
||||||
|
|
||||||
|
## What we won't merge
|
||||||
|
|
||||||
|
- **Anything slower than GNU wc.** We didn't come this far to tie.
|
||||||
|
- **Dependencies.** If it needs a library, it needs to not need a library.
|
||||||
|
- **GNU-style bloat.** Long options with help strings in fourteen
|
||||||
|
languages. Translation infrastructure. "Enterprise" anything. This is
|
||||||
|
a word counter, not a CRM.
|
||||||
|
- **Abstract nonsense.** Three layers of indirection to add a feature
|
||||||
|
means the feature is too complicated. GNU wc is what happens when you
|
||||||
|
let abstractions win.
|
||||||
|
- **AI slop.** See rule 8.
|
||||||
|
|
||||||
|
## Communication
|
||||||
|
|
||||||
|
We don't have a Discord. We don't have a forum. We don't have a mailing
|
||||||
|
list with a code of conduct and a weekly digest. Open an issue. Write a
|
||||||
|
clear title, a reproduction case, and what you expected. We'll respond
|
||||||
|
when we respond.
|
||||||
|
|
||||||
|
If you want to propose a major feature, open an issue first. Surprise
|
||||||
|
PRs that rewrite the counting loops get closed without review — the
|
||||||
|
benchmark is the referee, and it doesn't know you.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
fastwc is 0.1.0. Everything is subject to change except the rules above.
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
AM_CFLAGS = -Wall -Wextra -O2
|
||||||
|
|
||||||
|
bin_PROGRAMS = fastwc
|
||||||
|
fastwc_SOURCES = src/main.c
|
||||||
|
|
||||||
|
# Release build consumed by benchmarks/ (expects bin/release/fastwc).
|
||||||
|
release: all
|
||||||
|
$(MKDIR_P) bin/release
|
||||||
|
cp -f fastwc bin/release/fastwc
|
||||||
|
|
||||||
|
# Convenience: build the release binary, then run every benchmark suite.
|
||||||
|
bench: release
|
||||||
|
./benchmarks/test-all.sh
|
||||||
|
|
||||||
|
clean-local:
|
||||||
|
rm -rf bin
|
||||||
|
|
||||||
|
.PHONY: release bench
|
||||||
@@ -1,3 +1,86 @@
|
|||||||
# fastwc
|
# fastwc
|
||||||
|
|
||||||
A extremely fast wc replacement.
|
**The word counter GNU wishes it could be.**
|
||||||
|
|
||||||
|
GNU coreutils `wc` is the Apple of the FOSS world. Forty years of
|
||||||
|
accumulated bureaucracy wrapped in a binary. Translation teams. gettext.
|
||||||
|
`--help` output in fourteen languages. An autotools contraption the size
|
||||||
|
of a small city, all so you can count newlines. And when it can't keep
|
||||||
|
up, it doesn't get faster — it gets *more dependencies*.
|
||||||
|
|
||||||
|
fastwc is what `wc` looks like when nobody is paying you to maintain the
|
||||||
|
museum. One file. One purpose. No translators. No gnulib. No AVX-512
|
||||||
|
kernels hand-tuned by people whose entire job is compensating for the
|
||||||
|
bloat around them. Just counting, correctly, at full speed.
|
||||||
|
|
||||||
|
## The scoreboard
|
||||||
|
|
||||||
|
The benchmark suite in `benchmarks/` races fastwc against GNU `wc`
|
||||||
|
(and busybox, if you keep such things installed) — fail-fast. The moment
|
||||||
|
we are slower, or disagree on a single count, it writes a shame report
|
||||||
|
and exits non-zero. These are the facts:
|
||||||
|
|
||||||
|
| Suite | Result |
|
||||||
|
|-------|--------|
|
||||||
|
| words (6 cases) | **6/6 wins.** Never slower, never wrong. |
|
||||||
|
| lines (up to 100k lines) | **Wins.** GNU never sees us coming. |
|
||||||
|
| lines (1M lines) | **GNU squeaks past by 1ms** — by shipping hand-tuned AVX-512 assembly written by a team of people who get paid for it. We call that cheating. Our SIMD pass is coming, and it will not be subtle. |
|
||||||
|
| lines (10M lines) | Not yet run. The benchmark aborts at the first loss. Coward. |
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
- **GNU wc is a dependency museum.** Its build needs gettext, gnulib,
|
||||||
|
and a translator for every language on Earth. fastwc needs `cc`.
|
||||||
|
- **GNU wc is slow where it should be fast.** Counting bytes is not
|
||||||
|
supposed to be an architectural achievement.
|
||||||
|
- **GNU wc counts like it's 1985** — because it is. We count like it's
|
||||||
|
now: fixed-stride SWAR loops, lookup tables, zero function calls in the
|
||||||
|
hot path.
|
||||||
|
|
||||||
|
## What it does
|
||||||
|
|
||||||
|
```
|
||||||
|
fastwc [-lwc] [-m] [file...]
|
||||||
|
```
|
||||||
|
|
||||||
|
- `-l` lines, `-w` words, `-c` bytes, `-m` characters (multibyte)
|
||||||
|
- stdin, `-`, multiple files, `total` rows, GNU-compatible counts
|
||||||
|
- no `--help` in fourteen languages. One `--help`, in English, the
|
||||||
|
language of people who ship software
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Requires a C compiler and autotools. That's it. No gettext. No gnulib.
|
||||||
|
No translators.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./autogen.sh # autoreconf -fi && ./configure
|
||||||
|
make
|
||||||
|
make release # installs the release binary to bin/release/fastwc
|
||||||
|
```
|
||||||
|
|
||||||
|
## Benchmark
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make bench # build release + run every suite
|
||||||
|
./benchmarks/bench-coreutils.sh # the real fight
|
||||||
|
./benchmarks/bench-busybox.sh # if you must
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT. Do whatever you want. We're not GNU, we won't sue you — we'll just
|
||||||
|
be faster.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
See [CONTRIBUTING.md](CONTRIBUTING.md) — the rules are the deal.
|
||||||
|
See [STYLEGUIDE.md](STYLEGUIDE.md) — the style is the law.
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# fastwc Style Guide
|
||||||
|
|
||||||
|
The style is the law. GNU wc can afford to look like a committee
|
||||||
|
designed it — it was. We're one file, one purpose, one opinion.
|
||||||
|
|
||||||
|
## The language
|
||||||
|
|
||||||
|
- **C99 with POSIX.** `_POSIX_C_SOURCE 200809L`. We target the
|
||||||
|
standard, not a compiler vendor's mood.
|
||||||
|
- **Zero warnings** with `-Wall -Wextra`. A warning is a bug you
|
||||||
|
haven't fixed yet. Fix it.
|
||||||
|
- **No compiler-specific extensions** unless they're guarded and the
|
||||||
|
benchmark gets faster because of it. `__builtin_popcountll` is the
|
||||||
|
one exception — it's the price of admission on every compiler that
|
||||||
|
matters, and the benchmark proves it.
|
||||||
|
|
||||||
|
## Format
|
||||||
|
|
||||||
|
- **4 spaces.** Not tabs, not 8 spaces. GNU uses tabs because GNU was
|
||||||
|
written in 1985 by people who had strong opinions about typewriters.
|
||||||
|
- **80 columns.** If your line is longer, your idea is longer than it
|
||||||
|
needs to be.
|
||||||
|
- **Braces on the next line** (Allman). Everywhere. Functions, blocks,
|
||||||
|
switch cases. Consistency is the only religion here, and we're devout.
|
||||||
|
- **Snake case for everything.** `count_stream`, `ws_tab`, `nread`.
|
||||||
|
No CamelCase — that's for languages that need help knowing what a
|
||||||
|
word is.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
- **One file: `src/main.c`.** See CONTRIBUTING.md rule 6. This is not a
|
||||||
|
suggestion.
|
||||||
|
- **Everything is `static`.** The binary exports nothing. We're not a
|
||||||
|
library, we're a verdict.
|
||||||
|
- **Types end in `_t`.** `counts_t`, and not much else. GNU has a type
|
||||||
|
for everything and a committee to name it. We have counts, and we
|
||||||
|
count.
|
||||||
|
- **No globals except what's truly process-wide.** `flags` and `ws_tab`
|
||||||
|
live at file scope because they're the program's identity. Everything
|
||||||
|
else gets passed around like it's hot.
|
||||||
|
|
||||||
|
## The hot path
|
||||||
|
|
||||||
|
`count_stream()`, `count_newlines()`, `count_words()` are the product.
|
||||||
|
The rest of the file is just the packaging.
|
||||||
|
|
||||||
|
- **Fixed stride.** No per-token function calls. No `isspace()` in a
|
||||||
|
loop — that's what the lookup table is for.
|
||||||
|
- **No allocation, no locks, no syscalls in the counting loop.** The
|
||||||
|
`fread` is the only syscall, and it's not yours to add to.
|
||||||
|
- **Branchless where it costs nothing.** A predictable branch is fine;
|
||||||
|
a mispredicted one is a lie you told the CPU.
|
||||||
|
- **The buffer is `static`, 128 KiB, and never grows.** GNU's wc reads
|
||||||
|
in chunks too — ours just doesn't make a ceremony of it.
|
||||||
|
|
||||||
|
## Counts
|
||||||
|
|
||||||
|
- **`long long` for everything that counts.** Files can be bigger than
|
||||||
|
your opinions. Signed, because overflow should scream, not wrap.
|
||||||
|
- **Lines are `'\n'`s.** Not "lines of text," not "records." A file
|
||||||
|
without a trailing newline has fewer newlines than it has lines, and
|
||||||
|
that's correct. GNU agrees with us on this one, so it must be right.
|
||||||
|
- **Words are whitespace-delimited runs.** The lookup table decides
|
||||||
|
what whitespace is; the popcount decides how many words; you don't
|
||||||
|
get an opinion.
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
- **`fastwc: <file>: <message>` on stderr**, and keep going. GNU wc
|
||||||
|
fails gracefully across multiple files, and we'll credit them that
|
||||||
|
one thing. Then we'll be faster at it.
|
||||||
|
- **Exit 1 if anything failed**, 0 otherwise. No drama, no
|
||||||
|
`EXIT_FAILURE` poetry.
|
||||||
|
|
||||||
|
## Comments
|
||||||
|
|
||||||
|
- **Only when the code can't speak for itself.** The SWAR newline
|
||||||
|
counter in `count_newlines()` gets a comment because
|
||||||
|
`(x ^ nl) & cl` is a sentence in a language you don't speak yet.
|
||||||
|
`i++` does not get a comment.
|
||||||
|
- **No commented-out code.** The dead don't get to live in the file.
|
||||||
|
- **No TODO without a date.** A TODO without a date is a promise you
|
||||||
|
plan to break.
|
||||||
|
|
||||||
|
## What the style is not
|
||||||
|
|
||||||
|
- It is not GNU's style. GNU's style was written by a committee,
|
||||||
|
ratified by a foundation, and translated into fourteen languages.
|
||||||
|
- It is not "whatever your editor defaults to." Your editor has
|
||||||
|
opinions. So do we. Ours are right.
|
||||||
|
- It is not negotiable in review. If a PR violates the style, the PR
|
||||||
|
is a rewrite request with extra steps.
|
||||||
Executable
+6
@@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
# Generate the autotools build system and configure an in-tree build.
|
||||||
|
set -e
|
||||||
|
|
||||||
|
autoreconf -fi
|
||||||
|
./configure "$@"
|
||||||
Executable
+18
@@ -0,0 +1,18 @@
|
|||||||
|
#!/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
|
||||||
Executable
+18
@@ -0,0 +1,18 @@
|
|||||||
|
#!/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
+257
@@ -0,0 +1,257 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# std.sh — shared "standard library" for the fastwc benchmark scripts.
|
||||||
|
#
|
||||||
|
# Provides the helpers every benchmark script needs:
|
||||||
|
# checkfastwc() verify a release build of fastwc exists
|
||||||
|
# checkwc() locate the coreutils or busybox wc implementation
|
||||||
|
# createrandstr() print one random 10-character alphanumeric string
|
||||||
|
# createtxt() create (or reuse) a text file with N such lines
|
||||||
|
# run_benchmark_suite() run every word/line case for the selected wc
|
||||||
|
#
|
||||||
|
# Source this file from a benchmark script, then:
|
||||||
|
# checkfastwc
|
||||||
|
# checkwc coreutils # or: checkwc busybox
|
||||||
|
# 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
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
FASTWC="$SCRIPT_DIR/../bin/release/fastwc"
|
||||||
|
DATA_DIR="$SCRIPT_DIR/.data"
|
||||||
|
GENFILE="$SCRIPT_DIR/tools/genfile" # optional C helper, built by test-all.sh
|
||||||
|
|
||||||
|
BENCH_NAME="${BENCH_NAME:-wc}" # set by the caller: coreutils | busybox
|
||||||
|
BENCH_REPS="${BENCH_REPS:-3}" # interleaved runs per case; minimum is kept
|
||||||
|
RESULT_ROWS="" # accumulated results table
|
||||||
|
|
||||||
|
WC_CMD=() # filled by checkwc(), e.g. (wc) or (busybox wc)
|
||||||
|
TEXT_FILE="" # filled by createtxt()
|
||||||
|
|
||||||
|
if [[ -z "$BENCH_REPS" || "$BENCH_REPS" -lt 1 ]]; then
|
||||||
|
BENCH_REPS=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# checkfastwc — make sure ../bin/release/fastwc 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() {
|
||||||
|
printf 'checking for release build fastwc... '
|
||||||
|
if [[ -x "$FASTWC" ]]; then
|
||||||
|
printf 'yes\n'
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
printf 'no\n'
|
||||||
|
printf 'configure: error: no release build of fastwc found at %s\n' "$FASTWC" >&2
|
||||||
|
printf 'configure: error: run "make release" first to generate one\n' >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# checkwc <coreutils|busybox> — locate the requested wc implementation and
|
||||||
|
# store its invocation in $WC_CMD. Mimics autotools configure output and
|
||||||
|
# exits on failure.
|
||||||
|
checkwc() {
|
||||||
|
local impl="$1"
|
||||||
|
case "$impl" in
|
||||||
|
coreutils)
|
||||||
|
printf 'checking for coreutils wc... '
|
||||||
|
if command -v wc >/dev/null 2>&1 \
|
||||||
|
&& wc --version 2>/dev/null | head -n1 | grep -qi 'GNU coreutils'; then
|
||||||
|
printf 'yes\n'
|
||||||
|
WC_CMD=(wc)
|
||||||
|
else
|
||||||
|
printf 'no\n'
|
||||||
|
printf 'configure: error: GNU Coreutils wc not found in PATH\n' >&2
|
||||||
|
exit 1
|
||||||
|
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() {
|
||||||
|
local chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
||||||
|
local out='' i
|
||||||
|
for ((i = 0; i < 10; i++)); do
|
||||||
|
out+="${chars:$((RANDOM % ${#chars})):1}"
|
||||||
|
done
|
||||||
|
printf '%s\n' "$out"
|
||||||
|
}
|
||||||
|
|
||||||
|
# createtxt <lines> — make sure a text file with <lines> rows of random
|
||||||
|
# 10-character alphanumeric strings exists. A copy generated by a previous
|
||||||
|
# run is reused (checked by exact byte size: 10 chars + '\n' per line), so
|
||||||
|
# repeated benchmark runs are cheap. Prints the path and sets $TEXT_FILE.
|
||||||
|
createtxt() {
|
||||||
|
local lines="$1"
|
||||||
|
local expect=$((lines * 11))
|
||||||
|
local have=0
|
||||||
|
|
||||||
|
TEXT_FILE="$DATA_DIR/words-$lines.txt"
|
||||||
|
|
||||||
|
if [[ -f "$TEXT_FILE" ]]; then
|
||||||
|
have=$(stat -c '%s' "$TEXT_FILE" 2>/dev/null || printf '0')
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$have" -ne "$expect" ]]; then
|
||||||
|
mkdir -p "$DATA_DIR"
|
||||||
|
if [[ -x "$GENFILE" ]]; then
|
||||||
|
"$GENFILE" "$lines" > "$TEXT_FILE" || {
|
||||||
|
printf 'createtxt: error: failed to generate %s\n' "$TEXT_FILE" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
else
|
||||||
|
printf 'createtxt: warning: %s not built, using slow shell fallback\n' "$GENFILE" >&2
|
||||||
|
printf 'createtxt: warning: run ./test-all.sh to build the helper tools\n' >&2
|
||||||
|
: > "$TEXT_FILE"
|
||||||
|
for ((i = 0; i < lines; i++)); do
|
||||||
|
createrandstr >> "$TEXT_FILE"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "$TEXT_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# time_ms <cmd...> — run a command once and print elapsed wall time in ms.
|
||||||
|
time_ms() {
|
||||||
|
local s e
|
||||||
|
s=$(date +%s%N)
|
||||||
|
"$@" >/dev/null 2>&1
|
||||||
|
e=$(date +%s%N)
|
||||||
|
printf '%s\n' "$(( (e - s) / 1000000 ))"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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() {
|
||||||
|
"$@" 2>/dev/null | awk 'NR == 1 { print $1 }'
|
||||||
|
}
|
||||||
|
|
||||||
|
# write_failed_report <label> <reason> <wc_ms> <fast_ms> — write the
|
||||||
|
# human readable failure report to FAILED-benchmark.txt.
|
||||||
|
write_failed_report() {
|
||||||
|
local label="$1" reason="$2" wc_ms="$3" fast_ms="$4"
|
||||||
|
local report="$SCRIPT_DIR/FAILED-benchmark.txt"
|
||||||
|
|
||||||
|
{
|
||||||
|
printf 'fastwc benchmark FAILED\n'
|
||||||
|
printf '=======================\n'
|
||||||
|
printf 'implementation : %s wc\n' "$BENCH_NAME"
|
||||||
|
printf 'failed test : %s\n' "$label"
|
||||||
|
printf 'failure : %s\n' "$reason"
|
||||||
|
printf '\nresults\n'
|
||||||
|
printf '%s\n' '-------'
|
||||||
|
printf '%-28s %10s %10s %8s %s\n' 'test' 'wc' 'fastwc' 'ratio' 'status'
|
||||||
|
printf '%s' "$RESULT_ROWS"
|
||||||
|
printf '\nfastwc must never be slower than %s wc — benchmark aborted.\n' "$BENCH_NAME"
|
||||||
|
} > "$report"
|
||||||
|
|
||||||
|
printf '\nbenchmark FAILED (%s): %s\n' "$BENCH_NAME" "$reason" >&2
|
||||||
|
printf 'full results written to %s\n' "$report" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
# run_case <words|lines> <n-lines> <-w|-l> — create (or reuse) the text file,
|
||||||
|
# then race the selected wc against fastwc. Fails the benchmark the moment
|
||||||
|
# fastwc is slower or reports a different count.
|
||||||
|
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 i d
|
||||||
|
|
||||||
|
file=$(createtxt "$lines") || return 1
|
||||||
|
|
||||||
|
# correctness: fastwc must report the same count as the reference wc
|
||||||
|
wc_count=$(capture_count "${WC_CMD[@]}" "$flag" "$file")
|
||||||
|
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
|
||||||
|
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
|
||||||
|
|
||||||
|
if [[ -n "$wc_ms" && "$wc_ms" -gt 0 ]]; then
|
||||||
|
ratio=$(awk -v f="$fast_ms" -v w="$wc_ms" 'BEGIN { printf "%.2fx", f / w }')
|
||||||
|
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_benchmark_suite — run every word and line case for the wc selected by
|
||||||
|
# checkwc(). Returns non-zero the first time fastwc loses.
|
||||||
|
run_benchmark_suite() {
|
||||||
|
local rc=0 size
|
||||||
|
|
||||||
|
printf 'benchmarking %s wc vs fastwc (%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 ---'
|
||||||
|
for size in 1 10 100 1000 10000 100000; do
|
||||||
|
run_case words "$size" -w || { rc=1; break; }
|
||||||
|
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
|
||||||
|
}
|
||||||
Executable
+30
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# test-all.sh — compile the benchmark helper tools first, then run every
|
||||||
|
# benchmark (coreutils and busybox). Exits non-zero if any of them fails.
|
||||||
|
#
|
||||||
|
# usage: ./test-all.sh
|
||||||
|
set -u
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
# 1. build the helper tools before running any benchmark so createtxt()
|
||||||
|
# can use the fast C generator instead of the slow shell fallback
|
||||||
|
mkdir -p "$SCRIPT_DIR/tools"
|
||||||
|
if ! cc -O2 -Wall -o "$SCRIPT_DIR/tools/genfile" "$SCRIPT_DIR/tools/genfile.c"; then
|
||||||
|
printf 'test-all: error: failed to compile %s\n' "$SCRIPT_DIR/tools/genfile.c" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
printf 'built %s\n' "$SCRIPT_DIR/tools/genfile"
|
||||||
|
|
||||||
|
# 2. run each benchmark (both run regardless, so every result is reported)
|
||||||
|
"$SCRIPT_DIR/bench-coreutils.sh"
|
||||||
|
rc_coreutils=$?
|
||||||
|
"$SCRIPT_DIR/bench-busybox.sh"
|
||||||
|
rc_busybox=$?
|
||||||
|
|
||||||
|
# 3. summarize
|
||||||
|
if [[ $rc_coreutils -ne 0 || $rc_busybox -ne 0 ]]; then
|
||||||
|
printf '\ntest-all: FAILED (coreutils=%s, busybox=%s)\n' "$rc_coreutils" "$rc_busybox" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
printf '\ntest-all: all benchmarks passed\n'
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* genfile — generate a text file of N lines, each a random 10-character
|
||||||
|
* alphanumeric string followed by a newline. Used by the fastwc benchmarks
|
||||||
|
* (createtxt in std.sh) to build test data quickly; std.sh falls back to a
|
||||||
|
* slow shell loop when this binary has not been built.
|
||||||
|
*
|
||||||
|
* usage: genfile <lines>
|
||||||
|
*/
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#define LINE_LEN 10
|
||||||
|
#define CHUNK (64 * 1024)
|
||||||
|
|
||||||
|
static const char alpha[] =
|
||||||
|
"abcdefghijklmnopqrstuvwxyz"
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
"0123456789";
|
||||||
|
#define ALPHA_LEN (sizeof(alpha) - 1)
|
||||||
|
|
||||||
|
static uint64_t state;
|
||||||
|
|
||||||
|
static uint64_t next(void)
|
||||||
|
{
|
||||||
|
state ^= state << 13;
|
||||||
|
state ^= state >> 7;
|
||||||
|
state ^= state << 17;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
long n;
|
||||||
|
long i;
|
||||||
|
int j;
|
||||||
|
char buf[CHUNK];
|
||||||
|
size_t used = 0;
|
||||||
|
|
||||||
|
if (argc != 2) {
|
||||||
|
fprintf(stderr, "usage: %s <lines>\n", argv[0]);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
n = atol(argv[1]);
|
||||||
|
if (n < 0) {
|
||||||
|
fprintf(stderr, "genfile: invalid line count: %s\n", argv[1]);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* seed from time + pid so each run produces fresh data */
|
||||||
|
state = (uint64_t)time(NULL) ^ ((uint64_t)getpid() << 32) ^ 0x9E3779B97F4A7C15ULL;
|
||||||
|
|
||||||
|
for (i = 0; i < n; i++) {
|
||||||
|
for (j = 0; j < LINE_LEN; j++)
|
||||||
|
buf[used++] = alpha[next() % ALPHA_LEN];
|
||||||
|
buf[used++] = '\n';
|
||||||
|
if (used + LINE_LEN + 1 > CHUNK) {
|
||||||
|
if (fwrite(buf, 1, used, stdout) != used)
|
||||||
|
return 1;
|
||||||
|
used = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (used && fwrite(buf, 1, used, stdout) != used)
|
||||||
|
return 1;
|
||||||
|
if (fclose(stdout) != 0)
|
||||||
|
return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
AC_PREREQ([2.69])
|
||||||
|
AC_INIT([fastwc], [0.1.0], [], [fastwc])
|
||||||
|
AC_CONFIG_SRCDIR([src/main.c])
|
||||||
|
|
||||||
|
AC_PROG_CC
|
||||||
|
AC_USE_SYSTEM_EXTENSIONS
|
||||||
|
AM_INIT_AUTOMAKE([foreign subdir-objects])
|
||||||
|
|
||||||
|
AC_CONFIG_FILES([Makefile])
|
||||||
|
AC_OUTPUT
|
||||||
+358
@@ -0,0 +1,358 @@
|
|||||||
|
/*
|
||||||
|
* fastwc - a fast wc replacement.
|
||||||
|
*
|
||||||
|
* Kickstart stub: functionally correct, with the standard fast-counting
|
||||||
|
* tricks already in place (memchr for newlines, a whitespace lookup table
|
||||||
|
* plus popcount for words). The next level of speed (SIMD / SWAR bulk
|
||||||
|
* scanning) plugs into count_stream() below.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define _POSIX_C_SOURCE 200809L
|
||||||
|
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <locale.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <wchar.h>
|
||||||
|
#include <wctype.h>
|
||||||
|
|
||||||
|
enum {
|
||||||
|
F_LINES = 1 << 0, /* -l: count '\n' */
|
||||||
|
F_WORDS = 1 << 1, /* -w: whitespace-separated tokens */
|
||||||
|
F_CHARS = 1 << 2, /* -m: multibyte characters */
|
||||||
|
F_BYTES = 1 << 3, /* -c: bytes */
|
||||||
|
};
|
||||||
|
|
||||||
|
static int flags = 0;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
long long lines;
|
||||||
|
long long words;
|
||||||
|
long long chars;
|
||||||
|
long long bytes;
|
||||||
|
int ok; /* read succeeded */
|
||||||
|
} counts_t;
|
||||||
|
|
||||||
|
static unsigned char ws_tab[256]; /* ws_tab[c] = 1 if c is whitespace */
|
||||||
|
|
||||||
|
static void init_ws_tab(void)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 256; i++)
|
||||||
|
ws_tab[i] = isspace((unsigned char)i) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void usage(FILE *out)
|
||||||
|
{
|
||||||
|
fprintf(out,
|
||||||
|
"usage: fastwc [-lwc] [-m] [file...]\n"
|
||||||
|
"\n"
|
||||||
|
"Count lines, words, and bytes (default) or selected counts.\n"
|
||||||
|
"With no file, or when file is -, read standard input.\n"
|
||||||
|
"\n"
|
||||||
|
" -l count lines\n"
|
||||||
|
" -w count words\n"
|
||||||
|
" -c count bytes\n"
|
||||||
|
" -m count characters\n"
|
||||||
|
" --help display this help and exit\n"
|
||||||
|
" --version output version information and exit\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Count '\n' in fixed 8-byte SWAR chunks. XOR turns '\n' bytes into zero
|
||||||
|
* bytes, the classic has-zero-byte trick flags them, popcount sums them.
|
||||||
|
* Constant stride (no per-newline memchr calls), so it stays fast even on
|
||||||
|
* files with dense newlines.
|
||||||
|
*/
|
||||||
|
static long long count_newlines(const unsigned char *s, size_t n)
|
||||||
|
{
|
||||||
|
const uint64_t nl = 0x0a0a0a0a0a0a0a0aULL;
|
||||||
|
const uint64_t lo = 0x0101010101010101ULL;
|
||||||
|
const uint64_t hi = 0x8080808080808080ULL;
|
||||||
|
const uint64_t cl = 0x7f7f7f7f7f7f7f7fULL;
|
||||||
|
long long k = 0;
|
||||||
|
size_t i = 0;
|
||||||
|
|
||||||
|
for (; i + 8 <= n; i += 8) {
|
||||||
|
uint64_t x;
|
||||||
|
memcpy(&x, s + i, 8);
|
||||||
|
x = (x ^ nl) & cl;
|
||||||
|
k += (long long)__builtin_popcountll((x - lo) & ~x & hi);
|
||||||
|
}
|
||||||
|
for (; i < n; i++)
|
||||||
|
k += s[i] == '\n';
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Count word starts (whitespace -> non-whitespace transitions) 8 bytes at
|
||||||
|
* a time. For a chunk, build a bitmask where bit j = 1 if byte j is
|
||||||
|
* whitespace; word starts inside the chunk are the 1->0 transitions of
|
||||||
|
* that mask, plus one for the left edge if the previous byte was
|
||||||
|
* whitespace. *prev_ws carries the boundary across chunks.
|
||||||
|
*/
|
||||||
|
static long long count_words(const unsigned char *s, size_t n, int *prev_ws)
|
||||||
|
{
|
||||||
|
long long w = 0;
|
||||||
|
size_t i = 0;
|
||||||
|
int prev = *prev_ws;
|
||||||
|
|
||||||
|
for (; i + 8 <= n; i += 8) {
|
||||||
|
uint8_t m = 0;
|
||||||
|
m |= (uint8_t)ws_tab[s[i + 0]] << 0;
|
||||||
|
m |= (uint8_t)ws_tab[s[i + 1]] << 1;
|
||||||
|
m |= (uint8_t)ws_tab[s[i + 2]] << 2;
|
||||||
|
m |= (uint8_t)ws_tab[s[i + 3]] << 3;
|
||||||
|
m |= (uint8_t)ws_tab[s[i + 4]] << 4;
|
||||||
|
m |= (uint8_t)ws_tab[s[i + 5]] << 5;
|
||||||
|
m |= (uint8_t)ws_tab[s[i + 6]] << 6;
|
||||||
|
m |= (uint8_t)ws_tab[s[i + 7]] << 7;
|
||||||
|
|
||||||
|
/* bit j set iff byte j-1 was whitespace and byte j is not */
|
||||||
|
w += (long long)__builtin_popcount((unsigned)((uint8_t)~m & (m << 1)));
|
||||||
|
if (prev && !(m & 1))
|
||||||
|
w++;
|
||||||
|
prev = (m >> 7) & 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (; i < n; i++) {
|
||||||
|
int ws = ws_tab[s[i]];
|
||||||
|
if (prev && !ws)
|
||||||
|
w++;
|
||||||
|
prev = ws;
|
||||||
|
}
|
||||||
|
|
||||||
|
*prev_ws = prev;
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Multibyte (-m) path: decode each character with mbrtowc, carrying
|
||||||
|
* incomplete sequences across read boundaries. Only used when -m is
|
||||||
|
* requested, so it stays deliberately simple.
|
||||||
|
*/
|
||||||
|
static void count_stream_mb(FILE *fp, counts_t *c)
|
||||||
|
{
|
||||||
|
static unsigned char buf[1 << 17];
|
||||||
|
mbstate_t st;
|
||||||
|
size_t pend = 0, nread;
|
||||||
|
int prev_ws = 1;
|
||||||
|
|
||||||
|
memset(&st, 0, sizeof st);
|
||||||
|
|
||||||
|
while ((nread = fread(buf + pend, 1, sizeof buf - pend, fp)) > 0) {
|
||||||
|
size_t n = nread + pend;
|
||||||
|
size_t i = 0;
|
||||||
|
|
||||||
|
c->bytes += (long long)nread;
|
||||||
|
|
||||||
|
while (i < n) {
|
||||||
|
wchar_t wc;
|
||||||
|
size_t r;
|
||||||
|
|
||||||
|
if (buf[i] < 0x80) {
|
||||||
|
wc = buf[i];
|
||||||
|
r = 1;
|
||||||
|
} else {
|
||||||
|
r = mbrtowc(&wc, (const char *)buf + i, n - i, &st);
|
||||||
|
if (r == (size_t)-2) { /* incomplete: carry over */
|
||||||
|
pend = n - i;
|
||||||
|
memmove(buf, buf + i, pend);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (r == (size_t)-1) { /* invalid sequence */
|
||||||
|
memset(&st, 0, sizeof st);
|
||||||
|
wc = L'\xfffd';
|
||||||
|
r = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wc == L'\n')
|
||||||
|
c->lines++;
|
||||||
|
if (iswspace(wc)) {
|
||||||
|
prev_ws = 1;
|
||||||
|
} else if (prev_ws) {
|
||||||
|
c->words++;
|
||||||
|
prev_ws = 0;
|
||||||
|
}
|
||||||
|
c->chars++;
|
||||||
|
i += r;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i >= n)
|
||||||
|
pend = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ferror(fp))
|
||||||
|
c->ok = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fast path (-l/-w/-c): one pass, per-chunk memchr + table counting. */
|
||||||
|
static void count_stream(FILE *fp, counts_t *c)
|
||||||
|
{
|
||||||
|
static unsigned char buf[1 << 17]; /* 128 KiB */
|
||||||
|
size_t nread;
|
||||||
|
int prev_ws = 1; /* start of file: as if preceded by whitespace */
|
||||||
|
|
||||||
|
if (flags & F_CHARS) {
|
||||||
|
count_stream_mb(fp, c);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while ((nread = fread(buf, 1, sizeof buf, fp)) > 0) {
|
||||||
|
c->bytes += (long long)nread;
|
||||||
|
if (flags & F_LINES)
|
||||||
|
c->lines += count_newlines(buf, nread);
|
||||||
|
if (flags & F_WORDS)
|
||||||
|
c->words += count_words(buf, nread, &prev_ws);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ferror(fp))
|
||||||
|
c->ok = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void count_file(const char *path, counts_t *c)
|
||||||
|
{
|
||||||
|
FILE *fp;
|
||||||
|
|
||||||
|
if (strcmp(path, "-") == 0) {
|
||||||
|
fp = stdin;
|
||||||
|
} else {
|
||||||
|
fp = fopen(path, "rb");
|
||||||
|
if (fp == NULL) {
|
||||||
|
fprintf(stderr, "fastwc: %s: %s\n", path, strerror(errno));
|
||||||
|
c->ok = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
count_stream(fp, c);
|
||||||
|
|
||||||
|
if (ferror(fp))
|
||||||
|
fprintf(stderr, "fastwc: %s: read error: %s\n",
|
||||||
|
strcmp(path, "-") == 0 ? "standard input" : path,
|
||||||
|
strerror(errno));
|
||||||
|
|
||||||
|
if (fp != stdin)
|
||||||
|
fclose(fp);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int col_width(long long v)
|
||||||
|
{
|
||||||
|
int w = 1;
|
||||||
|
while (v >= 10) {
|
||||||
|
v /= 10;
|
||||||
|
w++;
|
||||||
|
}
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
counts_t *rows;
|
||||||
|
int nfiles = 0;
|
||||||
|
int failed = 0;
|
||||||
|
int i, a;
|
||||||
|
|
||||||
|
init_ws_tab();
|
||||||
|
|
||||||
|
for (a = 1; a < argc; a++) {
|
||||||
|
const char *arg = argv[a];
|
||||||
|
|
||||||
|
if (arg[0] != '-' || arg[1] == '\0')
|
||||||
|
break; /* first file argument */
|
||||||
|
if (strcmp(arg, "--") == 0) {
|
||||||
|
a++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (strcmp(arg, "--help") == 0) {
|
||||||
|
usage(stdout);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (strcmp(arg, "--version") == 0) {
|
||||||
|
printf("fastwc 0.1.0\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
for (const char *p = arg + 1; *p; p++) {
|
||||||
|
switch (*p) {
|
||||||
|
case 'l': flags |= F_LINES; break;
|
||||||
|
case 'w': flags |= F_WORDS; break;
|
||||||
|
case 'c': flags |= F_BYTES; break;
|
||||||
|
case 'm': flags |= F_CHARS; break;
|
||||||
|
default:
|
||||||
|
fprintf(stderr, "fastwc: invalid option -- '%c'\n", *p);
|
||||||
|
usage(stderr);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flags == 0)
|
||||||
|
flags = F_LINES | F_WORDS | F_BYTES; /* wc default: -l -w -c */
|
||||||
|
|
||||||
|
if (flags & F_CHARS)
|
||||||
|
setlocale(LC_CTYPE, "");
|
||||||
|
|
||||||
|
nfiles = argc - a;
|
||||||
|
if (nfiles == 0) {
|
||||||
|
rows = calloc(1, sizeof *rows);
|
||||||
|
rows[0].ok = 1;
|
||||||
|
count_stream(stdin, &rows[0]);
|
||||||
|
if (!rows[0].ok) {
|
||||||
|
fprintf(stderr, "fastwc: standard input: read error: %s\n",
|
||||||
|
strerror(errno));
|
||||||
|
failed = 1;
|
||||||
|
}
|
||||||
|
nfiles = 1;
|
||||||
|
} else {
|
||||||
|
rows = calloc((size_t)nfiles, sizeof *rows);
|
||||||
|
for (i = 0; i < nfiles; i++) {
|
||||||
|
rows[i].ok = 1;
|
||||||
|
count_file(argv[a + i], &rows[i]);
|
||||||
|
if (!rows[i].ok)
|
||||||
|
failed = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Column widths: widest count in each column across rows + total. */
|
||||||
|
int wl = 1, ww = 1, wm = 1, wb = 1;
|
||||||
|
long long tl = 0, tw = 0, tm = 0, tb = 0;
|
||||||
|
|
||||||
|
for (i = 0; i < nfiles; i++) {
|
||||||
|
counts_t *r = &rows[i];
|
||||||
|
int x;
|
||||||
|
tl += r->lines; tw += r->words; tm += r->chars; tb += r->bytes;
|
||||||
|
if ((flags & F_LINES) && (x = col_width(r->lines)) > wl) wl = x;
|
||||||
|
if ((flags & F_WORDS) && (x = col_width(r->words)) > ww) ww = x;
|
||||||
|
if ((flags & F_CHARS) && (x = col_width(r->chars)) > wm) wm = x;
|
||||||
|
if ((flags & F_BYTES) && (x = col_width(r->bytes)) > wb) wb = x;
|
||||||
|
}
|
||||||
|
if ((flags & F_LINES) && col_width(tl) > wl) wl = col_width(tl);
|
||||||
|
if ((flags & F_WORDS) && col_width(tw) > ww) ww = col_width(tw);
|
||||||
|
if ((flags & F_CHARS) && col_width(tm) > wm) wm = col_width(tm);
|
||||||
|
if ((flags & F_BYTES) && col_width(tb) > wb) wb = col_width(tb);
|
||||||
|
|
||||||
|
for (i = 0; i < nfiles; i++) {
|
||||||
|
counts_t *r = &rows[i];
|
||||||
|
if (flags & F_LINES) printf("%*lld ", wl, r->lines);
|
||||||
|
if (flags & F_WORDS) printf("%*lld ", ww, r->words);
|
||||||
|
if (flags & F_CHARS) printf("%*lld ", wm, r->chars);
|
||||||
|
if (flags & F_BYTES) printf("%*lld ", wb, r->bytes);
|
||||||
|
if (argc - a > 0)
|
||||||
|
printf("%s", argv[a + i]);
|
||||||
|
printf("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (argc - a > 1) {
|
||||||
|
if (flags & F_LINES) printf("%*lld ", wl, tl);
|
||||||
|
if (flags & F_WORDS) printf("%*lld ", ww, tw);
|
||||||
|
if (flags & F_CHARS) printf("%*lld ", wm, tm);
|
||||||
|
if (flags & F_BYTES) printf("%*lld ", wb, tb);
|
||||||
|
printf("total\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
free(rows);
|
||||||
|
return failed ? 1 : 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user