uucore: write each diagnostic in a single stderr call - #14220
Conversation
|
I think you can remove lock() too if we write everythin at once. |
|
Good point - let me test... |
Merging this PR will degrade performance by 7.46%
Warning Please fix the performance issues or acknowledge them on CodSpeed. Performance Changes
Tip Investigate this regression by commenting Comparing Footnotes
|
|
Tests pass, and you're right - no need for that since write! is atomic. NICE! knocked another 84k off.
How reliable are those codspeed benchmarks? My testing showed basically same speed. |
|
CodSpeed does not measure time of syscalls. So it is not suitable for the case reducing num of write syscalls.
|
|
That's quite a nice optimization! It's surprising that removing |
|
I'm surprised too! I'm guessing that it was the only place it was used and LTO was able to remove the function from the binary.... All credit to oech3 |
|
GNU testsuite comparison: |
|
It's especially surprising given that the impl Write for &Stderr {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.lock().write(buf)
}
}So, in principle, removing |
|
It'll be down to differences in inlining, see https://rust.godbolt.org/z/oz6MofdY5. |
|
Yes it's inlining. Honestly a lot of rust inlining things is not good for binary size. In my exploring of rust codebases inlining and generics are pretty bad at bloating a binary. edit: there is the #[inline(never)] option too LLM generated:The diagnostics macros ( // before (single write, still locking)
let _ = writeln!(std::io::stderr().lock(), "{}: {}", $crate::util_name(), format_args!($($args)+));
// after
let _ = writeln!(std::io::stderr(), "{}: {}", $crate::util_name(), format_args!($($args)+));This alone shrank the fat-LTO Method
Sizes
Symbol-level diff (cargo bloat --functions)
Top shrinkers:
Conclusion
Dropping Key point: it is not "LTO removing a lock function" (no single symbol Notes
|
|
@haydonryan Could you also remove the |
|
Done. Another 29k reduction, nice! Ran another scan to see if it can find more similar places to optimize. None for this track. There are definitely others (but some are very small)
|
|
Small additional code change, but saves a clone and drops 400b.
|
Unable to generate the performance reportThere was an internal error while processing the run's data. We're working on fixing the issue. Feel free to contact us on Discord or at support@codspeed.io if the issue persists. |
Doing some more optimization runs on coreutils.
This results in smaller binary.
Below here is LLM Generated:
Every
show_error!/show_warning!/show_warning_caps!diagnostic issued twoseparate writes to the locked stderr handle — one for the
util_name:prefix andone for the message — and the three macro bodies were otherwise identical,
triplicating the code. These are the most widely-invoked diagnostics in the
codebase, so the extra write is paid on every error/warning path in every utility.
A/B measurement
Baseline vs change are identical trees except for
src/uucore/src/lib/macros.rs.Release
coreutilsbinary size (before → after), and runtime on a representativediagnostic workload (
odover 5000 non-existent files, oneshow_error!perfile, hyperfine mean of 5 runs):
.textin everyutility that uses them. Fat-LTO already merges the copies whole-program, so the
LTO deltas are smaller than the non-LTO ones.
paths — per-file/line errors route through
show!, which already does a singlewrite — so the 2→1 write reduction per
show_error!/show_warning!call doesnot move wall-clock on realistic workloads (verified: 41.9→42.1 ms LTO,
45.8→45.5 ms non-LTO, both within noise; strace shows identical write counts).
Verification
cargo check -p uucore --releasepasses.test_chmod.rs,test_install.rs,test_wc.rs) passunchanged; output is byte-identical.