Compare commits

..
2 Commits
Author SHA1 Message Date
huntedbytheirs 6ad747dee7 test: make the avx512 mirror reproduce the kernel's real expression
The scalar AVX-512 mirror built its whitespace mask from ws_tab lookups,
so a bug in the kernel's actual predicate - the (x - 9) < 5 unsigned
range trick plus newline/space/NBSP compares - would pass the selftest
on hosts without AVX-512 and only surface on real hardware. The mirror
now computes the mask with the kernel's exact expression, and
check_kernel gains an exhaustive 256-byte ramp (forward and reversed)
so any divergence from the reference table changes a count. Verified by
mutation: shifting the range constant to (x - 8) < 5 makes the selftest
fail.
2026-08-29 17:07:42 -04:00
huntedbytheirs ae5068d4e7 fix: thread-safety in count_sliced, ship measured wins
Review (5-lane) found one MAJOR: count_sliced ignored pthread_create's
return value - on EAGAIN it joined an indeterminate pthread_t and summed
an uninitialized slice, UB plus a silently wrong count. Threads are now
initialized to 0, a failed create counts its slice inline, and nt is
hard-capped at a named MAX_THREADS (the fixed jobs[8]/th[8] arrays
smash the stack past 8 threads - reproduced by QA's thread sweep).

Shipped from the measured optimization hunt:
- -w-only mode skips the newline compare/popcount entirely (the
  (x-9)<5 range already covers '\n' in the whitespace mask) - a
  need_lines gate threads through every kernel, the scalar reference,
  the avx512 mirror, and the sliced workers.
- Thread retune: 12 threads past 32 MiB, 16 past 256 MiB (was 8) -
  up to 18% on 110 MB, ~3% on 1.1 GB in QA's interleaved sweep.
- Benchmark suites pin LC_ALL=C so GNU wc -w can't silently switch to
  multibyte decoding and inflate the win.
- popcount16 is guarded to x86 builds (zero-warnings on other arches);
  checkwc's stray argument dropped.

Verified: selftest (kernels + sliced, now with need_lines coverage),
120-trial fuzz vs GNU (file + stdin, both locales), full test-all suite
three consecutive times, format-check and clang-tidy clean. The 1B
solo monster now lands at 3.7-6s (up to 268 Mlines/s) depending on how
warm the page cache is feeling.
2026-08-29 17:03:31 -04:00
7 changed files with 150 additions and 73 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ writes a shame report and exits non-zero. These are the facts:
| lines (1M lines) | **Win: 1ms vs 2ms.** GNU's AVX-512 assist can't beat a mapped file. | | lines (1M lines) | **Win: 1ms vs 2ms.** GNU's AVX-512 assist can't beat a mapped file. |
| lines (10M lines) | **Win: 8-9ms vs 22-24ms (~2.5x).** GNU's lead never survives contact with the buffer. | | lines (10M lines) | **Win: 8-9ms vs 22-24ms (~2.5x).** GNU's lead never survives contact with the buffer. |
| lines (100M lines) | **Win: ~70ms vs ~140ms.** The monster race. GNU gets lapped. | | lines (100M lines) | **Win: ~70ms vs ~140ms.** The monster race. GNU gets lapped. |
| lines (1B lines) | **Solo, ~6-8s.** 11 GB in one pass; the only bottleneck left is the disk. | | lines (1B lines) | **Solo, ~4-6s.** 11 GB in one pass; the only bottleneck left is the disk. |
| stdin words (1M lines) | **Win: 12x.** GNU still reads stdin like it's 1985. | | stdin words (1M lines) | **Win: 12x.** GNU still reads stdin like it's 1985. |
| stdin lines (10M lines) | **Win: ~2.5x.** We map stdin redirects; GNU maps nothing. | | stdin lines (10M lines) | **Win: ~2.5x.** We map stdin redirects; GNU maps nothing. |
+1 -1
View File
@@ -14,7 +14,7 @@ REPO_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)"
source "$REPO_DIR/benchmarks/std.sh" source "$REPO_DIR/benchmarks/std.sh"
checkfastwc checkfastwc
checkwc coreutils checkwc
BENCH_NAME="coreutils" BENCH_NAME="coreutils"
printf 'benchmarking %s wc vs fastwc: lines, file input (%s interleaved runs each, minimum kept)\n' \ printf 'benchmarking %s wc vs fastwc: lines, file input (%s interleaved runs each, minimum kept)\n' \
+1 -1
View File
@@ -12,7 +12,7 @@ REPO_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)"
source "$REPO_DIR/benchmarks/std.sh" source "$REPO_DIR/benchmarks/std.sh"
checkfastwc checkfastwc
checkwc coreutils checkwc
BENCH_NAME="coreutils" BENCH_NAME="coreutils"
printf 'benchmarking %s wc vs fastwc: words, file input (%s interleaved runs each, minimum kept)\n' \ printf 'benchmarking %s wc vs fastwc: words, file input (%s interleaved runs each, minimum kept)\n' \
+6
View File
@@ -27,6 +27,12 @@
set -u set -u
# Pin the C locale: GNU wc -w silently switches to multibyte decoding under a
# UTF-8 locale, which would slow the oracle down and mask the documented
# byte-semantics divergence. Both sides count bytes here.
export LC_ALL=C
export LC_CTYPE=C
FASTWC="$REPO_DIR/bin/release/fastwc" FASTWC="$REPO_DIR/bin/release/fastwc"
DATA_DIR="$SCRIPT_DIR/.data" DATA_DIR="$SCRIPT_DIR/.data"
GENFILE="$REPO_DIR/benchmarks/tools/genfile" # optional C helper, built by test-all.sh GENFILE="$REPO_DIR/benchmarks/tools/genfile" # optional C helper, built by test-all.sh
+1 -1
View File
@@ -14,7 +14,7 @@ REPO_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)"
source "$REPO_DIR/benchmarks/std.sh" source "$REPO_DIR/benchmarks/std.sh"
checkfastwc checkfastwc
checkwc coreutils checkwc
BENCH_NAME="coreutils" BENCH_NAME="coreutils"
printf 'benchmarking %s wc vs fastwc: stdin (%s interleaved runs each, minimum kept)\n' \ printf 'benchmarking %s wc vs fastwc: stdin (%s interleaved runs each, minimum kept)\n' \
+8 -7
View File
@@ -17,12 +17,13 @@ case, so every number below survived contact with the contract.
| lines, 1M | 2-3ms | **1ms** | | lines, 1M | 2-3ms | **1ms** |
| lines, 10M | 21-24ms | **8-9ms** | | lines, 10M | 21-24ms | **8-9ms** |
| lines, 100M (monster) | ~140ms | **~70ms** | | lines, 100M (monster) | ~140ms | **~70ms** |
| lines, 1B (solo) | — | **~6s** | | lines, 1B (solo) | — | **~4-6s** |
| bytes, 1GB sparse | reads all of it | `st_size`, no read | | bytes, 1GB sparse | reads all of it | `st_size`, no read |
That is a ~2.5x win over GNU on 10M lines, a 2x win on 1M lines, and That is a ~2.5x win over GNU on 10M lines, a 2x win on 1M lines, and
a 2x win on the 100M monster. At 1B lines — 11 GB of data — the solo a 2x win on the 100M monster. At 1B lines — 11 GB of data — the solo
run lands around 6-8 seconds (125-170 Mlines/s), and the bottleneck is run lands around 4-6 seconds (200-270 Mlines/s, warm cache), and the
bottleneck is
honest to admit: an 11 GB file does not fit in the 15 GB of RAM this honest to admit: an 11 GB file does not fit in the 15 GB of RAM this
machine has, so the last monster is racing the disk. The 100M case, machine has, so the last monster is racing the disk. The 100M case,
which fits, runs at ~17 GB/s, and that number is the counting. which fits, runs at ~17 GB/s, and that number is the counting.
@@ -42,11 +43,11 @@ which fits, runs at ~17 GB/s, and that number is the counting.
the data comes through stdin, but how we read it is our business. the data comes through stdin, but how we read it is our business.
The stdin suite is why this shows up in the scoreboard too. The stdin suite is why this shows up in the scoreboard too.
3. **Parallel across cores.** Files over 8 MiB are split into 64-byte 3. **Parallel across cores.** Files over 8 MiB are split into 64-byte
aligned slices counted by up to 8 threads. The kernels are pure, so aligned slices counted by up to 16 threads (12 past 32 MiB, 16 past
the split needs no locks; word boundaries between slices are seeded 256 MiB). The kernels are pure, so the split needs no locks; word
from the byte before the slice, which makes the split exact. Below boundaries between slices are seeded from the byte before the slice,
8 MiB the thread spawn would cost more than the counting, so we which makes the split exact. Below 8 MiB the thread spawn would cost
don't bother. more than the counting, so we don't bother.
4. **No work that isn't asked for.** `-c` on a regular file is 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 `st_size` from `fstat` — GNU figured that one out too, so we copied
the good idea. `-l` without `-w` skips the whitespace mask entirely. the good idea. `-l` without `-w` skips the whitespace mask entirely.
+110 -40
View File
@@ -146,6 +146,7 @@ static long long count_words(const unsigned char *s, size_t n, int *prev_ws)
return w; return w;
} }
#if defined(__x86_64__) || defined(__i386__)
/* SSE2 predates POPCNT; count 16-bit masks with the classic bit trick. */ /* SSE2 predates POPCNT; count 16-bit masks with the classic bit trick. */
static unsigned popcount16(unsigned x) static unsigned popcount16(unsigned x)
{ {
@@ -154,6 +155,7 @@ static unsigned popcount16(unsigned x)
x = (x + (x >> 4)) & 0x0f0f; x = (x + (x >> 4)) & 0x0f0f;
return (x + (x >> 8)) & 0xff; return (x + (x >> 8)) & 0xff;
} }
#endif /* x86 */
typedef struct typedef struct
{ {
@@ -162,7 +164,7 @@ typedef struct
} lw_t; } lw_t;
typedef lw_t (*count_lw_fn)(const unsigned char *s, size_t n, int *prev_ws, typedef lw_t (*count_lw_fn)(const unsigned char *s, size_t n, int *prev_ws,
int need_words); int need_lines, int need_words);
/* /*
* Word separators match GNU wc (the benchmark oracle): the six C-locale * Word separators match GNU wc (the benchmark oracle): the six C-locale
@@ -172,7 +174,8 @@ typedef lw_t (*count_lw_fn)(const unsigned char *s, size_t n, int *prev_ws,
#if defined(__x86_64__) || defined(__i386__) #if defined(__x86_64__) || defined(__i386__)
__attribute__((target("avx512f,avx512bw"))) static lw_t __attribute__((target("avx512f,avx512bw"))) static lw_t
count_lw_avx512(const unsigned char *s, size_t n, int *prev_ws, int need_words) count_lw_avx512(const unsigned char *s, size_t n, int *prev_ws, int need_lines,
int need_words)
{ {
const __m512i nl = _mm512_set1_epi8('\n'); const __m512i nl = _mm512_set1_epi8('\n');
const __m512i sp = _mm512_set1_epi8(' '); const __m512i sp = _mm512_set1_epi8(' ');
@@ -186,13 +189,18 @@ count_lw_avx512(const unsigned char *s, size_t n, int *prev_ws, int need_words)
for (; i + 64 <= n; i += 64) for (; i + 64 <= n; i += 64)
{ {
__m512i v = _mm512_loadu_si512((const void *)(s + i)); __m512i v = _mm512_loadu_si512((const void *)(s + i));
uint64_t nl_mask = (uint64_t)_mm512_cmpeq_epi8_mask(v, nl); uint64_t nl_mask = 0;
if (need_lines)
{
nl_mask = (uint64_t)_mm512_cmpeq_epi8_mask(v, nl);
lines += (long long)_mm_popcnt_u64(nl_mask); lines += (long long)_mm_popcnt_u64(nl_mask);
}
if (need_words) if (need_words)
{ {
/* min(d, 4) == d <=> (x - 9) < 5 unsigned */ /* min(d, 4) == d <=> (x - 9) < 5 unsigned; the range
* already covers '\n', so no newline compare is needed */
__m512i d = _mm512_sub_epi8(v, lo); __m512i d = _mm512_sub_epi8(v, lo);
uint64_t ws = uint64_t ws =
nl_mask | (uint64_t)_mm512_cmpeq_epi8_mask(v, sp) | nl_mask | (uint64_t)_mm512_cmpeq_epi8_mask(v, sp) |
@@ -206,6 +214,7 @@ count_lw_avx512(const unsigned char *s, size_t n, int *prev_ws, int need_words)
for (; i < n; i++) for (; i < n; i++)
{ {
int ws = ws_tab[s[i]]; int ws = ws_tab[s[i]];
if (need_lines)
lines += s[i] == '\n'; lines += s[i] == '\n';
if (need_words) if (need_words)
{ {
@@ -221,7 +230,8 @@ count_lw_avx512(const unsigned char *s, size_t n, int *prev_ws, int need_words)
} }
__attribute__((target("avx2"))) static lw_t __attribute__((target("avx2"))) static lw_t
count_lw_avx2(const unsigned char *s, size_t n, int *prev_ws, int need_words) count_lw_avx2(const unsigned char *s, size_t n, int *prev_ws, int need_lines,
int need_words)
{ {
const __m256i nl = _mm256_set1_epi8('\n'); const __m256i nl = _mm256_set1_epi8('\n');
const __m256i sp = _mm256_set1_epi8(' '); const __m256i sp = _mm256_set1_epi8(' ');
@@ -235,14 +245,18 @@ count_lw_avx2(const unsigned char *s, size_t n, int *prev_ws, int need_words)
for (; i + 32 <= n; i += 32) for (; i + 32 <= n; i += 32)
{ {
__m256i v = _mm256_loadu_si256((const void *)(s + i)); __m256i v = _mm256_loadu_si256((const void *)(s + i));
uint32_t nl_mask = uint32_t nl_mask = 0;
(uint32_t)_mm256_movemask_epi8(_mm256_cmpeq_epi8(v, nl));
if (need_lines)
{
nl_mask = (uint32_t)_mm256_movemask_epi8(_mm256_cmpeq_epi8(v, nl));
lines += (long long)_mm_popcnt_u32(nl_mask); lines += (long long)_mm_popcnt_u32(nl_mask);
}
if (need_words) if (need_words)
{ {
/* min(d, 4) == d <=> (x - 9) < 5 unsigned */ /* min(d, 4) == d <=> (x - 9) < 5 unsigned; the range
* already covers '\n', so no newline compare is needed */
__m256i d = _mm256_sub_epi8(v, lo); __m256i d = _mm256_sub_epi8(v, lo);
uint32_t ws = uint32_t ws =
nl_mask | nl_mask |
@@ -258,6 +272,7 @@ count_lw_avx2(const unsigned char *s, size_t n, int *prev_ws, int need_words)
for (; i < n; i++) for (; i < n; i++)
{ {
int ws = ws_tab[s[i]]; int ws = ws_tab[s[i]];
if (need_lines)
lines += s[i] == '\n'; lines += s[i] == '\n';
if (need_words) if (need_words)
{ {
@@ -273,7 +288,8 @@ count_lw_avx2(const unsigned char *s, size_t n, int *prev_ws, int need_words)
} }
__attribute__((target("sse2"))) static lw_t __attribute__((target("sse2"))) static lw_t
count_lw_sse2(const unsigned char *s, size_t n, int *prev_ws, int need_words) count_lw_sse2(const unsigned char *s, size_t n, int *prev_ws, int need_lines,
int need_words)
{ {
const __m128i nl = _mm_set1_epi8('\n'); const __m128i nl = _mm_set1_epi8('\n');
const __m128i sp = _mm_set1_epi8(' '); const __m128i sp = _mm_set1_epi8(' ');
@@ -287,9 +303,13 @@ count_lw_sse2(const unsigned char *s, size_t n, int *prev_ws, int need_words)
for (; i + 16 <= n; i += 16) for (; i + 16 <= n; i += 16)
{ {
__m128i v = _mm_loadu_si128((const void *)(s + i)); __m128i v = _mm_loadu_si128((const void *)(s + i));
uint32_t nl_mask = (uint32_t)_mm_movemask_epi8(_mm_cmpeq_epi8(v, nl)); uint32_t nl_mask = 0;
if (need_lines)
{
nl_mask = (uint32_t)_mm_movemask_epi8(_mm_cmpeq_epi8(v, nl));
lines += (long long)popcount16(nl_mask); lines += (long long)popcount16(nl_mask);
}
if (need_words) if (need_words)
{ {
@@ -307,6 +327,7 @@ count_lw_sse2(const unsigned char *s, size_t n, int *prev_ws, int need_words)
for (; i < n; i++) for (; i < n; i++)
{ {
int ws = ws_tab[s[i]]; int ws = ws_tab[s[i]];
if (need_lines)
lines += s[i] == '\n'; lines += s[i] == '\n';
if (need_words) if (need_words)
{ {
@@ -325,11 +346,11 @@ count_lw_sse2(const unsigned char *s, size_t n, int *prev_ws, int need_words)
/* Reference path: the two scalar SWAR counters, kept as the fallback. */ /* 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, static lw_t count_lw_scalar(const unsigned char *s, size_t n, int *prev_ws,
int need_words) int need_lines, int need_words)
{ {
lw_t r; lw_t r;
r.lines = count_newlines(s, n); r.lines = need_lines ? count_newlines(s, n) : 0;
r.words = need_words ? count_words(s, n, prev_ws) : 0; r.words = need_words ? count_words(s, n, prev_ws) : 0;
return r; return r;
} }
@@ -513,7 +534,8 @@ static void count_stream(FILE *fp, counts_t *c)
if (flags & (F_LINES | F_WORDS)) if (flags & (F_LINES | F_WORDS))
{ {
lw_t r = lw_t r =
count_lw(buf, nread, &prev_ws, (flags & F_WORDS) != 0); count_lw(buf, nread, &prev_ws, (flags & F_LINES) != 0,
(flags & F_WORDS) != 0);
if (flags & F_LINES) if (flags & F_LINES)
c->lines += r.lines; c->lines += r.lines;
if (flags & F_WORDS) if (flags & F_WORDS)
@@ -560,7 +582,8 @@ static void count_stream(FILE *fp, counts_t *c)
c->bytes += (long long)nread; c->bytes += (long long)nread;
if (flags & (F_LINES | F_WORDS)) if (flags & (F_LINES | F_WORDS))
{ {
lw_t r = count_lw(buf, nread, &prev_ws, (flags & F_WORDS) != 0); lw_t r = count_lw(buf, nread, &prev_ws, (flags & F_LINES) != 0,
(flags & F_WORDS) != 0);
if (flags & F_LINES) if (flags & F_LINES)
c->lines += r.lines; c->lines += r.lines;
if (flags & F_WORDS) if (flags & F_WORDS)
@@ -577,6 +600,7 @@ typedef struct
const unsigned char *s; const unsigned char *s;
size_t n; size_t n;
int prev_ws; int prev_ws;
int need_lines;
int need_words; int need_words;
lw_t r; lw_t r;
} mjob_t; } mjob_t;
@@ -585,7 +609,7 @@ static void *map_worker(void *arg)
{ {
mjob_t *j = arg; mjob_t *j = arg;
j->r = count_lw(j->s, j->n, &j->prev_ws, j->need_words); j->r = count_lw(j->s, j->n, &j->prev_ws, j->need_lines, j->need_words);
return NULL; return NULL;
} }
@@ -596,14 +620,21 @@ static void *map_worker(void *arg)
* which makes the split exact. The kernels are pure, so no locks. * 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, static void count_sliced(const unsigned char *p, size_t n, int nt,
int need_words, long long *lines, long long *words) int need_lines, int need_words, long long *lines,
long long *words)
{ {
mjob_t jobs[8]; enum
pthread_t th[8]; {
MAX_THREADS = 16
};
mjob_t jobs[MAX_THREADS];
pthread_t th[MAX_THREADS];
long long tl = 0, tw = 0; long long tl = 0, tw = 0;
size_t per = (n + (size_t)nt - 1) / (size_t)nt; size_t per = (n + (size_t)nt - 1) / (size_t)nt;
int i; int i;
if (nt > MAX_THREADS)
nt = MAX_THREADS;
per = (per + 63) & ~(size_t)63; per = (per + 63) & ~(size_t)63;
if (per == 0) if (per == 0)
per = 64; per = 64;
@@ -615,11 +646,18 @@ static void count_sliced(const unsigned char *p, size_t n, int nt,
jobs[i].s = p + start; jobs[i].s = p + start;
jobs[i].n = (start + per <= n) ? per : (start < n ? n - start : 0); 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].prev_ws = (i == 0 || jobs[i].n == 0) ? 1 : ws_tab[p[start - 1]];
jobs[i].need_lines = need_lines;
jobs[i].need_words = need_words; jobs[i].need_words = need_words;
if (jobs[i].n > 0)
pthread_create(&th[i], NULL, map_worker, &jobs[i]);
else
th[i] = 0; th[i] = 0;
if (jobs[i].n > 0)
{
if (pthread_create(&th[i], NULL, map_worker, &jobs[i]) != 0)
{ /* out of threads: count this slice inline rather than lose it */
map_worker(&jobs[i]);
tl += jobs[i].r.lines;
tw += jobs[i].r.words;
}
}
} }
for (i = 0; i < nt; i++) for (i = 0; i < nt; i++)
@@ -640,8 +678,10 @@ static int pick_threads(size_t n)
long ncpu = sysconf(_SC_NPROCESSORS_ONLN); long ncpu = sysconf(_SC_NPROCESSORS_ONLN);
int nt; int nt;
if (n >= (size_t)32 << 20) if (n >= (size_t)256 << 20)
nt = 8; nt = 16;
else if (n >= (size_t)32 << 20)
nt = 12;
else if (n >= (size_t)8 << 20) else if (n >= (size_t)8 << 20)
nt = 4; nt = 4;
else else
@@ -653,6 +693,7 @@ static int pick_threads(size_t n)
static void count_mapped(const unsigned char *p, size_t n, counts_t *c) static void count_mapped(const unsigned char *p, size_t n, counts_t *c)
{ {
int need_lines = (flags & F_LINES) != 0;
int need_words = (flags & F_WORDS) != 0; int need_words = (flags & F_WORDS) != 0;
int nt = pick_threads(n); int nt = pick_threads(n);
long long lines = 0, words = 0; long long lines = 0, words = 0;
@@ -662,14 +703,14 @@ static void count_mapped(const unsigned char *p, size_t n, counts_t *c)
if (nt <= 1) if (nt <= 1)
{ {
int prev_ws = 1; int prev_ws = 1;
lw_t r = count_lw(p, n, &prev_ws, need_words); lw_t r = count_lw(p, n, &prev_ws, need_lines, need_words);
lines = r.lines; lines = r.lines;
words = r.words; words = r.words;
} }
else else
{ {
count_sliced(p, n, nt, need_words, &lines, &words); count_sliced(p, n, nt, need_lines, need_words, &lines, &words);
} }
if (flags & F_LINES) if (flags & F_LINES)
@@ -911,11 +952,14 @@ static long long ref_words(const unsigned char *s, size_t n, int *prev_ws)
/* /*
* Scalar mirror of the AVX-512 kernel: identical 64-byte chunking and * 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 * 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 * hosts that cannot execute the real zmm code. It reproduces the
* the algorithm; this proves the width. * kernel's exact whitespace expression - newline/space/NBSP equality
* plus the (x - 9) < 5 unsigned range trick - rather than consulting
* ws_tab, so a bug in the kernel's expression is caught here against
* the reference instead of only on real AVX-512 hardware.
*/ */
static lw_t count_lw_avx512_mirror(const unsigned char *s, size_t n, static lw_t count_lw_avx512_mirror(const unsigned char *s, size_t n,
int *prev_ws, int need_words) int *prev_ws, int need_lines, int need_words)
{ {
long long lines = 0, words = 0; long long lines = 0, words = 0;
size_t i = 0; size_t i = 0;
@@ -926,11 +970,15 @@ static lw_t count_lw_avx512_mirror(const unsigned char *s, size_t n,
uint64_t nl_mask = 0, ws = 0; uint64_t nl_mask = 0, ws = 0;
for (size_t j = 0; j < 64; j++) for (size_t j = 0; j < 64; j++)
{ {
if (s[i + j] == '\n') unsigned char c = s[i + j];
nl_mask |= (uint64_t)1 << j; uint64_t bit = (uint64_t)1 << j;
if (ws_tab[s[i + j]])
ws |= (uint64_t)1 << j; if (c == '\n')
nl_mask |= bit;
if (c == '\n' || c == ' ' || c == 0xa0 || (uint8_t)(c - 9) < 5)
ws |= bit;
} }
if (need_lines)
lines += (long long)__builtin_popcountll(nl_mask); lines += (long long)__builtin_popcountll(nl_mask);
if (need_words) if (need_words)
{ {
@@ -942,6 +990,7 @@ static lw_t count_lw_avx512_mirror(const unsigned char *s, size_t n,
for (; i < n; i++) for (; i < n; i++)
{ {
int ws = ws_tab[s[i]]; int ws = ws_tab[s[i]];
if (need_lines)
lines += s[i] == '\n'; lines += s[i] == '\n';
if (need_words) if (need_words)
{ {
@@ -967,21 +1016,23 @@ static int check_kernel(const char *name, count_lw_fn fn)
for (k = 0; k < n; k++) for (k = 0; k < n; k++)
buf[k] = (unsigned char)rng32(); buf[k] = (unsigned char)rng32();
for (int pw = 0; pw <= 1; pw++) for (int pw = 0; pw <= 1; pw++)
{
for (int nl = 0; nl <= 1; nl++)
{ {
for (int nw = 0; nw <= 1; nw++) for (int nw = 0; nw <= 1; nw++)
{ {
int a = pw, b = pw; int a = pw, b = pw;
lw_t got = fn(buf, n, &a, nw); lw_t got = fn(buf, n, &a, nl, nw);
long long want_l = ref_lines(buf, n); long long want_l = nl ? ref_lines(buf, n) : 0;
long long want_w = nw ? ref_words(buf, n, &b) : 0; long long want_w = nw ? ref_words(buf, n, &b) : 0;
if (got.lines != want_l || got.words != want_w || if (got.lines != want_l || got.words != want_w ||
a != (nw ? b : pw)) a != (nw ? b : pw))
{ {
printf("%s: n=%zu pw=%d nw=%d lines %lld/%lld " printf("%s: n=%zu pw=%d nl=%d nw=%d lines %lld/%lld "
"words %lld/%lld state %d/%d\n", "words %lld/%lld state %d/%d\n",
name, n, pw, nw, got.lines, want_l, got.words, name, n, pw, nl, nw, got.lines, want_l,
want_w, a, b); got.words, want_w, a, b);
if (n <= 64) if (n <= 64)
{ {
for (k = 0; k < n; k++) for (k = 0; k < n; k++)
@@ -995,6 +1046,7 @@ static int check_kernel(const char *name, count_lw_fn fn)
} }
} }
} }
}
/* whitespace-heavy patterns exercise the boundary logic harder */ /* whitespace-heavy patterns exercise the boundary logic harder */
for (n = 1; n <= 200; n++) for (n = 1; n <= 200; n++)
@@ -1004,7 +1056,7 @@ static int check_kernel(const char *name, count_lw_fn fn)
for (int pw = 0; pw <= 1; pw++) for (int pw = 0; pw <= 1; pw++)
{ {
int a = pw, b = pw; int a = pw, b = pw;
lw_t got = fn(buf, n, &a, 1); lw_t got = fn(buf, n, &a, 1, 1);
if (got.lines != ref_lines(buf, n) || if (got.lines != ref_lines(buf, n) ||
got.words != ref_words(buf, n, &b) || a != b) got.words != ref_words(buf, n, &b) || a != b)
@@ -1019,6 +1071,24 @@ static int check_kernel(const char *name, count_lw_fn fn)
} }
} }
/* exhaustive predicate check: every byte value in a 256-byte ramp,
* forward and reversed, so a divergence between the kernel's
* whitespace expression and the reference table changes a count */
for (int pass = 0; pass < 2; pass++)
{
for (k = 0; k < 256; k++)
buf[k] = pass == 0 ? (unsigned char)k : (unsigned char)(255 - k);
int a = 1, b = 1;
lw_t got = fn(buf, 256, &a, 1, 1);
if (got.lines != ref_lines(buf, 256) ||
got.words != ref_words(buf, 256, &b) || a != b)
{
printf("%s: predicate ramp pass=%d failed\n", name, pass);
fails++;
}
}
printf("%s: %s\n", name, fails ? "FAIL" : "ok"); printf("%s: %s\n", name, fails ? "FAIL" : "ok");
return fails; return fails;
} }
@@ -1045,7 +1115,7 @@ static int check_sliced(void)
int pw = 1; int pw = 1;
long long want_w = count_words(buf, n, &pw); long long want_w = count_words(buf, n, &pw);
count_sliced(buf, n, nt, 1, &tl, &tw); count_sliced(buf, n, nt, 1, 1, &tl, &tw);
if (tl != want_l || tw != want_w) if (tl != want_l || tw != want_w)
{ {
printf("sliced: n=%zu nt=%d lines %lld/%lld " printf("sliced: n=%zu nt=%d lines %lld/%lld "
@@ -1070,7 +1140,7 @@ static int check_sliced(void)
int pw = 1; int pw = 1;
long long want_w = count_words(buf, n, &pw); long long want_w = count_words(buf, n, &pw);
count_sliced(buf, n, nt, 1, &tl, &tw); count_sliced(buf, n, nt, 1, 1, &tl, &tw);
if (tl != want_l || tw != want_w) if (tl != want_l || tw != want_w)
{ {
printf("sliced-ws: n=%zu nt=%d lines %lld/%lld " printf("sliced-ws: n=%zu nt=%d lines %lld/%lld "