Skip to content

unexpand: reuse a smaller buf, perf 14%+#11178

Merged
sylvestre merged 1 commit intouutils:mainfrom
oech3:unexpand-buf
Mar 4, 2026
Merged

unexpand: reuse a smaller buf, perf 14%+#11178
sylvestre merged 1 commit intouutils:mainfrom
oech3:unexpand-buf

Conversation

@oech3
Copy link
Contributor

@oech3 oech3 commented Mar 2, 2026

Closes? #10654

@codspeed-hq
Copy link

codspeed-hq bot commented Mar 2, 2026

Merging this PR will improve performance by 14.46%

⚡ 2 improved benchmarks
✅ 300 untouched benchmarks
⏩ 42 skipped benchmarks1

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation unexpand_large_file[10] 328.6 ms 287.1 ms +14.46%
Simulation unexpand_many_lines[100000] 156.8 ms 137 ms +14.46%

Comparing oech3:unexpand-buf (9d3fc99) with main (d24bf80)

Open in CodSpeed

Footnotes

  1. 42 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@oech3 oech3 force-pushed the unexpand-buf branch 6 times, most recently from 32f948d to a479877 Compare March 2, 2026 17:06
@github-actions
Copy link

github-actions bot commented Mar 2, 2026

GNU testsuite comparison:

Skip an intermittent issue tests/cut/bounded-memory (fails in this run but passes in the 'main' branch)
Skipping an intermittent issue tests/date/resolution (passes in this run but fails in the 'main' branch)
Skipping an intermittent issue tests/pr/bounded-memory (passes in this run but fails in the 'main' branch)
Skipping an intermittent issue tests/tty/tty-eof (passes in this run but fails in the 'main' branch)
Note: The gnu test tests/tail/tail-n0f is now being skipped but was previously passing.
Congrats! The gnu test tests/rm/many-dir-entries-vs-OOM is now passing!

@oech3 oech3 marked this pull request as ready for review March 2, 2026 17:39
@github-actions
Copy link

github-actions bot commented Mar 3, 2026

GNU testsuite comparison:

Skip an intermittent issue tests/pr/bounded-memory (fails in this run but passes in the 'main' branch)
Note: The gnu test tests/tail/tail-n0f is now being skipped but was previously passing.

@oech3 oech3 changed the title unexpand: mitigate 2% perf drop by cgu=1 unexpand: reuse a smaller buf, perf 6%+ Mar 3, 2026
@oech3
Copy link
Contributor Author

oech3 commented Mar 3, 2026

It seems we achieve 41.12% by reusing BufReader's buf. But I'll wait this since it has large diff.

@github-actions
Copy link

github-actions bot commented Mar 3, 2026

GNU testsuite comparison:

Note: The gnu test tests/tail/tail-n0f is now being skipped but was previously passing.

@oech3 oech3 changed the title unexpand: reuse a smaller buf, perf 6%+ unexpand: reuse a smaller buf, perf 14%+ Mar 3, 2026
@oech3 oech3 mentioned this pull request Mar 3, 2026
@oech3
Copy link
Contributor Author

oech3 commented Mar 3, 2026

Similar change for expand dropped perf.

@xtqqczze
Copy link
Contributor

xtqqczze commented Mar 3, 2026

It's probably ok to use DEFAULT_BUF_SIZE (8196 bytes) for the stack array, we already do so here:

let mut buffer = [0u8; DEFAULT_BUF_SIZE];

@oech3
Copy link
Contributor Author

oech3 commented Mar 3, 2026

It is too large. Would cause serious performance drop. Smaller size was faster.

@xtqqczze
Copy link
Contributor

xtqqczze commented Mar 3, 2026

I guess there is overhead from zeroing. I'm interested to see your code for reusing BufReader (uses 8K internally), do you have a branch in your fork?

@oech3
Copy link
Contributor Author

oech3 commented Mar 3, 2026

It seems I was misunderstanding (or perf is very flakey). It was just +4.16%

--- a/src/uu/unexpand/src/unexpand.rs
+++ b/src/uu/unexpand/src/unexpand.rs
@@ -8,7 +8,7 @@
 use clap::{Arg, ArgAction, Command};
 use std::ffi::OsString;
 use std::fs::File;
-use std::io::{BufReader, BufWriter, Read, Stdout, Write, stdin, stdout};
+use std::io::{BufRead, BufReader, BufWriter, Read, Stdout, Write, stdin, stdout};
 use std::num::IntErrorKind;
 use std::path::Path;
 use std::str::from_utf8;
@@ -553,7 +553,6 @@ fn unexpand_file(
     lastcol: usize,
     tab_config: &TabConfig,
 ) -> UResult<()> {
-    let mut buf = [0u8; 4096];
     let mut input = open(file)?;
     let mut print_state = PrintState {
         col: 0,
@@ -563,18 +562,21 @@ fn unexpand_file(
     };
 
     loop {
-        match input.read(&mut buf) {
-            Ok(0) => break,
-            Ok(n) => {
-                for line in buf[..n].split_inclusive(|b| *b == b'\n') {
-                    unexpand_buf(line, output, options, lastcol, tab_config, &mut print_state)?;
-                    if let Some(b'\n') = line.last() {
-                        print_state.new_line();
-                    }
-                }
+        let buf = input
+            .fill_buf()
+            .map_err_context(|| file.maybe_quote().to_string())?;
+        let buf_len = buf.len();
+        if buf_len == 0 {
+            break;
+        }
+        for line in buf.split_inclusive(|b| *b == b'\n') {
+            unexpand_buf(line, output, options, lastcol, tab_config, &mut print_state)?;
+            if let Some(b'\n') = line.last() {
+                print_state.new_line();
             }
-            Err(e) => return Err(e.map_err_context(|| file.maybe_quote().to_string())),
         }
+
+        input.consume(buf_len);
     }
     // write out anything remaining
     write_tabs(output, tab_config, &mut print_state, options.aflag)?;

@sylvestre sylvestre merged commit 30434cb into uutils:main Mar 4, 2026
163 checks passed
@oech3 oech3 deleted the unexpand-buf branch March 4, 2026 08:26
@oech3
Copy link
Contributor Author

oech3 commented Mar 4, 2026

I might try to avoid 0fill

@xtqqczze
Copy link
Contributor

xtqqczze commented Mar 4, 2026

@oech3 I doubt there is much to be gained from avoiding zero fill to a stack allocated array. I think we could wait for the Rust standard library to gain support with Read::read_buf.

If you could remove reuse existing BufReader though, that would be great.

@oech3
Copy link
Contributor Author

oech3 commented Mar 4, 2026

Should we try reusing BufReader with same buf size?

@oech3
Copy link
Contributor Author

oech3 commented Mar 4, 2026

One of a possibility is unexpand was causing stack spill.
expand's perf was dropped with similar change.

@oech3
Copy link
Contributor Author

oech3 commented Mar 10, 2026

still cgu=2 is 7% better

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants