A from-scratch C reimplementation of GNU wc(1). Byte-identical output
(parity target: coreutils 9.11), faster on every workload measured. Also
installs as ty, which is easier to type than wc.
GNU wc counts lines with SIMD but counts words with a scalar per-character
loop through the locale machinery. On the default invocation — the one every
user runs — that loop is the whole cost. tally fuses line, word, and byte
counting into one branchless SIMD pass (SSE2/AVX2 on x86-64, NEON on arm64)
that classifies UTF-8 whitespace without decoding, and counts -m characters
as valid-sequence starts — position-independent, so even hostile binary input
stays at vector speed instead of one mbrtoc32() call per character.
Parity is enforced, not aspired to: a golden suite diffs tally against a
pinned GNU wc built from source — stdout, stderr, and exit codes, across
locales and POSIXLY_CORRECT — plus a differential fuzzer and a CI perf
gate that fails if tally is slower anywhere.
hyperfine, 200 MB corpora, page-cache-hot. FreeBSD 15 box: Ryzen-class x86-64 with AVX-512 (which GNU wc uses; tally caps at AVX2). macOS box: Apple Silicon.
| workload | vs wc 9.11 (x86-64) | vs wc 9.11 (arm64) |
|---|---|---|
wc FILE (ASCII) |
22x | 14x |
wc FILE (Cyrillic/CJK) |
123x | 73x |
wc FILE (smart-quote prose) |
15x | 5.7x |
wc FILE (binary) |
39x | 27x |
-m (UTF-8) |
117x | 47x |
-m (binary) |
99x | 44x |
-L (ASCII) |
10x | 5.2x |
-L (UTF-8) |
5.5x | 4.2x |
-l (big file) |
1.0x (read-bound tie) | 2.8-6.1x |
| 10,000 small files | 1.5x | 1.2x |
Homebrew (macOS, Linuxbrew):
brew install tenseleyflow/tap/tally
Arch: a PKGBUILD ships in packaging/. Release tarballs with checksums
are on the releases page.
./configure && gmake # BSD: gmake; Linux/macOS: make works too
gmake test # unit + golden parity + fuzz (builds GNU wc
# 9.11 from source once as the oracle)
gmake install # tally + ty + man pages
Requires a C11 compiler and GNU make. Runs on FreeBSD, Linux (glibc and musl), and macOS. SIMD is compiled per translation unit and selected at runtime; the binary never executes instructions the host lacks.
Anything beyond GNU wc's surface lives under --tally-* and is off by
default. --tally-threads=N (or TAL_THREADS=N) counts large regular
files with N parallel readers, covering -l, -w, and -m. On the dev
box with 4 threads and a 200 MB cached file: -l 23 ms to 13 ms (the
read-bound tie with GNU becomes 1.8x), the default invocation 30 ms to
14 ms, -m on UTF-8 45 ms to 16 ms. Output stays byte-identical; word
and character joins are placed only at positions where no multibyte
sequence or separator can span them.
Same counts, column widths, diagnostics, and exit codes as GNU wc, with
these documented exceptions (doc/tally.1 DEVIATIONS): the program-name
token, --help/--version text, --debug output, untranslated "total",
and one behavioral fix — GNU wc 9.11 miscounts words when adjacent
multibyte spaces split across read boundaries (reported upstream); tally's
counts are independent of read fragmentation.
GPL-3.0-or-later, like the tool it reimplements.