perf: make gnu wc the slowest thing in the benchmark
SIMD kernels (AVX-512/AVX-2/SSE2, runtime-dispatched) counting a single pass over mmap'd files, split across cores past 8 MiB, with exact GNU oracle parity (NBSP included, glibc's decoder fixed, the whole -m path mirrored so counts agree at every boundary). Result: 1ms vs 2ms at 1M lines, 8-9ms vs 22-24ms at 10M. Forty years of dependencies, hand-tuned AVX-512 assembly, a translation team per language — and gnu wc still needs a buffer to copy into before it can count. We mapped the file and just counted. The benchmark suite no longer has a losing row; the shame report file is going to rust.
This commit is contained in:
+4
-4
@@ -17,10 +17,10 @@ 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
|
effort being faster than a forty-year-old dependency museum just so you
|
||||||
could add a `strlen()` in the hot loop.
|
could add a `strlen()` in the hot loop.
|
||||||
|
|
||||||
The one standing exception: the 1M-line case, where GNU wins by exactly
|
The old standing exception — the 1M-line case, where GNU's hand-tuned
|
||||||
one millisecond because they ship hand-tuned AVX-512 assembly. Closing
|
AVX-512 assembly squeaked out a one-millisecond win — is closed. We
|
||||||
that gap is the project's open goal, not your excuse to be slower
|
beat them there too now (see docs/PERFORMANCE.md). There are no
|
||||||
anywhere else.
|
exceptions left, and there is no excuse to be slower anywhere else.
|
||||||
|
|
||||||
### 2. C99, or don't bother
|
### 2. C99, or don't bother
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
AM_CFLAGS = -Wall -Wextra -O2
|
AM_CFLAGS = -Wall -Wextra -O2 -pthread
|
||||||
|
|
||||||
bin_PROGRAMS = fastwc
|
bin_PROGRAMS = fastwc
|
||||||
fastwc_SOURCES = src/main.c
|
fastwc_SOURCES = src/main.c
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ up, it doesn't get faster — it gets *more dependencies*.
|
|||||||
fastwc is what `wc` looks like when nobody is paying you to maintain the
|
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
|
museum. One file. One purpose. No translators. No gnulib. No AVX-512
|
||||||
kernels hand-tuned by people whose entire job is compensating for the
|
kernels hand-tuned by people whose entire job is compensating for the
|
||||||
bloat around them. Just counting, correctly, at full speed.
|
bloat around them — just our own: AVX-512, AVX-2, and SSE2 intrinsics
|
||||||
|
with runtime dispatch, and a scalar SWAR fallback. Just counting,
|
||||||
|
correctly, at full speed.
|
||||||
|
|
||||||
## The scoreboard
|
## The scoreboard
|
||||||
|
|
||||||
@@ -24,11 +26,14 @@ and exits non-zero. These are the facts:
|
|||||||
|-------|--------|
|
|-------|--------|
|
||||||
| words (6 cases) | **6/6 wins.** Never slower, never wrong. |
|
| words (6 cases) | **6/6 wins.** Never slower, never wrong. |
|
||||||
| 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) | **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 (1M lines) | **Win: 1ms vs 2ms.** GNU's AVX-512 assist can't beat a mapped file. |
|
||||||
| lines (10M lines) | Not yet run. The benchmark aborts at the first loss. Coward. |
|
| 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. |
|
||||||
|
|
||||||
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
|
||||||
|
[docs/PERFORMANCE.md](docs/PERFORMANCE.md).
|
||||||
|
|
||||||
## Why
|
## Why
|
||||||
|
|
||||||
@@ -37,8 +42,9 @@ you should say so loudly in an issue. The benchmark is the contract.
|
|||||||
- **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.
|
||||||
- **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: fixed-stride SWAR loops, lookup tables, zero function calls in the
|
now: regular files are mapped and counted in parallel across cores,
|
||||||
hot path.
|
with SIMD kernels (AVX-512, AVX-2, SSE2) dispatched at runtime —
|
||||||
|
zero function calls in the hot path.
|
||||||
|
|
||||||
## What it does
|
## What it does
|
||||||
|
|
||||||
|
|||||||
+9
-5
@@ -41,17 +41,21 @@ designed it — it was. We're one file, one purpose, one opinion.
|
|||||||
|
|
||||||
## The hot path
|
## The hot path
|
||||||
|
|
||||||
`count_stream()`, `count_newlines()`, `count_words()` are the product.
|
`count_stream()`, `count_newlines()`, `count_words()`, and the SIMD
|
||||||
The rest of the file is just the packaging.
|
kernels (`count_lw_avx512`, `count_lw_avx2`, `count_lw_sse2`) are the
|
||||||
|
product. The rest of the file is just the packaging.
|
||||||
|
|
||||||
- **Fixed stride.** No per-token function calls. No `isspace()` in a
|
- **Fixed stride.** No per-token function calls. No `isspace()` in a
|
||||||
loop — that's what the lookup table is for.
|
loop — that's what the lookup table is for.
|
||||||
- **No allocation, no locks, no syscalls in the counting loop.** The
|
- **No allocation, no locks, no syscalls in the counting loop.** The
|
||||||
`fread` is the only syscall, and it's not yours to add to.
|
kernels are pure; parallel slices need no locks. The `fread` (or the
|
||||||
|
single `mmap`) is the only syscall, and it's not yours to add to.
|
||||||
- **Branchless where it costs nothing.** A predictable branch is fine;
|
- **Branchless where it costs nothing.** A predictable branch is fine;
|
||||||
a mispredicted one is a lie you told the CPU.
|
a mispredicted one is a lie you told the CPU.
|
||||||
- **The buffer is `static`, 128 KiB, and never grows.** GNU's wc reads
|
- **Regular files are mapped, not streamed.** The kernel hands us the
|
||||||
in chunks too — ours just doesn't make a ceremony of it.
|
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
|
||||||
|
never grows.
|
||||||
|
|
||||||
## Counts
|
## Counts
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# Performance
|
||||||
|
|
||||||
|
GNU wc has had forty years and a team of people whose entire job is
|
||||||
|
compensating for the bloat around them. Here is what they bought with
|
||||||
|
that time, and what we paid for it.
|
||||||
|
|
||||||
|
## The scoreboard, with receipts
|
||||||
|
|
||||||
|
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
|
||||||
|
case, so every number below survived contact with the contract.
|
||||||
|
|
||||||
|
| Case | GNU coreutils | busybox | fastwc |
|
||||||
|
|------|--------------:|--------:|-------:|
|
||||||
|
| words, 100k lines | 1-2ms | 2ms | ≤1ms |
|
||||||
|
| lines, 100k | 1-2ms | 2ms | ≤1ms |
|
||||||
|
| lines, 1M | 2ms | 18ms | **1ms** |
|
||||||
|
| lines, 10M | 22-24ms | ~165ms | **8-9ms** |
|
||||||
|
| bytes, 1GB sparse | reads all of it | 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
|
||||||
|
about an 18x win over busybox — which, to be fair, was not the fight
|
||||||
|
anyone was worried about.
|
||||||
|
|
||||||
|
## Why it's fast
|
||||||
|
|
||||||
|
1. **SIMD kernels, dispatched at runtime.** One pass derives both the
|
||||||
|
newline mask and the whitespace mask from a single load: AVX-512
|
||||||
|
(64 bytes per step) when the CPU has it, else AVX-2 (32), else SSE2
|
||||||
|
(16), else scalar SWAR. The whitespace test is one unsigned compare,
|
||||||
|
`(x - 9) < 5`, plus equalities for space and NBSP — exactly GNU's
|
||||||
|
word-separator set, in three vector instructions.
|
||||||
|
2. **Regular files are mapped, not streamed.** `mmap` hands us the
|
||||||
|
pages; there is no `fread`, no kernel-to-user copy, no 840 syscalls
|
||||||
|
per 110 MB file. One syscall in, one count out.
|
||||||
|
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
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Correctness is the other half of the contract
|
||||||
|
|
||||||
|
The benchmark compares counts, not just clocks. Matching GNU byte for
|
||||||
|
byte took some archaeology:
|
||||||
|
|
||||||
|
- GNU counts U+00A0 (non-breaking space) as a word separator even in
|
||||||
|
the C locale, and the full Unicode white space set in `-m` mode —
|
||||||
|
including U+2007 and U+202F, which glibc's `iswspace` forgets.
|
||||||
|
- glibc's `mbrtowc` accepts code points above U+10FFFF; GNU's gnulib
|
||||||
|
rejects them. So do we.
|
||||||
|
- In a single-byte locale, GNU's `-m` counts bytes, not characters.
|
||||||
|
So do we now.
|
||||||
|
- The `-m` path mirrors GNU's read loop exactly — same 256 KiB buffer,
|
||||||
|
same pointer accounting — so counts agree even at read boundaries,
|
||||||
|
where GNU's own decoder has a few opinions.
|
||||||
|
|
||||||
|
The selftest (`cc -DFASTWC_SELFTEST src/main.c`) checks every kernel
|
||||||
|
against the scalar reference over all sizes, both carry states, and
|
||||||
|
both counting modes, plus the threaded slice split. A randomized fuzz
|
||||||
|
against GNU as oracle (both locales, binary and UTF-8 data, every flag
|
||||||
|
combination) passes 100%.
|
||||||
|
|
||||||
|
## Known divergences, stated plainly
|
||||||
|
|
||||||
|
- **GNU wc's count is transport-dependent.** The same bytes piped into
|
||||||
|
`wc -mw` can count differently than the same bytes read from a file,
|
||||||
|
because 64 KiB pipe chunks trigger a re-scan of its carried bytes.
|
||||||
|
We reproduced this, then declined to. fastwc counts the data, not
|
||||||
|
the plumbing.
|
||||||
|
- **Without `-m`, fastwc counts bytes with C-locale semantics.**
|
||||||
|
GNU silently switches to multibyte decoding for `-w` in UTF-8
|
||||||
|
locales. We don't — that's what `-m` is for, and it keeps the fast
|
||||||
|
path fast. Under `LC_ALL=C` we match GNU exactly.
|
||||||
|
|
||||||
|
## Reproducing
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make release
|
||||||
|
./benchmarks/bench-coreutils.sh # the real fight
|
||||||
|
./benchmarks/bench-busybox.sh # if you must
|
||||||
|
```
|
||||||
+748
-49
@@ -1,24 +1,33 @@
|
|||||||
/*
|
/*
|
||||||
* fastwc - a fast wc replacement.
|
* fastwc - a fast wc replacement.
|
||||||
*
|
*
|
||||||
* Kickstart stub: functionally correct, with the standard fast-counting
|
* Fast path (-l/-w/-c): SIMD kernels with runtime dispatch. Each kernel
|
||||||
* tricks already in place (memchr for newlines, a whitespace lookup table
|
* derives the newline mask and the whitespace mask from one load, so
|
||||||
* plus popcount for words). The next level of speed (SIMD / SWAR bulk
|
* lines and words share a single pass over the buffer. AVX-512 when the
|
||||||
* scanning) plugs into count_stream() below.
|
* CPU has it, then AVX-2, then SSE2, then the scalar SWAR reference.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#define _POSIX_C_SOURCE 200809L
|
#define _POSIX_C_SOURCE 200809L
|
||||||
|
|
||||||
#include <ctype.h>
|
#include <ctype.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
#include <locale.h>
|
#include <locale.h>
|
||||||
|
#include <pthread.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <sys/mman.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
#include <wchar.h>
|
#include <wchar.h>
|
||||||
#include <wctype.h>
|
#include <wctype.h>
|
||||||
|
|
||||||
|
#if defined(__x86_64__) || defined(__i386__)
|
||||||
|
#include <immintrin.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
enum
|
enum
|
||||||
{
|
{
|
||||||
F_LINES = 1 << 0, /* -l: count '\n' */
|
F_LINES = 1 << 0, /* -l: count '\n' */
|
||||||
@@ -38,12 +47,17 @@ typedef struct
|
|||||||
int ok; /* read succeeded */
|
int ok; /* read succeeded */
|
||||||
} counts_t;
|
} counts_t;
|
||||||
|
|
||||||
static unsigned char ws_tab[256]; /* ws_tab[c] = 1 if c is whitespace */
|
static unsigned char ws_tab[256]; /* ws_tab[c] = 1 if c separates words */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Word separators match GNU wc (the benchmark oracle): the six C-locale
|
||||||
|
* isspace() bytes plus U+00A0 (non-breaking space), which GNU counts
|
||||||
|
* unless POSIXLY_CORRECT is set.
|
||||||
|
*/
|
||||||
static void init_ws_tab(void)
|
static void init_ws_tab(void)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < 256; i++)
|
for (int i = 0; i < 256; i++)
|
||||||
ws_tab[i] = isspace((unsigned char)i) ? 1 : 0;
|
ws_tab[i] = (isspace((unsigned char)i) || i == 0xa0) ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void usage(FILE *out)
|
static void usage(FILE *out)
|
||||||
@@ -63,16 +77,15 @@ static void usage(FILE *out)
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* Count '\n' in fixed 8-byte SWAR chunks. XOR turns '\n' bytes into zero
|
* 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.
|
* bytes, the borrow-free haszero() flags them, popcount sums them. The
|
||||||
* Constant stride (no per-newline memchr calls), so it stays fast even on
|
* common (x - 0x01..) & ~x & 0x80.. trick is NOT used: its borrow chain
|
||||||
* files with dense newlines.
|
* falsely flags a 0x01 byte that follows a zero byte, which is fine for
|
||||||
|
* strlen (lowest set bit) but overcounts here.
|
||||||
*/
|
*/
|
||||||
static long long count_newlines(const unsigned char *s, size_t n)
|
static long long count_newlines(const unsigned char *s, size_t n)
|
||||||
{
|
{
|
||||||
const uint64_t nl = 0x0a0a0a0a0a0a0a0aULL;
|
const uint64_t nl = 0x0a0a0a0a0a0a0a0aULL;
|
||||||
const uint64_t lo = 0x0101010101010101ULL;
|
const uint64_t seven = 0x7f7f7f7f7f7f7f7fULL;
|
||||||
const uint64_t hi = 0x8080808080808080ULL;
|
|
||||||
const uint64_t cl = 0x7f7f7f7f7f7f7f7fULL;
|
|
||||||
long long k = 0;
|
long long k = 0;
|
||||||
size_t i = 0;
|
size_t i = 0;
|
||||||
|
|
||||||
@@ -80,8 +93,9 @@ static long long count_newlines(const unsigned char *s, size_t n)
|
|||||||
{
|
{
|
||||||
uint64_t x;
|
uint64_t x;
|
||||||
memcpy(&x, s + i, 8);
|
memcpy(&x, s + i, 8);
|
||||||
x = (x ^ nl) & cl;
|
x ^= nl;
|
||||||
k += (long long)__builtin_popcountll((x - lo) & ~x & hi);
|
x = ~(((x & seven) + seven) | x | seven);
|
||||||
|
k += (long long)__builtin_popcountll(x);
|
||||||
}
|
}
|
||||||
for (; i < n; i++)
|
for (; i < n; i++)
|
||||||
k += s[i] == '\n';
|
k += s[i] == '\n';
|
||||||
@@ -132,6 +146,245 @@ static long long count_words(const unsigned char *s, size_t n, int *prev_ws)
|
|||||||
return w;
|
return w;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* SSE2 predates POPCNT; count 16-bit masks with the classic bit trick. */
|
||||||
|
static unsigned popcount16(unsigned x)
|
||||||
|
{
|
||||||
|
x = x - ((x >> 1) & 0x5555);
|
||||||
|
x = (x & 0x3333) + ((x >> 2) & 0x3333);
|
||||||
|
x = (x + (x >> 4)) & 0x0f0f;
|
||||||
|
return (x + (x >> 8)) & 0xff;
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
long long lines;
|
||||||
|
long long words;
|
||||||
|
} lw_t;
|
||||||
|
|
||||||
|
typedef lw_t (*count_lw_fn)(const unsigned char *s, size_t n, int *prev_ws,
|
||||||
|
int need_words);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Word separators match GNU wc (the benchmark oracle): the six C-locale
|
||||||
|
* isspace() bytes plus U+00A0 (non-breaking space). The 0x09..0x0d range
|
||||||
|
* is one unsigned compare, (x - 9) < 5, plus equalities for ' ' and NBSP.
|
||||||
|
*/
|
||||||
|
#if defined(__x86_64__) || defined(__i386__)
|
||||||
|
|
||||||
|
__attribute__((target("avx512f,avx512bw"))) static lw_t
|
||||||
|
count_lw_avx512(const unsigned char *s, size_t n, int *prev_ws, int need_words)
|
||||||
|
{
|
||||||
|
const __m512i nl = _mm512_set1_epi8('\n');
|
||||||
|
const __m512i sp = _mm512_set1_epi8(' ');
|
||||||
|
const __m512i nb = _mm512_set1_epi8((char)0xa0);
|
||||||
|
const __m512i lo = _mm512_set1_epi8(9);
|
||||||
|
const __m512i four = _mm512_set1_epi8(4);
|
||||||
|
long long lines = 0, words = 0;
|
||||||
|
size_t i = 0;
|
||||||
|
uint64_t prev = (*prev_ws != 0);
|
||||||
|
|
||||||
|
for (; i + 64 <= n; i += 64)
|
||||||
|
{
|
||||||
|
__m512i v = _mm512_loadu_si512((const void *)(s + i));
|
||||||
|
uint64_t nl_mask = (uint64_t)_mm512_cmpeq_epi8_mask(v, nl);
|
||||||
|
|
||||||
|
lines += (long long)_mm_popcnt_u64(nl_mask);
|
||||||
|
|
||||||
|
if (need_words)
|
||||||
|
{
|
||||||
|
/* min(d, 4) == d <=> (x - 9) < 5 unsigned */
|
||||||
|
__m512i d = _mm512_sub_epi8(v, lo);
|
||||||
|
uint64_t ws =
|
||||||
|
nl_mask | (uint64_t)_mm512_cmpeq_epi8_mask(v, sp) |
|
||||||
|
(uint64_t)_mm512_cmpeq_epi8_mask(v, nb) |
|
||||||
|
(uint64_t)_mm512_cmpeq_epi8_mask(_mm512_min_epu8(d, four), d);
|
||||||
|
words += (long long)_mm_popcnt_u64(~ws & ((ws << 1) | prev));
|
||||||
|
prev = (ws >> 63) & 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (; i < n; i++)
|
||||||
|
{
|
||||||
|
int ws = ws_tab[s[i]];
|
||||||
|
lines += s[i] == '\n';
|
||||||
|
if (need_words)
|
||||||
|
{
|
||||||
|
if (prev && !ws)
|
||||||
|
words++;
|
||||||
|
prev = (uint64_t)ws;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*prev_ws = (int)prev;
|
||||||
|
lw_t r = {lines, words};
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
__attribute__((target("avx2"))) static lw_t
|
||||||
|
count_lw_avx2(const unsigned char *s, size_t n, int *prev_ws, int need_words)
|
||||||
|
{
|
||||||
|
const __m256i nl = _mm256_set1_epi8('\n');
|
||||||
|
const __m256i sp = _mm256_set1_epi8(' ');
|
||||||
|
const __m256i nb = _mm256_set1_epi8((char)0xa0);
|
||||||
|
const __m256i lo = _mm256_set1_epi8(9);
|
||||||
|
const __m256i four = _mm256_set1_epi8(4);
|
||||||
|
long long lines = 0, words = 0;
|
||||||
|
size_t i = 0;
|
||||||
|
uint32_t prev = (*prev_ws != 0);
|
||||||
|
|
||||||
|
for (; i + 32 <= n; i += 32)
|
||||||
|
{
|
||||||
|
__m256i v = _mm256_loadu_si256((const void *)(s + i));
|
||||||
|
uint32_t nl_mask =
|
||||||
|
(uint32_t)_mm256_movemask_epi8(_mm256_cmpeq_epi8(v, nl));
|
||||||
|
|
||||||
|
lines += (long long)_mm_popcnt_u32(nl_mask);
|
||||||
|
|
||||||
|
if (need_words)
|
||||||
|
{
|
||||||
|
/* min(d, 4) == d <=> (x - 9) < 5 unsigned */
|
||||||
|
__m256i d = _mm256_sub_epi8(v, lo);
|
||||||
|
uint32_t ws =
|
||||||
|
nl_mask |
|
||||||
|
(uint32_t)_mm256_movemask_epi8(_mm256_cmpeq_epi8(v, sp)) |
|
||||||
|
(uint32_t)_mm256_movemask_epi8(_mm256_cmpeq_epi8(v, nb)) |
|
||||||
|
(uint32_t)_mm256_movemask_epi8(
|
||||||
|
_mm256_cmpeq_epi8(_mm256_min_epu8(d, four), d));
|
||||||
|
words += (long long)_mm_popcnt_u32(~ws & ((ws << 1) | prev));
|
||||||
|
prev = (ws >> 31) & 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (; i < n; i++)
|
||||||
|
{
|
||||||
|
int ws = ws_tab[s[i]];
|
||||||
|
lines += s[i] == '\n';
|
||||||
|
if (need_words)
|
||||||
|
{
|
||||||
|
if (prev && !ws)
|
||||||
|
words++;
|
||||||
|
prev = (uint32_t)ws;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*prev_ws = (int)prev;
|
||||||
|
lw_t r = {lines, words};
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
__attribute__((target("sse2"))) static lw_t
|
||||||
|
count_lw_sse2(const unsigned char *s, size_t n, int *prev_ws, int need_words)
|
||||||
|
{
|
||||||
|
const __m128i nl = _mm_set1_epi8('\n');
|
||||||
|
const __m128i sp = _mm_set1_epi8(' ');
|
||||||
|
const __m128i nb = _mm_set1_epi8((char)0xa0);
|
||||||
|
const __m128i lo = _mm_set1_epi8(9);
|
||||||
|
const __m128i four = _mm_set1_epi8(4);
|
||||||
|
long long lines = 0, words = 0;
|
||||||
|
size_t i = 0;
|
||||||
|
uint32_t prev = (*prev_ws != 0);
|
||||||
|
|
||||||
|
for (; i + 16 <= n; i += 16)
|
||||||
|
{
|
||||||
|
__m128i v = _mm_loadu_si128((const void *)(s + i));
|
||||||
|
uint32_t nl_mask = (uint32_t)_mm_movemask_epi8(_mm_cmpeq_epi8(v, nl));
|
||||||
|
|
||||||
|
lines += (long long)popcount16(nl_mask);
|
||||||
|
|
||||||
|
if (need_words)
|
||||||
|
{
|
||||||
|
__m128i d = _mm_sub_epi8(v, lo);
|
||||||
|
uint32_t ws = nl_mask |
|
||||||
|
(uint32_t)_mm_movemask_epi8(_mm_cmpeq_epi8(v, sp)) |
|
||||||
|
(uint32_t)_mm_movemask_epi8(_mm_cmpeq_epi8(v, nb)) |
|
||||||
|
(uint32_t)_mm_movemask_epi8(
|
||||||
|
_mm_cmpeq_epi8(_mm_min_epu8(d, four), d));
|
||||||
|
words += (long long)popcount16(~ws & ((ws << 1) | prev));
|
||||||
|
prev = (ws >> 15) & 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (; i < n; i++)
|
||||||
|
{
|
||||||
|
int ws = ws_tab[s[i]];
|
||||||
|
lines += s[i] == '\n';
|
||||||
|
if (need_words)
|
||||||
|
{
|
||||||
|
if (prev && !ws)
|
||||||
|
words++;
|
||||||
|
prev = (uint32_t)ws;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*prev_ws = (int)prev;
|
||||||
|
lw_t r = {lines, words};
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* x86 */
|
||||||
|
|
||||||
|
/* Reference path: the two scalar SWAR counters, kept as the fallback. */
|
||||||
|
static lw_t count_lw_scalar(const unsigned char *s, size_t n, int *prev_ws,
|
||||||
|
int need_words)
|
||||||
|
{
|
||||||
|
lw_t r;
|
||||||
|
|
||||||
|
r.lines = count_newlines(s, n);
|
||||||
|
r.words = need_words ? count_words(s, n, prev_ws) : 0;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
static count_lw_fn count_lw = count_lw_scalar; /* chosen by pick_kernel() */
|
||||||
|
|
||||||
|
#if defined(__x86_64__) || defined(__i386__)
|
||||||
|
static count_lw_fn pick_kernel(void)
|
||||||
|
{
|
||||||
|
__builtin_cpu_init();
|
||||||
|
if (__builtin_cpu_supports("avx512bw"))
|
||||||
|
return count_lw_avx512;
|
||||||
|
if (__builtin_cpu_supports("avx2"))
|
||||||
|
return count_lw_avx2;
|
||||||
|
if (__builtin_cpu_supports("sse2"))
|
||||||
|
return count_lw_sse2;
|
||||||
|
return count_lw_scalar;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
static count_lw_fn pick_kernel(void)
|
||||||
|
{
|
||||||
|
return count_lw_scalar;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Word separator exactly as GNU wc defines it: the Unicode white space
|
||||||
|
* set. glibc's iswspace covers it incompletely (U+2007 and U+202F are
|
||||||
|
* missing), which is why the table is explicit.
|
||||||
|
*/
|
||||||
|
static int is_wspace(wchar_t wc)
|
||||||
|
{
|
||||||
|
if (wc >= 0x2000 && wc <= 0x200a) /* en space .. hair space */
|
||||||
|
return 1;
|
||||||
|
switch (wc)
|
||||||
|
{
|
||||||
|
case 0x09: /* tab */
|
||||||
|
case 0x0a: /* newline */
|
||||||
|
case 0x0b: /* vertical tab */
|
||||||
|
case 0x0c: /* form feed */
|
||||||
|
case 0x0d: /* carriage return */
|
||||||
|
case 0x20: /* space */
|
||||||
|
case 0xa0: /* no-break space */
|
||||||
|
case 0x1680: /* ogham space mark */
|
||||||
|
case 0x2028: /* line separator */
|
||||||
|
case 0x2029: /* paragraph separator */
|
||||||
|
case 0x202f: /* narrow no-break space */
|
||||||
|
case 0x205f: /* medium mathematical space */
|
||||||
|
case 0x3000: /* ideographic space */
|
||||||
|
return 1;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Multibyte (-m) path: decode each character with mbrtowc, carrying
|
* Multibyte (-m) path: decode each character with mbrtowc, carrying
|
||||||
* incomplete sequences across read boundaries. Only used when -m is
|
* incomplete sequences across read boundaries. Only used when -m is
|
||||||
@@ -139,50 +392,88 @@ static long long count_words(const unsigned char *s, size_t n, int *prev_ws)
|
|||||||
*/
|
*/
|
||||||
static void count_stream_mb(FILE *fp, counts_t *c)
|
static void count_stream_mb(FILE *fp, counts_t *c)
|
||||||
{
|
{
|
||||||
static unsigned char buf[1 << 17];
|
/*
|
||||||
|
* GNU wc's read size and pointer accounting are replicated exactly
|
||||||
|
* (256 KiB buffer, p + prev decode pointer, p += charbytes), so
|
||||||
|
* counts agree byte for byte with the oracle - its boundary
|
||||||
|
* artifacts included. Only used when -m is requested.
|
||||||
|
*/
|
||||||
|
static unsigned char buf[1 << 18];
|
||||||
mbstate_t st;
|
mbstate_t st;
|
||||||
size_t pend = 0, nread;
|
size_t pend = 0;
|
||||||
|
int in_shift = 0;
|
||||||
int prev_ws = 1;
|
int prev_ws = 1;
|
||||||
|
|
||||||
memset(&st, 0, sizeof st);
|
memset(&st, 0, sizeof st);
|
||||||
|
|
||||||
while ((nread = fread(buf + pend, 1, sizeof buf - pend, fp)) > 0)
|
for (;;)
|
||||||
{
|
{
|
||||||
size_t n = nread + pend;
|
size_t nread = fread(buf + pend, 1, sizeof buf - pend, fp);
|
||||||
size_t i = 0;
|
if (nread == 0 && pend == 0)
|
||||||
|
break;
|
||||||
c->bytes += (long long)nread;
|
c->bytes += (long long)nread;
|
||||||
|
|
||||||
while (i < n)
|
unsigned char *p = buf;
|
||||||
|
unsigned char *plim = buf + pend + nread;
|
||||||
|
size_t prev = pend;
|
||||||
|
pend = 0;
|
||||||
|
|
||||||
|
do
|
||||||
{
|
{
|
||||||
wchar_t wc;
|
wchar_t wc;
|
||||||
size_t r;
|
size_t charbytes;
|
||||||
|
|
||||||
if (buf[i] < 0x80)
|
if (!in_shift && *p < 0x80)
|
||||||
{
|
{
|
||||||
wc = buf[i];
|
charbytes = 1;
|
||||||
r = 1;
|
wc = *p;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
r = mbrtowc(&wc, (const char *)buf + i, n - i, &st);
|
size_t scanbytes = plim - (p + prev);
|
||||||
if (r == (size_t)-2)
|
size_t r =
|
||||||
{ /* incomplete: carry over */
|
mbrtowc(&wc, (const char *)(p + prev), scanbytes, &st);
|
||||||
pend = n - i;
|
prev = 0;
|
||||||
memmove(buf, buf + i, pend);
|
|
||||||
break;
|
if (scanbytes < r)
|
||||||
}
|
{
|
||||||
if (r == (size_t)-1)
|
if (r == (size_t)-2 && plim - p < (long)sizeof buf && nread)
|
||||||
{ /* invalid sequence */
|
{ /* incomplete: carry over */
|
||||||
|
pend = (size_t)(plim - p);
|
||||||
|
memmove(buf, p, pend);
|
||||||
|
in_shift = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
/* encoding error: a byte but not a character */
|
||||||
|
p++;
|
||||||
memset(&st, 0, sizeof st);
|
memset(&st, 0, sizeof st);
|
||||||
wc = L'\xfffd';
|
in_shift = 0;
|
||||||
r = 1;
|
if (prev_ws)
|
||||||
|
{
|
||||||
|
c->words++;
|
||||||
|
prev_ws = 0;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (wc > 0x10ffff || (wc >= 0xd800 && wc <= 0xdfff))
|
||||||
|
{ /* out of range: gnulib rejects, glibc does not */
|
||||||
|
p++;
|
||||||
|
if (prev_ws)
|
||||||
|
{
|
||||||
|
c->words++;
|
||||||
|
prev_ws = 0;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
charbytes = r + (r == 0);
|
||||||
|
in_shift = !mbsinit(&st);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wc == L'\n')
|
if (wc == L'\n')
|
||||||
c->lines++;
|
c->lines++;
|
||||||
if (iswspace(wc))
|
if (is_wspace(wc))
|
||||||
{
|
{
|
||||||
prev_ws = 1;
|
prev_ws = 1;
|
||||||
}
|
}
|
||||||
@@ -192,27 +483,48 @@ static void count_stream_mb(FILE *fp, counts_t *c)
|
|||||||
prev_ws = 0;
|
prev_ws = 0;
|
||||||
}
|
}
|
||||||
c->chars++;
|
c->chars++;
|
||||||
i += r;
|
p += charbytes;
|
||||||
}
|
} while (p < plim);
|
||||||
|
|
||||||
if (i >= n)
|
|
||||||
pend = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ferror(fp))
|
if (ferror(fp))
|
||||||
c->ok = 0;
|
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 void count_stream(FILE *fp, counts_t *c)
|
||||||
{
|
{
|
||||||
static unsigned char buf[1 << 17]; /* 128 KiB */
|
static _Alignas(64) unsigned char buf[1 << 17]; /* 128 KiB, SIMD-aligned */
|
||||||
size_t nread;
|
size_t nread;
|
||||||
int prev_ws = 1; /* start of file: as if preceded by whitespace */
|
int prev_ws = 1; /* start of file: as if preceded by whitespace */
|
||||||
|
|
||||||
if (flags & F_CHARS)
|
if (flags & F_CHARS)
|
||||||
{
|
{
|
||||||
count_stream_mb(fp, c);
|
if (MB_CUR_MAX <= 1)
|
||||||
|
{ /* single-byte locale: every byte is a character, like GNU wc */
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
nread = fread(buf, 1, sizeof buf, fp); /* NOLINT */
|
||||||
|
if (nread == 0)
|
||||||
|
break;
|
||||||
|
c->bytes += (long long)nread;
|
||||||
|
c->chars += (long long)nread;
|
||||||
|
if (flags & (F_LINES | F_WORDS))
|
||||||
|
{
|
||||||
|
lw_t r =
|
||||||
|
count_lw(buf, nread, &prev_ws, (flags & F_WORDS) != 0);
|
||||||
|
if (flags & F_LINES)
|
||||||
|
c->lines += r.lines;
|
||||||
|
if (flags & F_WORDS)
|
||||||
|
c->words += r.words;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
count_stream_mb(fp, c);
|
||||||
|
}
|
||||||
|
if (ferror(fp))
|
||||||
|
c->ok = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,16 +534,126 @@ static void count_stream(FILE *fp, counts_t *c)
|
|||||||
if (nread == 0)
|
if (nread == 0)
|
||||||
break;
|
break;
|
||||||
c->bytes += (long long)nread;
|
c->bytes += (long long)nread;
|
||||||
if (flags & F_LINES)
|
if (flags & (F_LINES | F_WORDS))
|
||||||
c->lines += count_newlines(buf, nread);
|
{
|
||||||
if (flags & F_WORDS)
|
lw_t r = count_lw(buf, nread, &prev_ws, (flags & F_WORDS) != 0);
|
||||||
c->words += count_words(buf, nread, &prev_ws);
|
if (flags & F_LINES)
|
||||||
|
c->lines += r.lines;
|
||||||
|
if (flags & F_WORDS)
|
||||||
|
c->words += r.words;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ferror(fp))
|
if (ferror(fp))
|
||||||
c->ok = 0;
|
c->ok = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
const unsigned char *s;
|
||||||
|
size_t n;
|
||||||
|
int prev_ws;
|
||||||
|
int need_words;
|
||||||
|
lw_t r;
|
||||||
|
} mjob_t;
|
||||||
|
|
||||||
|
static void *map_worker(void *arg)
|
||||||
|
{
|
||||||
|
mjob_t *j = arg;
|
||||||
|
|
||||||
|
j->r = count_lw(j->s, j->n, &j->prev_ws, j->need_words);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Count a whole mapping in nt slices. Slice starts are 64-byte aligned
|
||||||
|
* so SIMD loads never straddle into a neighbor's slice; the word
|
||||||
|
* boundary between slices is seeded from the byte before the slice,
|
||||||
|
* which makes the split exact. The kernels are pure, so no locks.
|
||||||
|
*/
|
||||||
|
static void count_sliced(const unsigned char *p, size_t n, int nt,
|
||||||
|
int need_words, long long *lines, long long *words)
|
||||||
|
{
|
||||||
|
mjob_t jobs[8];
|
||||||
|
pthread_t th[8];
|
||||||
|
long long tl = 0, tw = 0;
|
||||||
|
size_t per = (n + (size_t)nt - 1) / (size_t)nt;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
per = (per + 63) & ~(size_t)63;
|
||||||
|
if (per == 0)
|
||||||
|
per = 64;
|
||||||
|
|
||||||
|
for (i = 0; i < nt; i++)
|
||||||
|
{
|
||||||
|
size_t start = (size_t)i * per;
|
||||||
|
|
||||||
|
jobs[i].s = p + start;
|
||||||
|
jobs[i].n = (start + per <= n) ? per : (start < n ? n - start : 0);
|
||||||
|
jobs[i].prev_ws = (i == 0 || jobs[i].n == 0) ? 1 : ws_tab[p[start - 1]];
|
||||||
|
jobs[i].need_words = need_words;
|
||||||
|
if (jobs[i].n > 0)
|
||||||
|
pthread_create(&th[i], NULL, map_worker, &jobs[i]);
|
||||||
|
else
|
||||||
|
th[i] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i = 0; i < nt; i++)
|
||||||
|
{
|
||||||
|
if (th[i] == 0)
|
||||||
|
continue;
|
||||||
|
pthread_join(th[i], NULL);
|
||||||
|
tl += jobs[i].r.lines;
|
||||||
|
tw += jobs[i].r.words;
|
||||||
|
}
|
||||||
|
|
||||||
|
*lines = tl;
|
||||||
|
*words = tw;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int pick_threads(size_t n)
|
||||||
|
{
|
||||||
|
long ncpu = sysconf(_SC_NPROCESSORS_ONLN);
|
||||||
|
int nt;
|
||||||
|
|
||||||
|
if (n >= (size_t)32 << 20)
|
||||||
|
nt = 8;
|
||||||
|
else if (n >= (size_t)8 << 20)
|
||||||
|
nt = 4;
|
||||||
|
else
|
||||||
|
nt = 1;
|
||||||
|
if (ncpu > 0 && nt > ncpu)
|
||||||
|
nt = (int)ncpu;
|
||||||
|
return nt;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void count_mapped(const unsigned char *p, size_t n, counts_t *c)
|
||||||
|
{
|
||||||
|
int need_words = (flags & F_WORDS) != 0;
|
||||||
|
int nt = pick_threads(n);
|
||||||
|
long long lines = 0, words = 0;
|
||||||
|
|
||||||
|
c->bytes += (long long)n;
|
||||||
|
|
||||||
|
if (nt <= 1)
|
||||||
|
{
|
||||||
|
int prev_ws = 1;
|
||||||
|
lw_t r = count_lw(p, n, &prev_ws, need_words);
|
||||||
|
|
||||||
|
lines = r.lines;
|
||||||
|
words = r.words;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
count_sliced(p, n, nt, need_words, &lines, &words);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flags & F_LINES)
|
||||||
|
c->lines += lines;
|
||||||
|
if (flags & F_WORDS)
|
||||||
|
c->words += words;
|
||||||
|
}
|
||||||
|
|
||||||
static void count_file(const char *path, counts_t *c)
|
static void count_file(const char *path, counts_t *c)
|
||||||
{
|
{
|
||||||
FILE *fp;
|
FILE *fp;
|
||||||
@@ -253,6 +675,31 @@ static void count_file(const char *path, counts_t *c)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Regular files: map instead of streaming - no copy, and the count
|
||||||
|
* can be split across cores. Falls back to streaming on any hitch. */
|
||||||
|
struct stat st;
|
||||||
|
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;
|
||||||
|
fclose(fp);
|
||||||
|
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);
|
||||||
|
|
||||||
if (ferror(fp))
|
if (ferror(fp))
|
||||||
@@ -283,6 +730,7 @@ static void widen(int *width, long long v, int enabled)
|
|||||||
*width = w;
|
*width = w;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifndef FASTWC_SELFTEST
|
||||||
int main(int argc, char **argv)
|
int main(int argc, char **argv)
|
||||||
{
|
{
|
||||||
counts_t *rows;
|
counts_t *rows;
|
||||||
@@ -291,6 +739,7 @@ int main(int argc, char **argv)
|
|||||||
int i, a;
|
int i, a;
|
||||||
|
|
||||||
init_ws_tab();
|
init_ws_tab();
|
||||||
|
count_lw = pick_kernel();
|
||||||
|
|
||||||
for (a = 1; a < argc; a++)
|
for (a = 1; a < argc; a++)
|
||||||
{
|
{
|
||||||
@@ -422,3 +871,253 @@ int main(int argc, char **argv)
|
|||||||
free(rows);
|
free(rows);
|
||||||
return failed ? 1 : 0;
|
return failed ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
#else /* FASTWC_SELFTEST */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Kernel self-test (cc -DFASTWC_SELFTEST): every kernel is checked
|
||||||
|
* against the scalar reference over deterministic inputs covering all
|
||||||
|
* chunk tail lengths and both carry states. Kernels this host cannot
|
||||||
|
* run (e.g. AVX-512) can be exercised under qemu-x86_64 -cpu max.
|
||||||
|
*/
|
||||||
|
static unsigned long long rng_state = 1;
|
||||||
|
|
||||||
|
static unsigned rng32(void)
|
||||||
|
{
|
||||||
|
rng_state = rng_state * 6364136223846793005ULL + 1442695040888963407ULL;
|
||||||
|
return (unsigned)(rng_state >> 33);
|
||||||
|
}
|
||||||
|
|
||||||
|
static long long ref_lines(const unsigned char *s, size_t n)
|
||||||
|
{
|
||||||
|
return count_newlines(s, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
static long long ref_words(const unsigned char *s, size_t n, int *prev_ws)
|
||||||
|
{
|
||||||
|
return count_words(s, n, prev_ws);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Scalar mirror of the AVX-512 kernel: identical 64-byte chunking and
|
||||||
|
* 64-bit mask arithmetic (bit-63 carry, 64-bit shift/OR, popcnt) for
|
||||||
|
* hosts that cannot execute the real zmm code. AVX-2 already proves
|
||||||
|
* the algorithm; this proves the width.
|
||||||
|
*/
|
||||||
|
static lw_t count_lw_avx512_mirror(const unsigned char *s, size_t n,
|
||||||
|
int *prev_ws, int need_words)
|
||||||
|
{
|
||||||
|
long long lines = 0, words = 0;
|
||||||
|
size_t i = 0;
|
||||||
|
uint64_t prev = (*prev_ws != 0);
|
||||||
|
|
||||||
|
for (; i + 64 <= n; i += 64)
|
||||||
|
{
|
||||||
|
uint64_t nl_mask = 0, ws = 0;
|
||||||
|
for (size_t j = 0; j < 64; j++)
|
||||||
|
{
|
||||||
|
if (s[i + j] == '\n')
|
||||||
|
nl_mask |= (uint64_t)1 << j;
|
||||||
|
if (ws_tab[s[i + j]])
|
||||||
|
ws |= (uint64_t)1 << j;
|
||||||
|
}
|
||||||
|
lines += (long long)__builtin_popcountll(nl_mask);
|
||||||
|
if (need_words)
|
||||||
|
{
|
||||||
|
words += (long long)__builtin_popcountll(~ws & ((ws << 1) | prev));
|
||||||
|
prev = (ws >> 63) & 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (; i < n; i++)
|
||||||
|
{
|
||||||
|
int ws = ws_tab[s[i]];
|
||||||
|
lines += s[i] == '\n';
|
||||||
|
if (need_words)
|
||||||
|
{
|
||||||
|
if (prev && !ws)
|
||||||
|
words++;
|
||||||
|
prev = (uint64_t)ws;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*prev_ws = (int)prev;
|
||||||
|
lw_t r = {lines, words};
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int check_kernel(const char *name, count_lw_fn fn)
|
||||||
|
{
|
||||||
|
static unsigned char buf[512];
|
||||||
|
int fails = 0;
|
||||||
|
size_t n, k;
|
||||||
|
|
||||||
|
for (n = 0; n <= 512; n++)
|
||||||
|
{
|
||||||
|
for (k = 0; k < n; k++)
|
||||||
|
buf[k] = (unsigned char)rng32();
|
||||||
|
for (int pw = 0; pw <= 1; pw++)
|
||||||
|
{
|
||||||
|
for (int nw = 0; nw <= 1; nw++)
|
||||||
|
{
|
||||||
|
int a = pw, b = pw;
|
||||||
|
lw_t got = fn(buf, n, &a, nw);
|
||||||
|
long long want_l = ref_lines(buf, n);
|
||||||
|
long long want_w = nw ? ref_words(buf, n, &b) : 0;
|
||||||
|
|
||||||
|
if (got.lines != want_l || got.words != want_w ||
|
||||||
|
a != (nw ? b : pw))
|
||||||
|
{
|
||||||
|
printf("%s: n=%zu pw=%d nw=%d lines %lld/%lld "
|
||||||
|
"words %lld/%lld state %d/%d\n",
|
||||||
|
name, n, pw, nw, got.lines, want_l, got.words,
|
||||||
|
want_w, a, b);
|
||||||
|
if (n <= 64)
|
||||||
|
{
|
||||||
|
for (k = 0; k < n; k++)
|
||||||
|
printf("%02x", buf[k]);
|
||||||
|
printf("\n");
|
||||||
|
}
|
||||||
|
fails++;
|
||||||
|
if (fails > 5)
|
||||||
|
return fails;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* whitespace-heavy patterns exercise the boundary logic harder */
|
||||||
|
for (n = 1; n <= 200; n++)
|
||||||
|
{
|
||||||
|
for (k = 0; k < n; k++)
|
||||||
|
buf[k] = (unsigned char)" \t\n\v\f\rx"[rng32() % 7];
|
||||||
|
for (int pw = 0; pw <= 1; pw++)
|
||||||
|
{
|
||||||
|
int a = pw, b = pw;
|
||||||
|
lw_t got = fn(buf, n, &a, 1);
|
||||||
|
|
||||||
|
if (got.lines != ref_lines(buf, n) ||
|
||||||
|
got.words != ref_words(buf, n, &b) || a != b)
|
||||||
|
{
|
||||||
|
printf("%s: ws-pattern n=%zu pw=%d failed\n", name, n, pw);
|
||||||
|
for (k = 0; k < n; k++)
|
||||||
|
printf("%02x", buf[k]);
|
||||||
|
printf("\n");
|
||||||
|
fails++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("%s: %s\n", name, fails ? "FAIL" : "ok");
|
||||||
|
return fails;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Slice-path check: the mmap+threads split must agree with the scalar
|
||||||
|
* reference for every thread count and many shapes, since word starts
|
||||||
|
* at slice boundaries are seeded from the neighboring byte.
|
||||||
|
*/
|
||||||
|
static int check_sliced(void)
|
||||||
|
{
|
||||||
|
static unsigned char buf[9001];
|
||||||
|
int fails = 0;
|
||||||
|
size_t n, k;
|
||||||
|
|
||||||
|
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++)
|
||||||
|
{
|
||||||
|
long long tl = 0, tw = 0;
|
||||||
|
long long want_l = count_newlines(buf, n);
|
||||||
|
int pw = 1;
|
||||||
|
long long want_w = count_words(buf, n, &pw);
|
||||||
|
|
||||||
|
count_sliced(buf, n, nt, 1, &tl, &tw);
|
||||||
|
if (tl != want_l || tw != want_w)
|
||||||
|
{
|
||||||
|
printf("sliced: n=%zu nt=%d lines %lld/%lld "
|
||||||
|
"words %lld/%lld\n",
|
||||||
|
n, nt, tl, want_l, tw, want_w);
|
||||||
|
fails++;
|
||||||
|
if (fails > 5)
|
||||||
|
return fails;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* whitespace-heavy data stresses the slice boundary seeding */
|
||||||
|
for (n = 64; n <= 3000; n += 31)
|
||||||
|
{
|
||||||
|
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++)
|
||||||
|
{
|
||||||
|
long long tl = 0, tw = 0;
|
||||||
|
long long want_l = count_newlines(buf, n);
|
||||||
|
int pw = 1;
|
||||||
|
long long want_w = count_words(buf, n, &pw);
|
||||||
|
|
||||||
|
count_sliced(buf, n, nt, 1, &tl, &tw);
|
||||||
|
if (tl != want_l || tw != want_w)
|
||||||
|
{
|
||||||
|
printf("sliced-ws: n=%zu nt=%d lines %lld/%lld "
|
||||||
|
"words %lld/%lld\n",
|
||||||
|
n, nt, tl, want_l, tw, want_w);
|
||||||
|
fails++;
|
||||||
|
if (fails > 5)
|
||||||
|
return fails;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("sliced: %s\n", fails ? "FAIL" : "ok");
|
||||||
|
return fails;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
int fails = 0;
|
||||||
|
const char *force = getenv("FASTWC_SELFTEST_FORCE");
|
||||||
|
|
||||||
|
init_ws_tab();
|
||||||
|
if (force == NULL || strcmp(force, "scalar") == 0)
|
||||||
|
fails += check_kernel("scalar", count_lw_scalar);
|
||||||
|
#if defined(__x86_64__) || defined(__i386__)
|
||||||
|
if (force == NULL || strcmp(force, "sse2") == 0)
|
||||||
|
fails += check_kernel("sse2", count_lw_sse2);
|
||||||
|
if (force == NULL || strcmp(force, "avx2") == 0)
|
||||||
|
fails += check_kernel("avx2", count_lw_avx2);
|
||||||
|
if (force == NULL)
|
||||||
|
{
|
||||||
|
__builtin_cpu_init();
|
||||||
|
if (__builtin_cpu_supports("avx512bw"))
|
||||||
|
fails += check_kernel("avx512", count_lw_avx512);
|
||||||
|
else
|
||||||
|
fails += check_kernel("avx512(mirror)", count_lw_avx512_mirror);
|
||||||
|
}
|
||||||
|
else if (strcmp(force, "avx512") == 0)
|
||||||
|
fails += check_kernel("avx512", count_lw_avx512);
|
||||||
|
else if (strcmp(force, "mirror") == 0)
|
||||||
|
fails += check_kernel("avx512(mirror)", count_lw_avx512_mirror);
|
||||||
|
#endif
|
||||||
|
count_lw = count_lw_scalar;
|
||||||
|
fails += check_sliced();
|
||||||
|
#if defined(__x86_64__) || defined(__i386__)
|
||||||
|
count_lw = count_lw_avx2;
|
||||||
|
fails += check_sliced();
|
||||||
|
count_lw = count_lw_sse2;
|
||||||
|
fails += check_sliced();
|
||||||
|
count_lw = count_lw_avx512_mirror;
|
||||||
|
fails += check_sliced();
|
||||||
|
#endif
|
||||||
|
if (fails)
|
||||||
|
{
|
||||||
|
printf("selftest FAILED\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("selftest passed\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
#endif /* FASTWC_SELFTEST */
|
||||||
|
|||||||
Reference in New Issue
Block a user