1use memchr::memchr_iter;
2use rayon::prelude::*;
3
4const PARALLEL_THRESHOLD: usize = 2 * 1024 * 1024;
7
8#[derive(Debug, Clone, Default, PartialEq, Eq)]
10pub struct WcCounts {
11 pub lines: u64,
12 pub words: u64,
13 pub bytes: u64,
14 pub chars: u64,
15 pub max_line_length: u64,
16}
17
18const fn make_ws_table() -> [u8; 256] {
21 let mut t = [0u8; 256];
22 t[0x09] = 1; t[0x0A] = 1; t[0x0B] = 1; t[0x0C] = 1; t[0x0D] = 1; t[0x20] = 1; t
29}
30
31const WS_TABLE: [u8; 256] = make_ws_table();
33
34const fn make_printable_table() -> [u8; 256] {
36 let mut t = [0u8; 256];
37 let mut i = 0x20u16;
38 while i <= 0x7E {
39 t[i as usize] = 1;
40 i += 1;
41 }
42 t
43}
44
45const PRINTABLE_TABLE: [u8; 256] = make_printable_table();
46
47#[inline]
50pub fn count_lines(data: &[u8]) -> u64 {
51 memchr_iter(b'\n', data).count() as u64
52}
53
54#[inline]
56pub fn count_bytes(data: &[u8]) -> u64 {
57 data.len() as u64
58}
59
60pub fn count_words(data: &[u8]) -> u64 {
72 #[cfg(target_arch = "x86_64")]
73 {
74 return unsafe { count_words_sse2(data) };
76 }
77 #[cfg(not(target_arch = "x86_64"))]
78 {
79 return count_words_scalar(data);
80 }
81}
82
83#[cfg(target_arch = "x86_64")]
87#[target_feature(enable = "sse2")]
88unsafe fn count_words_sse2(data: &[u8]) -> u64 {
89 use std::arch::x86_64::*;
90
91 unsafe {
92 let min_ws = _mm_set1_epi8(0x08); let max_ws = _mm_set1_epi8(0x0E); let space = _mm_set1_epi8(0x20);
97
98 let mut words = 0u64;
99 let mut prev_ws_bit = 1u64; let chunks = data.chunks_exact(64);
102 let remainder = chunks.remainder();
103
104 for chunk in chunks {
105 let ptr = chunk.as_ptr();
106
107 let v0 = _mm_loadu_si128(ptr as *const __m128i);
109 let v1 = _mm_loadu_si128(ptr.add(16) as *const __m128i);
110 let v2 = _mm_loadu_si128(ptr.add(32) as *const __m128i);
111 let v3 = _mm_loadu_si128(ptr.add(48) as *const __m128i);
112
113 macro_rules! detect_ws {
115 ($v:expr) => {{
116 let ge_9 = _mm_cmpgt_epi8($v, min_ws);
117 let le_d = _mm_cmpgt_epi8(max_ws, $v);
118 let in_range = _mm_and_si128(ge_9, le_d);
119 let is_sp = _mm_cmpeq_epi8($v, space);
120 _mm_or_si128(in_range, is_sp)
121 }};
122 }
123
124 let ws0 = detect_ws!(v0);
125 let ws1 = detect_ws!(v1);
126 let ws2 = detect_ws!(v2);
127 let ws3 = detect_ws!(v3);
128
129 let m0 = (_mm_movemask_epi8(ws0) as u16) as u64;
131 let m1 = (_mm_movemask_epi8(ws1) as u16) as u64;
132 let m2 = (_mm_movemask_epi8(ws2) as u16) as u64;
133 let m3 = (_mm_movemask_epi8(ws3) as u16) as u64;
134 let ws_mask = m0 | (m1 << 16) | (m2 << 32) | (m3 << 48);
135
136 let prev_mask = (ws_mask << 1) | prev_ws_bit;
138 let word_starts = prev_mask & !ws_mask;
139 words += word_starts.count_ones() as u64;
140
141 prev_ws_bit = (ws_mask >> 63) & 1;
142 }
143
144 let sub_chunks = remainder.chunks_exact(16);
146 let sub_remainder = sub_chunks.remainder();
147 let mut prev_ws_u32 = prev_ws_bit as u32;
148
149 for chunk in sub_chunks {
150 let v = _mm_loadu_si128(chunk.as_ptr() as *const __m128i);
151 let ge_9 = _mm_cmpgt_epi8(v, min_ws);
152 let le_d = _mm_cmpgt_epi8(max_ws, v);
153 let in_range = _mm_and_si128(ge_9, le_d);
154 let is_sp = _mm_cmpeq_epi8(v, space);
155 let ws_vec = _mm_or_si128(in_range, is_sp);
156 let ws_mask = _mm_movemask_epi8(ws_vec) as u32;
157
158 let prev_mask = (ws_mask << 1) | prev_ws_u32;
159 let word_starts = prev_mask & (!ws_mask & 0xFFFF);
160 words += word_starts.count_ones() as u64;
161 prev_ws_u32 = (ws_mask >> 15) & 1;
162 }
163
164 let mut prev_ws = prev_ws_u32 as u8;
166 for &b in sub_remainder {
167 let curr_ws = WS_TABLE[b as usize];
168 words += (prev_ws & (curr_ws ^ 1)) as u64;
169 prev_ws = curr_ws;
170 }
171 words
172 }
173}
174
175#[cfg(not(target_arch = "x86_64"))]
178fn count_words_scalar(data: &[u8]) -> u64 {
179 let mut words = 0u64;
180 let mut prev_ws_bit = 1u64;
181
182 let chunks = data.chunks_exact(64);
183 let remainder = chunks.remainder();
184
185 for chunk in chunks {
186 let mut ws_mask = 0u64;
187 let mut i = 0;
188 while i + 7 < 64 {
189 ws_mask |= (WS_TABLE[chunk[i] as usize] as u64) << i;
190 ws_mask |= (WS_TABLE[chunk[i + 1] as usize] as u64) << (i + 1);
191 ws_mask |= (WS_TABLE[chunk[i + 2] as usize] as u64) << (i + 2);
192 ws_mask |= (WS_TABLE[chunk[i + 3] as usize] as u64) << (i + 3);
193 ws_mask |= (WS_TABLE[chunk[i + 4] as usize] as u64) << (i + 4);
194 ws_mask |= (WS_TABLE[chunk[i + 5] as usize] as u64) << (i + 5);
195 ws_mask |= (WS_TABLE[chunk[i + 6] as usize] as u64) << (i + 6);
196 ws_mask |= (WS_TABLE[chunk[i + 7] as usize] as u64) << (i + 7);
197 i += 8;
198 }
199
200 let prev_mask = (ws_mask << 1) | prev_ws_bit;
201 let word_starts = prev_mask & !ws_mask;
202 words += word_starts.count_ones() as u64;
203 prev_ws_bit = (ws_mask >> 63) & 1;
204 }
205
206 let mut prev_ws = prev_ws_bit as u8;
207 for &b in remainder {
208 let curr_ws = WS_TABLE[b as usize];
209 words += (prev_ws & (curr_ws ^ 1)) as u64;
210 prev_ws = curr_ws;
211 }
212 words
213}
214
215pub fn count_lines_words(data: &[u8]) -> (u64, u64) {
222 let mut words = 0u64;
223 let mut lines = 0u64;
224 let mut prev_ws_bit = 1u64;
225
226 let chunks = data.chunks_exact(64);
227 let remainder = chunks.remainder();
228
229 for chunk in chunks {
230 let mut ws_mask = 0u64;
231 let mut nl_mask = 0u64;
232 let mut i = 0;
233 while i + 7 < 64 {
234 let b0 = chunk[i];
235 let b1 = chunk[i + 1];
236 let b2 = chunk[i + 2];
237 let b3 = chunk[i + 3];
238 let b4 = chunk[i + 4];
239 let b5 = chunk[i + 5];
240 let b6 = chunk[i + 6];
241 let b7 = chunk[i + 7];
242 ws_mask |= (WS_TABLE[b0 as usize] as u64) << i;
243 ws_mask |= (WS_TABLE[b1 as usize] as u64) << (i + 1);
244 ws_mask |= (WS_TABLE[b2 as usize] as u64) << (i + 2);
245 ws_mask |= (WS_TABLE[b3 as usize] as u64) << (i + 3);
246 ws_mask |= (WS_TABLE[b4 as usize] as u64) << (i + 4);
247 ws_mask |= (WS_TABLE[b5 as usize] as u64) << (i + 5);
248 ws_mask |= (WS_TABLE[b6 as usize] as u64) << (i + 6);
249 ws_mask |= (WS_TABLE[b7 as usize] as u64) << (i + 7);
250 nl_mask |= ((b0 == b'\n') as u64) << i;
251 nl_mask |= ((b1 == b'\n') as u64) << (i + 1);
252 nl_mask |= ((b2 == b'\n') as u64) << (i + 2);
253 nl_mask |= ((b3 == b'\n') as u64) << (i + 3);
254 nl_mask |= ((b4 == b'\n') as u64) << (i + 4);
255 nl_mask |= ((b5 == b'\n') as u64) << (i + 5);
256 nl_mask |= ((b6 == b'\n') as u64) << (i + 6);
257 nl_mask |= ((b7 == b'\n') as u64) << (i + 7);
258 i += 8;
259 }
260
261 let prev_mask = (ws_mask << 1) | prev_ws_bit;
262 let word_starts = prev_mask & !ws_mask;
263 words += word_starts.count_ones() as u64;
264 lines += nl_mask.count_ones() as u64;
265 prev_ws_bit = (ws_mask >> 63) & 1;
266 }
267
268 let mut prev_ws = prev_ws_bit as u8;
269 for &b in remainder {
270 if b == b'\n' {
271 lines += 1;
272 }
273 let curr_ws = WS_TABLE[b as usize];
274 words += (prev_ws & (curr_ws ^ 1)) as u64;
275 prev_ws = curr_ws;
276 }
277 (lines, words)
278}
279
280pub fn count_lines_words_chars_utf8(data: &[u8]) -> (u64, u64, u64) {
287 let mut words = 0u64;
288 let mut lines = 0u64;
289 let mut chars = 0u64;
290 let mut prev_ws_bit = 1u64;
291
292 let chunks = data.chunks_exact(64);
293 let remainder = chunks.remainder();
294
295 for chunk in chunks {
296 let mut ws_mask = 0u64;
297 let mut nl_mask = 0u64;
298 let mut char_mask = 0u64;
299 let mut i = 0;
300 while i + 7 < 64 {
301 let b0 = chunk[i];
302 let b1 = chunk[i + 1];
303 let b2 = chunk[i + 2];
304 let b3 = chunk[i + 3];
305 let b4 = chunk[i + 4];
306 let b5 = chunk[i + 5];
307 let b6 = chunk[i + 6];
308 let b7 = chunk[i + 7];
309
310 ws_mask |= (WS_TABLE[b0 as usize] as u64) << i
311 | (WS_TABLE[b1 as usize] as u64) << (i + 1)
312 | (WS_TABLE[b2 as usize] as u64) << (i + 2)
313 | (WS_TABLE[b3 as usize] as u64) << (i + 3)
314 | (WS_TABLE[b4 as usize] as u64) << (i + 4)
315 | (WS_TABLE[b5 as usize] as u64) << (i + 5)
316 | (WS_TABLE[b6 as usize] as u64) << (i + 6)
317 | (WS_TABLE[b7 as usize] as u64) << (i + 7);
318
319 nl_mask |= ((b0 == b'\n') as u64) << i
320 | ((b1 == b'\n') as u64) << (i + 1)
321 | ((b2 == b'\n') as u64) << (i + 2)
322 | ((b3 == b'\n') as u64) << (i + 3)
323 | ((b4 == b'\n') as u64) << (i + 4)
324 | ((b5 == b'\n') as u64) << (i + 5)
325 | ((b6 == b'\n') as u64) << (i + 6)
326 | ((b7 == b'\n') as u64) << (i + 7);
327
328 char_mask |= (((b0 & 0xC0) != 0x80) as u64) << i
329 | (((b1 & 0xC0) != 0x80) as u64) << (i + 1)
330 | (((b2 & 0xC0) != 0x80) as u64) << (i + 2)
331 | (((b3 & 0xC0) != 0x80) as u64) << (i + 3)
332 | (((b4 & 0xC0) != 0x80) as u64) << (i + 4)
333 | (((b5 & 0xC0) != 0x80) as u64) << (i + 5)
334 | (((b6 & 0xC0) != 0x80) as u64) << (i + 6)
335 | (((b7 & 0xC0) != 0x80) as u64) << (i + 7);
336
337 i += 8;
338 }
339 let prev_mask = (ws_mask << 1) | prev_ws_bit;
340 let word_starts = prev_mask & !ws_mask;
341 words += word_starts.count_ones() as u64;
342 lines += nl_mask.count_ones() as u64;
343 chars += char_mask.count_ones() as u64;
344 prev_ws_bit = (ws_mask >> 63) & 1;
345 }
346
347 let mut prev_ws = prev_ws_bit as u8;
348 for &b in remainder {
349 if b == b'\n' {
350 lines += 1;
351 }
352 let curr_ws = WS_TABLE[b as usize];
353 words += (prev_ws & (curr_ws ^ 1)) as u64;
354 prev_ws = curr_ws;
355 chars += ((b & 0xC0) != 0x80) as u64;
356 }
357 (lines, words, chars)
358}
359
360pub fn count_chars_utf8(data: &[u8]) -> u64 {
366 let mut count = 0u64;
367 let chunks = data.chunks_exact(64);
368 let remainder = chunks.remainder();
369
370 for chunk in chunks {
371 let mut char_mask = 0u64;
373 let mut i = 0;
374 while i + 7 < 64 {
375 char_mask |= (((chunk[i] & 0xC0) != 0x80) as u64) << i;
376 char_mask |= (((chunk[i + 1] & 0xC0) != 0x80) as u64) << (i + 1);
377 char_mask |= (((chunk[i + 2] & 0xC0) != 0x80) as u64) << (i + 2);
378 char_mask |= (((chunk[i + 3] & 0xC0) != 0x80) as u64) << (i + 3);
379 char_mask |= (((chunk[i + 4] & 0xC0) != 0x80) as u64) << (i + 4);
380 char_mask |= (((chunk[i + 5] & 0xC0) != 0x80) as u64) << (i + 5);
381 char_mask |= (((chunk[i + 6] & 0xC0) != 0x80) as u64) << (i + 6);
382 char_mask |= (((chunk[i + 7] & 0xC0) != 0x80) as u64) << (i + 7);
383 i += 8;
384 }
385 count += char_mask.count_ones() as u64;
386 }
387
388 for &b in remainder {
389 count += ((b & 0xC0) != 0x80) as u64;
390 }
391 count
392}
393
394#[inline]
396pub fn count_chars_c(data: &[u8]) -> u64 {
397 data.len() as u64
398}
399
400#[inline]
402pub fn count_chars(data: &[u8], utf8: bool) -> u64 {
403 if utf8 {
404 count_chars_utf8(data)
405 } else {
406 count_chars_c(data)
407 }
408}
409
410pub fn is_utf8_locale() -> bool {
412 for var in &["LC_ALL", "LC_CTYPE", "LANG"] {
413 if let Ok(val) = std::env::var(var) {
414 if !val.is_empty() {
415 let lower = val.to_ascii_lowercase();
416 return lower.contains("utf-8") || lower.contains("utf8");
417 }
418 }
419 }
420 false
421}
422
423#[inline]
426fn decode_utf8(bytes: &[u8]) -> (u32, usize) {
427 let b0 = bytes[0];
428 if b0 < 0x80 {
429 return (b0 as u32, 1);
430 }
431 if b0 < 0xC2 {
432 return (b0 as u32, 1);
434 }
435 if b0 < 0xE0 {
436 if bytes.len() < 2 || bytes[1] & 0xC0 != 0x80 {
437 return (b0 as u32, 1);
438 }
439 let cp = ((b0 as u32 & 0x1F) << 6) | (bytes[1] as u32 & 0x3F);
440 return (cp, 2);
441 }
442 if b0 < 0xF0 {
443 if bytes.len() < 3 || bytes[1] & 0xC0 != 0x80 || bytes[2] & 0xC0 != 0x80 {
444 return (b0 as u32, 1);
445 }
446 let cp =
447 ((b0 as u32 & 0x0F) << 12) | ((bytes[1] as u32 & 0x3F) << 6) | (bytes[2] as u32 & 0x3F);
448 return (cp, 3);
449 }
450 if b0 < 0xF5 {
451 if bytes.len() < 4
452 || bytes[1] & 0xC0 != 0x80
453 || bytes[2] & 0xC0 != 0x80
454 || bytes[3] & 0xC0 != 0x80
455 {
456 return (b0 as u32, 1);
457 }
458 let cp = ((b0 as u32 & 0x07) << 18)
459 | ((bytes[1] as u32 & 0x3F) << 12)
460 | ((bytes[2] as u32 & 0x3F) << 6)
461 | (bytes[3] as u32 & 0x3F);
462 return (cp, 4);
463 }
464 (b0 as u32, 1)
465}
466
467#[inline]
469fn is_wide_char(cp: u32) -> bool {
470 matches!(cp,
471 0x1100..=0x115F | 0x231A..=0x231B | 0x2329..=0x232A | 0x23E9..=0x23F3 | 0x23F8..=0x23FA
476 | 0x25FD..=0x25FE
477 | 0x2614..=0x2615
478 | 0x2648..=0x2653
479 | 0x267F
480 | 0x2693
481 | 0x26A1
482 | 0x26AA..=0x26AB
483 | 0x26BD..=0x26BE
484 | 0x26C4..=0x26C5
485 | 0x26CE
486 | 0x26D4
487 | 0x26EA
488 | 0x26F2..=0x26F3
489 | 0x26F5
490 | 0x26FA
491 | 0x26FD
492 | 0x2702
493 | 0x2705
494 | 0x2708..=0x270D
495 | 0x270F
496 | 0x2712
497 | 0x2714
498 | 0x2716
499 | 0x271D
500 | 0x2721
501 | 0x2728
502 | 0x2733..=0x2734
503 | 0x2744
504 | 0x2747
505 | 0x274C
506 | 0x274E
507 | 0x2753..=0x2755
508 | 0x2757
509 | 0x2763..=0x2764
510 | 0x2795..=0x2797
511 | 0x27A1
512 | 0x27B0
513 | 0x27BF
514 | 0x2934..=0x2935
515 | 0x2B05..=0x2B07
516 | 0x2B1B..=0x2B1C
517 | 0x2B50
518 | 0x2B55
519 | 0x2E80..=0x303E | 0x3041..=0x33BF | 0x3400..=0x4DBF | 0x4E00..=0xA4CF | 0xA960..=0xA97C | 0xAC00..=0xD7A3 | 0xF900..=0xFAFF | 0xFE10..=0xFE19 | 0xFE30..=0xFE6F | 0xFF01..=0xFF60 | 0xFFE0..=0xFFE6 | 0x1F004
531 | 0x1F0CF
532 | 0x1F170..=0x1F171
533 | 0x1F17E..=0x1F17F
534 | 0x1F18E
535 | 0x1F191..=0x1F19A
536 | 0x1F1E0..=0x1F1FF | 0x1F200..=0x1F202
538 | 0x1F210..=0x1F23B
539 | 0x1F240..=0x1F248
540 | 0x1F250..=0x1F251
541 | 0x1F260..=0x1F265
542 | 0x1F300..=0x1F64F | 0x1F680..=0x1F6FF | 0x1F900..=0x1F9FF | 0x1FA00..=0x1FA6F
546 | 0x1FA70..=0x1FAFF
547 | 0x20000..=0x2FFFD | 0x30000..=0x3FFFD )
550}
551
552pub fn max_line_length_c(data: &[u8]) -> u64 {
562 let mut max_len: u64 = 0;
563 let mut line_len: u64 = 0; let mut linepos: u64 = 0; for &b in data {
567 match b {
568 b'\n' => {
569 if line_len > max_len {
570 max_len = line_len;
571 }
572 linepos = 0;
573 line_len = 0;
574 }
575 b'\t' => {
576 linepos = (linepos + 8) & !7;
577 if linepos > line_len {
578 line_len = linepos;
579 }
580 }
581 b'\r' => {
582 linepos = 0;
583 }
584 0x0C => {
585 if line_len > max_len {
587 max_len = line_len;
588 }
589 linepos = 0;
590 line_len = 0;
591 }
592 _ => {
593 if PRINTABLE_TABLE[b as usize] != 0 {
594 linepos += 1;
595 if linepos > line_len {
596 line_len = linepos;
597 }
598 }
599 }
601 }
602 }
603
604 if line_len > max_len {
606 max_len = line_len;
607 }
608
609 max_len
610}
611
612pub fn max_line_length_utf8(data: &[u8]) -> u64 {
617 let mut max_len: u64 = 0;
618 let mut line_len: u64 = 0;
619 let mut linepos: u64 = 0;
620 let mut i = 0;
621
622 while i < data.len() {
623 let b = data[i];
624
625 if b < 0x80 {
627 match b {
628 b'\n' => {
629 if line_len > max_len {
630 max_len = line_len;
631 }
632 linepos = 0;
633 line_len = 0;
634 }
635 b'\t' => {
636 linepos = (linepos + 8) & !7;
637 if linepos > line_len {
638 line_len = linepos;
639 }
640 }
641 b'\r' => {
642 linepos = 0;
643 }
644 0x0C => {
645 if line_len > max_len {
647 max_len = line_len;
648 }
649 linepos = 0;
650 line_len = 0;
651 }
652 0x20..=0x7E => {
653 linepos += 1;
655 if linepos > line_len {
656 line_len = linepos;
657 }
658 }
659 _ => {
660 }
662 }
663 i += 1;
664 } else {
665 let (cp, len) = decode_utf8(&data[i..]);
667
668 if cp <= 0x9F {
670 } else if is_wide_char(cp) {
672 linepos += 2;
673 if linepos > line_len {
674 line_len = linepos;
675 }
676 } else {
677 linepos += 1;
679 if linepos > line_len {
680 line_len = linepos;
681 }
682 }
683 i += len;
684 }
685 }
686
687 if line_len > max_len {
689 max_len = line_len;
690 }
691
692 max_len
693}
694
695#[inline]
697pub fn max_line_length(data: &[u8], utf8: bool) -> u64 {
698 if utf8 {
699 max_line_length_utf8(data)
700 } else {
701 max_line_length_c(data)
702 }
703}
704
705pub fn count_all(data: &[u8], utf8: bool) -> WcCounts {
717 WcCounts {
718 lines: count_lines(data),
719 words: count_words(data),
720 bytes: data.len() as u64,
721 chars: count_chars(data, utf8),
722 max_line_length: max_line_length(data, utf8),
723 }
724}
725
726pub fn count_lines_parallel(data: &[u8]) -> u64 {
732 if data.len() < PARALLEL_THRESHOLD {
733 return count_lines(data);
734 }
735
736 let num_threads = rayon::current_num_threads().max(1);
737 let chunk_size = (data.len() / num_threads).max(1024 * 1024);
738
739 data.par_chunks(chunk_size)
740 .map(|chunk| memchr_iter(b'\n', chunk).count() as u64)
741 .sum()
742}
743
744pub fn count_words_parallel(data: &[u8]) -> u64 {
750 if data.len() < PARALLEL_THRESHOLD {
751 return count_words(data);
752 }
753
754 let num_threads = rayon::current_num_threads().max(1);
755 let chunk_size = (data.len() / num_threads).max(1024 * 1024);
756
757 let chunks: Vec<&[u8]> = data.chunks(chunk_size).collect();
758
759 let results: Vec<(u64, bool, bool)> = chunks
761 .par_iter()
762 .map(|chunk| {
763 let words = count_words(chunk);
764 let starts_non_ws = chunk.first().is_some_and(|&b| WS_TABLE[b as usize] == 0);
765 let ends_non_ws = chunk.last().is_some_and(|&b| WS_TABLE[b as usize] == 0);
766 (words, starts_non_ws, ends_non_ws)
767 })
768 .collect();
769
770 let mut total = 0u64;
771 for i in 0..results.len() {
772 total += results[i].0;
773 if i > 0 && results[i].1 && results[i - 1].2 {
776 total -= 1;
777 }
778 }
779 total
780}
781
782pub fn count_chars_parallel(data: &[u8], utf8: bool) -> u64 {
784 if !utf8 {
785 return data.len() as u64;
786 }
787 if data.len() < PARALLEL_THRESHOLD {
788 return count_chars_utf8(data);
789 }
790
791 let num_threads = rayon::current_num_threads().max(1);
792 let chunk_size = (data.len() / num_threads).max(1024 * 1024);
793
794 data.par_chunks(chunk_size).map(count_chars_utf8).sum()
795}
796
797struct ChunkResult {
799 lines: u64,
800 words: u64,
801 chars: u64,
802 starts_non_ws: bool,
803 ends_non_ws: bool,
804}
805
806pub fn count_lwc_parallel(data: &[u8], utf8: bool) -> (u64, u64, u64) {
814 if data.len() < PARALLEL_THRESHOLD {
815 let lines = count_lines(data);
817 let words = count_words(data);
818 let chars = count_chars(data, utf8);
819 return (lines, words, chars);
820 }
821
822 let num_threads = rayon::current_num_threads().max(1);
823 let chunk_size = (data.len() / num_threads).max(1024 * 1024);
824
825 let chunks: Vec<&[u8]> = data.chunks(chunk_size).collect();
826
827 let results: Vec<ChunkResult> = chunks
828 .par_iter()
829 .map(|chunk| {
830 let lines = memchr_iter(b'\n', chunk).count() as u64;
832 let words = count_words(chunk);
834 let chars = if utf8 {
836 count_chars_utf8(chunk)
837 } else {
838 chunk.len() as u64
839 };
840 let starts_non_ws = chunk.first().is_some_and(|&b| WS_TABLE[b as usize] == 0);
841 let ends_non_ws = chunk.last().is_some_and(|&b| WS_TABLE[b as usize] == 0);
842 ChunkResult {
843 lines,
844 words,
845 chars,
846 starts_non_ws,
847 ends_non_ws,
848 }
849 })
850 .collect();
851
852 let mut total_lines = 0u64;
853 let mut total_words = 0u64;
854 let mut total_chars = 0u64;
855 for i in 0..results.len() {
856 total_lines += results[i].lines;
857 total_words += results[i].words;
858 total_chars += results[i].chars;
859 if i > 0 && results[i].starts_non_ws && results[i - 1].ends_non_ws {
860 total_words -= 1;
861 }
862 }
863 (total_lines, total_words, total_chars)
864}