Skip to content

Commit

Permalink
wc: specialize scanning loop on settings. (#3708)
Browse files Browse the repository at this point in the history
* wc: specialize scanning loop on settings.

The primary computational loop in wc (iterating over all the
characters and computing word lengths, etc) is configured by a
number of boolean options that control the text-scanning behavior.
If we monomorphize the code loop for each possible combination of
scanning configurations, the rustc is able to generate better code
for each instantiation, at the least by removing the conditional
checks on each iteration, and possibly by allowing things like
vectorization.

On my computer (aarch64/macos), I am seeing at least a 5% performance
improvement in release builds on all wc flag configurations
(other than those that were already specialized) against
odyssey1024.txt, with wc -l showing the greatest improvement at 15%.

* Reduce the size of the wc dispatch table by half.

By extracting the handling of hand-written fast-paths to the
same dispatch as the automatic specializations, we can avoid
needing to pass `show_bytes` as a const generic to
`word_count_from_reader_specialized`. Eliminating this parameter
halves the number of arms in the dispatch.
  • Loading branch information
resistor authored Jul 18, 2022
1 parent 189a8af commit 735db78
Show file tree
Hide file tree
Showing 2 changed files with 180 additions and 26 deletions.
105 changes: 80 additions & 25 deletions src/uu/wc/src/wc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -306,29 +306,84 @@ fn word_count_from_reader<T: WordCountable>(
mut reader: T,
settings: &Settings,
) -> (WordCount, Option<io::Error>) {
let only_count_bytes = settings.show_bytes
&& (!(settings.show_chars
|| settings.show_lines
|| settings.show_max_line_length
|| settings.show_words));
if only_count_bytes {
let (bytes, error) = count_bytes_fast(&mut reader);
return (
WordCount {
bytes,
..WordCount::default()
},
error,
);
}

// we do not need to decode the byte stream if we're only counting bytes/newlines
let decode_chars = settings.show_chars || settings.show_words || settings.show_max_line_length;

if !decode_chars {
return count_bytes_and_lines_fast(&mut reader);
match (
settings.show_bytes,
settings.show_chars,
settings.show_lines,
settings.show_max_line_length,
settings.show_words,
) {
// Specialize scanning loop to improve the performance.
(false, false, false, false, false) => unreachable!(),
(true, false, false, false, false) => {
// Fast path when only show_bytes is true.
let (bytes, error) = count_bytes_fast(&mut reader);
(
WordCount {
bytes,
..WordCount::default()
},
error,
)
}
(false, false, true, false, false) | (true, false, true, false, false) => {
// Fast path when only (show_bytes || show_lines) is true.
count_bytes_and_lines_fast(&mut reader)
}
(_, false, false, false, true) => {
word_count_from_reader_specialized::<_, false, false, false, true>(reader)
}
(_, false, false, true, false) => {
word_count_from_reader_specialized::<_, false, false, true, false>(reader)
}
(_, false, false, true, true) => {
word_count_from_reader_specialized::<_, false, false, true, true>(reader)
}
(_, false, true, false, true) => {
word_count_from_reader_specialized::<_, false, true, false, true>(reader)
}
(_, false, true, true, false) => {
word_count_from_reader_specialized::<_, false, true, true, false>(reader)
}
(_, false, true, true, true) => {
word_count_from_reader_specialized::<_, false, true, true, true>(reader)
}
(_, true, false, false, false) => {
word_count_from_reader_specialized::<_, true, false, false, false>(reader)
}
(_, true, false, false, true) => {
word_count_from_reader_specialized::<_, true, false, false, true>(reader)
}
(_, true, false, true, false) => {
word_count_from_reader_specialized::<_, true, false, true, false>(reader)
}
(_, true, false, true, true) => {
word_count_from_reader_specialized::<_, true, false, true, true>(reader)
}
(_, true, true, false, false) => {
word_count_from_reader_specialized::<_, true, true, false, false>(reader)
}
(_, true, true, false, true) => {
word_count_from_reader_specialized::<_, true, true, false, true>(reader)
}
(_, true, true, true, false) => {
word_count_from_reader_specialized::<_, true, true, true, false>(reader)
}
(_, true, true, true, true) => {
word_count_from_reader_specialized::<_, true, true, true, true>(reader)
}
}
}

fn word_count_from_reader_specialized<
T: WordCountable,
const SHOW_CHARS: bool,
const SHOW_LINES: bool,
const SHOW_MAX_LINE_LENGTH: bool,
const SHOW_WORDS: bool,
>(
reader: T,
) -> (WordCount, Option<io::Error>) {
let mut total = WordCount::default();
let mut reader = BufReadDecoder::new(reader.buffered());
let mut in_word = false;
Expand All @@ -338,7 +393,7 @@ fn word_count_from_reader<T: WordCountable>(
match chunk {
Ok(text) => {
for ch in text.chars() {
if settings.show_words {
if SHOW_WORDS {
if ch.is_whitespace() {
in_word = false;
} else if ch.is_ascii_control() {
Expand All @@ -348,7 +403,7 @@ fn word_count_from_reader<T: WordCountable>(
total.words += 1;
}
}
if settings.show_max_line_length {
if SHOW_MAX_LINE_LENGTH {
match ch {
'\n' | '\r' | '\x0c' => {
total.max_line_length = max(current_len, total.max_line_length);
Expand All @@ -363,10 +418,10 @@ fn word_count_from_reader<T: WordCountable>(
}
}
}
if settings.show_lines && ch == '\n' {
if SHOW_LINES && ch == '\n' {
total.lines += 1;
}
if settings.show_chars {
if SHOW_CHARS {
total.chars += 1;
}
}
Expand Down
101 changes: 100 additions & 1 deletion tests/by-util/test_wc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,106 @@ fn test_utf8() {
}

#[test]
fn test_utf8_extra() {
fn test_utf8_words() {
new_ucmd!()
.arg("-w")
.pipe_in_fixture("UTF_8_weirdchars.txt")
.run()
.stdout_is("87\n");
}

#[test]
fn test_utf8_line_length_words() {
new_ucmd!()
.arg("-Lw")
.pipe_in_fixture("UTF_8_weirdchars.txt")
.run()
.stdout_is(" 87 48\n");
}

#[test]
fn test_utf8_line_length_chars() {
new_ucmd!()
.arg("-Lm")
.pipe_in_fixture("UTF_8_weirdchars.txt")
.run()
.stdout_is(" 442 48\n");
}

#[test]
fn test_utf8_line_length_chars_words() {
new_ucmd!()
.arg("-Lmw")
.pipe_in_fixture("UTF_8_weirdchars.txt")
.run()
.stdout_is(" 87 442 48\n");
}

#[test]
fn test_utf8_chars() {
new_ucmd!()
.arg("-m")
.pipe_in_fixture("UTF_8_weirdchars.txt")
.run()
.stdout_is("442\n");
}

#[test]
fn test_utf8_chars_words() {
new_ucmd!()
.arg("-mw")
.pipe_in_fixture("UTF_8_weirdchars.txt")
.run()
.stdout_is(" 87 442\n");
}

#[test]
fn test_utf8_line_length_lines() {
new_ucmd!()
.arg("-Ll")
.pipe_in_fixture("UTF_8_weirdchars.txt")
.run()
.stdout_is(" 25 48\n");
}

#[test]
fn test_utf8_line_length_lines_words() {
new_ucmd!()
.arg("-Llw")
.pipe_in_fixture("UTF_8_weirdchars.txt")
.run()
.stdout_is(" 25 87 48\n");
}

#[test]
fn test_utf8_lines_chars() {
new_ucmd!()
.arg("-ml")
.pipe_in_fixture("UTF_8_weirdchars.txt")
.run()
.stdout_is(" 25 442\n");
}

#[test]
fn test_utf8_lines_words_chars() {
new_ucmd!()
.arg("-mlw")
.pipe_in_fixture("UTF_8_weirdchars.txt")
.run()
.stdout_is(" 25 87 442\n");
}

#[test]
fn test_utf8_line_length_lines_chars() {
new_ucmd!()
.arg("-Llm")
.pipe_in_fixture("UTF_8_weirdchars.txt")
.run()
.stdout_is(" 25 442 48\n");
}

#[test]
fn test_utf8_all() {
new_ucmd!()
.arg("-lwmcL")
.pipe_in_fixture("UTF_8_weirdchars.txt")
Expand Down

0 comments on commit 735db78

Please sign in to comment.