File Coverage

utf8_swar.h
Criterion Covered Total %
statement 0 9 0.0
branch n/a
condition n/a
subroutine n/a
pod n/a
total 0 9 0.0


line stmt bran cond sub pod time code
1             /*
2             * Copyright (c) 2026 Christian Hansen
3             *
4             *
5             * Permission is hereby granted, free of charge, to any person obtaining a copy
6             * of this software and associated documentation files (the "Software"), to deal
7             * in the Software without restriction, including without limitation the rights
8             * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9             * copies of the Software, and to permit persons to whom the Software is
10             * furnished to do so, subject to the following conditions:
11             *
12             * The above copyright notice and this permission notice shall be included in all
13             * copies or substantial portions of the Software.
14             *
15             * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16             * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17             * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18             * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19             * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20             * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21             * SOFTWARE.
22             */
23              
24             /*
25             * utf8_swar.h -- SWAR primitives for UTF-8
26             * =============================================================================
27             *
28             * Counts UTF-8 codepoint boundaries in 8-byte words without SIMD instruction
29             * set extension. Assumes well-formed UTF-8 input.
30             *
31             *
32             * BACKGROUND
33             * ----------
34             *
35             * A UTF-8 byte falls into one of three classes:
36             *
37             * ASCII 0xxxxxxx (0x00-0x7F) -- starts a 1-byte sequence
38             * Lead 11xxxxxx (0xC0-0xFF) -- starts a 2/3/4-byte sequence
39             * Continuation 10xxxxxx (0x80-0xBF) -- interior byte, not a boundary
40             *
41             * Counting codepoints is equivalent to counting non-continuation bytes,
42             * since every codepoint begins at exactly one ASCII or lead byte.
43             *
44             *
45             * BYTE CLASS DETECTION
46             * --------------------
47             *
48             * Each mark function operates on an 8-byte word loaded with memcpy and
49             * returns a word with bit 0 of each byte set iff the corresponding byte
50             * belongs to the class. Each byte of the result is therefore 0 or 1.
51             *
52             * ASCII (bit 7 clear):
53             * (~w & 0x8080...) >> 7
54             *
55             * Continuation (10xxxxxx):
56             * (w & ~(w << 1)) & 0x8080...
57             * Bit 7 of each result byte is w[7] & ~w[6]. The shift crosses byte
58             * boundaries, but the mask isolates bit 7, so each byte's result
59             * depends only on its own bits 7 and 6.
60             *
61             * Non-continuation (ASCII or lead):
62             * (~w | (w << 1)) & 0x8080...
63             * Bit 7 of each result byte is ~w[7] | w[6] — true for ASCII
64             * (bit 7 clear) and lead bytes (bit 6 set). Same masking as above.
65             *
66             *
67             * HORIZONTAL SUMS
68             * ----------------
69             *
70             * Two reduction functions fold 8 byte lanes into a scalar count:
71             *
72             * utf8_swar_hsum_bits8(x) -- each byte is 0 or 1 (mark word).
73             * utf8_swar_hsum_bytes8(x) -- each byte is 0..255 (accumulated marks).
74             *
75             * hsum_bits8 uses hardware popcount when available (UTF8_SWAR_POPCNT),
76             * otherwise a multiply-fold sum. With popcount, independent calls on
77             * separate words can execute in parallel.
78             *
79             * hsum_bytes8 widens adjacent bytes into 16-bit lanes, then folds with
80             * a multiply. Used after accumulating mark words without per-word
81             * reduction.
82             *
83             *
84             * HARDWARE POPCOUNT DETECTION
85             * ---------------------------
86             *
87             * UTF8_SWAR_POPCNT is defined when a hardware popcount instruction is
88             * available:
89             *
90             * x86/x86-64 __POPCNT__ (-mpopcnt or -march=* implying it)
91             * AArch64 __aarch64__ (cnt always available)
92             * PowerPC __POPCNTD__ (popcntd, POWER5+ and later)
93             * RISC-V __riscv_zbb (Zbb bit-manipulation extension)
94             * MSVC __AVX__ (/arch:AVX or higher)
95             *
96             * __has_builtin(__builtin_popcountll) is intentionally NOT used: GCC
97             * recognises the builtin on all targets but may emit a libgcc soft
98             * call (__popcountdi2) when no hardware instruction is available.
99             *
100             */
101             #ifndef UTF8_SWAR_H
102             #define UTF8_SWAR_H
103             #include
104             #include
105             #include
106              
107             #ifdef __cplusplus
108             extern "C" {
109             #endif
110              
111             #if defined(__POPCNT__) || \
112             defined(__aarch64__) || \
113             defined(__POPCNTD__) || \
114             defined(__riscv_zbb) || \
115             (defined(_MSC_VER) && defined(__AVX__))
116             # define UTF8_SWAR_POPCNT 1
117             #endif
118              
119             #if defined(_MSC_VER) && defined(UTF8_SWAR_POPCNT)
120             # include
121             #endif
122              
123             /*
124             * utf8_swar_hsum_bits8 -- sum the 8 byte lanes of a mark word.
125             * Each byte of x must be 0 or 1. Returns the total count of 1-bytes.
126             */
127             #if defined(_MSC_VER) && defined(UTF8_SWAR_POPCNT)
128             static inline size_t utf8_swar_hsum_bits8(uint64_t x) {
129             return (size_t)__popcnt64(x);
130             }
131             #elif defined(UTF8_SWAR_POPCNT)
132             static inline size_t utf8_swar_hsum_bits8(uint64_t x) {
133             return (size_t)__builtin_popcountll(x);
134             }
135             #else
136 0           static inline size_t utf8_swar_hsum_bits8(uint64_t x) {
137 0           return (size_t)((x * UINT64_C(0x0101010101010101)) >> 56);
138             }
139             #endif
140              
141             /*
142             * utf8_swar_hsum_bytes8 -- sum the 8 byte lanes of an accumulated word.
143             * Each byte of x is in 0..255. Returns the total in [0, 2040].
144             */
145             static inline size_t utf8_swar_hsum_bytes8(uint64_t x) {
146             uint64_t pair_sum = (x & UINT64_C(0x00FF00FF00FF00FF))
147             + ((x >> 8) & UINT64_C(0x00FF00FF00FF00FF));
148             return (size_t)((pair_sum * UINT64_C(0x0001000100010001)) >> 48);
149             }
150              
151             /*
152             * has_ predicates -- high-bit mask per lane (0x80 = match, 0x00 = no match).
153             * mark_ functions -- normalized to bit 0 per lane (0x01 = match, 0x00 = no match).
154             */
155              
156             // utf8_swar_has_zero8 -- test for zero bytes.
157             static inline uint64_t utf8_swar_has_zero8(uint64_t w) {
158             return (w - UINT64_C(0x0101010101010101)) & ~w & UINT64_C(0x8080808080808080);
159             }
160              
161             // utf8_swar_has_byte8 -- test for bytes equal to byte.
162             static inline uint64_t utf8_swar_has_byte8(uint64_t w, uint8_t byte) {
163             return utf8_swar_has_zero8(w ^ (UINT64_C(0x0101010101010101) * byte));
164             }
165              
166             // utf8_swar_has_newline8 -- test for '\n' bytes (0x0A).
167             static inline uint64_t utf8_swar_has_newline8(uint64_t w) {
168             return utf8_swar_has_zero8(w ^ UINT64_C(0x0A0A0A0A0A0A0A0A));
169             }
170              
171             // utf8_swar_has_ascii8 -- test for ASCII bytes (0x00-0x7F).
172             static inline uint64_t utf8_swar_has_ascii8(uint64_t w) {
173             return ~w & UINT64_C(0x8080808080808080);
174             }
175              
176             // utf8_swar_has_continuations8 -- test for continuation bytes (0x80-0xBF).
177             static inline uint64_t utf8_swar_has_continuations8(uint64_t w) {
178             return (w & ~(w << 1)) & UINT64_C(0x8080808080808080);
179             }
180              
181             // utf8_swar_has_non_continuations8 -- test for non-continuation bytes (ASCII or lead).
182 0           static inline uint64_t utf8_swar_has_non_continuations8(uint64_t w) {
183 0           return (~w | (w << 1)) & UINT64_C(0x8080808080808080);
184             }
185              
186             // utf8_swar_mark_zero8 -- mark zero bytes (0x01 per lane).
187             static inline uint64_t utf8_swar_mark_zero8(uint64_t w) {
188             return utf8_swar_has_zero8(w) >> 7;
189             }
190              
191             // utf8_swar_mark_byte8 -- mark bytes equal to byte (0x01 per lane).
192             static inline uint64_t utf8_swar_mark_byte8(uint64_t w, uint8_t byte) {
193             return utf8_swar_has_byte8(w, byte) >> 7;
194             }
195              
196             // utf8_swar_mark_newline8 -- mark '\n' bytes (0x01 per lane).
197             static inline uint64_t utf8_swar_mark_newline8(uint64_t w) {
198             return utf8_swar_has_newline8(w) >> 7;
199             }
200              
201             // utf8_swar_mark_ascii8 -- mark ASCII bytes (0x01 per lane).
202             static inline uint64_t utf8_swar_mark_ascii8(uint64_t w) {
203             return utf8_swar_has_ascii8(w) >> 7;
204             }
205              
206             // utf8_swar_mark_continuations8 -- mark continuation bytes (0x01 per lane).
207             static inline uint64_t utf8_swar_mark_continuations8(uint64_t w) {
208             return utf8_swar_has_continuations8(w) >> 7;
209             }
210              
211             // utf8_swar_mark_non_continuations8 -- mark non-continuation bytes (0x01 per lane).
212 0           static inline uint64_t utf8_swar_mark_non_continuations8(uint64_t w) {
213 0           return utf8_swar_has_non_continuations8(w) >> 7;
214             }
215              
216             // utf8_swar_count_codepoints_1x8 -- count codepoints in one 8-byte block.
217 0           static inline size_t utf8_swar_count_codepoints_1x8(const void *src) {
218             uint64_t w;
219 0           memcpy(&w, src, sizeof w);
220 0           return utf8_swar_hsum_bits8(utf8_swar_mark_non_continuations8(w));
221             }
222              
223             /*
224             * utf8_swar_count_codepoints_Nx32 -- count codepoints in n 32-byte blocks.
225             * Each block is processed as four 8-byte words. Without hardware popcnt,
226             * mark words are accumulated and folded every 63 blocks (63*4 = 252 < 255).
227             */
228             static inline size_t utf8_swar_count_codepoints_Nx32(const void *src, size_t n) {
229             const uint8_t *bytes = (const uint8_t *)src;
230             size_t count = 0;
231              
232             #if defined(UTF8_SWAR_POPCNT)
233             for (size_t i = 0; i < n; i++, bytes += 32) {
234             uint64_t w0, w1, w2, w3;
235             memcpy(&w0, bytes + 0, 8);
236             memcpy(&w1, bytes + 8, 8);
237             memcpy(&w2, bytes + 16, 8);
238             memcpy(&w3, bytes + 24, 8);
239             count += utf8_swar_hsum_bits8(utf8_swar_mark_non_continuations8(w0))
240             + utf8_swar_hsum_bits8(utf8_swar_mark_non_continuations8(w1))
241             + utf8_swar_hsum_bits8(utf8_swar_mark_non_continuations8(w2))
242             + utf8_swar_hsum_bits8(utf8_swar_mark_non_continuations8(w3));
243             }
244             #else
245             while (n > 0) {
246             size_t batch = n < 63 ? n : 63;
247             n -= batch;
248             uint64_t acc = 0;
249             for (size_t i = 0; i < batch; i++, bytes += 32) {
250             uint64_t w0, w1, w2, w3;
251             memcpy(&w0, bytes + 0, 8);
252             memcpy(&w1, bytes + 8, 8);
253             memcpy(&w2, bytes + 16, 8);
254             memcpy(&w3, bytes + 24, 8);
255             acc += utf8_swar_mark_non_continuations8(w0)
256             + utf8_swar_mark_non_continuations8(w1)
257             + utf8_swar_mark_non_continuations8(w2)
258             + utf8_swar_mark_non_continuations8(w3);
259             }
260             count += utf8_swar_hsum_bytes8(acc);
261             }
262             #endif
263             return count;
264             }
265              
266             #ifdef __cplusplus
267             }
268             #endif
269             #endif /* UTF8_SWAR_H */