Repository navigation
Conversation
|
Binary size comparison: |
029cd83 to
4645661
Compare
4645661 to
4fdf20c
Compare
| fn write_str(&mut self, s: &str) -> fmt::Result { | ||
| let mut bytes = s.as_bytes(); | ||
| while !bytes.is_empty() { | ||
| let n = unsafe { libc::write(2, bytes.as_ptr().cast(), bytes.len()) }; |
There was a problem hiding this comment.
| let n = unsafe { libc::write(2, bytes.as_ptr().cast(), bytes.len()) }; | |
| let n = unsafe { libc::write(libc::STDERR_FILENO, bytes.as_ptr().cast(), bytes.len()) }; |
also, I think we could use the rustix crate here:
There was a problem hiding this comment.
I replaced 2 by STDERR_FILENO.
For rustix, I tried but uucore's fs feature enables rustix/std and uu_stdbuf depends on libstdbuf so Cargo unifies the features and reintroduces std.
There was a problem hiding this comment.
That's not ideal, but not really an issue so long as we use nothing that requires rustix/std?
There was a problem hiding this comment.
Does rustix with std increase binary size again? If not, I prefer rustix.
There was a problem hiding this comment.
Someone could accidentaly use API requiring rustix/std in the furure, but we can catch such change like #15461 (comment) .
There was a problem hiding this comment.
Would moving libstdbuf to a separate workspace avoid the feature unification issue?
There was a problem hiding this comment.
It's not about size: rustix/std makes rustix itself a std crate, so std gets linked and the build fails (duplicate panic_impl) whatever API we call.
A separate workspace doesn't help while uu_stdbuf depends on libstdbuf directly (tried it). And without std, getting the stderr fd from rustix is unsafe too, so it wouldn't remove an unsafe here.
There was a problem hiding this comment.
In that case, the current approach is reasonable.
4fdf20c to
b10fa18
Compare
| fn set_buffer_from_env(name: &CStr, get_stream: impl FnOnce() -> *mut FILE) { | ||
| // SAFETY: getenv returns null or a NUL-terminated string, which stays | ||
| // valid as nothing changes the environment while this runs. | ||
| let value = unsafe { |
There was a problem hiding this comment.
Each unsafe operation should have it's own unsafe block and SAFETY comment,
| required-features = ["uudoc"] | ||
|
|
||
| [profile.release] | ||
| # libstdbuf is no_std in release builds, which needs LTO to link. |
There was a problem hiding this comment.
Can we still build without LTO now? What happens when building with:
CARGO_PROFILE_RELEASE_LTO=false cargo build --releaseThere was a problem hiding this comment.
Yes, LTO is mandatory for release builds, is that a problem?
I could keep non-LTO builds working with an empty rust_eh_personality stub (never called since panics abort), made hidden with global_asm! so it can't override the real one in programs like rustc, is that ok or too tricky?
There was a problem hiding this comment.
What actually happens when LTO is disabled? If it results in a duplicate-symbol linker error, relying on LTO to work around it seems like a hack.
There was a problem hiding this comment.
= note: ld.lld: error: undefined symbol: rust_eh_personality
>>> referenced by core-5dac9b97830f4695.core.eb549dbce5e65809-cgu.0.rcgu.o:(DW.ref.rust_eh_personality) in archive /usr/lib/rustlib/x86_64-unknown-linux-gnu/lib/libcore-5dac9b97830f4695.rlib
collect2: error: ld returned 1 exit status
= note: ld.lld: error: undefined symbol: rust_eh_personality
>>> referenced by core-5dac9b97830f4695.core.eb549dbce5e65809-cgu.0.rcgu.o:(DW.ref.rust_eh_personality) in archive /usr/lib/rustlib/x86_64-unknown-linux-gnu/lib/libcore-5dac9b97830f4695.rlib
There was a problem hiding this comment.
I can build this branch successfully on macOS without LTO:
CARGO_PROFILE_RELEASE_LTO=false cargo build -p uu_stdbuf --releaseThis suggests that whether LTO is needed to eliminate references to rust_eh_personality depends on the target and linker configuration.
I think we should avoid breaking non-LTO builds if possible. I'm also concerned that LTO may not necessarily eliminate every reference to rust_eh_personality, so relying on it to resolve the issue might not be a robust solution.
There was a problem hiding this comment.
Fixed: non-LTO builds now link, through a hidden rust_eh_personality stub (never called since panics abort). CI runs test_stdbuf in release with and without LTO.
| // library is preloaded into every process stdbuf runs, and embedded in stdbuf | ||
| // itself. A no_std cdylib needs panics to abort and LTO on stable Rust, so | ||
| // other builds keep std. | ||
| #![cfg_attr(all(panic = "abort", not(debug_assertions)), no_std)] |
There was a problem hiding this comment.
this cfg doesn't check for LTO, so a release build with CARGO_PROFILE_RELEASE_LTO=false (distros do that) would fail to link, no?
could we gate it on an explicit cfg/feature instead?
There was a problem hiding this comment.
I've checked locally and a build with CARGO_PROFILE_RELEASE_LTO=off fails to link with undefined reference to rust_eh_personality. Something like this could be added to avoid the issue:
#[cfg(all(panic = "abort", not(debug_assertions)))]
#[unsafe(no_mangle)]
extern "C" fn rust_eh_personality() {}
There was a problem hiding this comment.
Thanks, that's what I added, but with hidden visibility: an exported stub replaces the real routine of programs that export one, e.g. rustc aborts with "failed to initiate panic" when run under stdbuf.
Since the no_std build no longer depends on LTO, I kept the panic = "abort" cfg; happy to add an explicit feature if you still want one.
| #[cfg(all(panic = "abort", not(debug_assertions)))] | ||
| #[panic_handler] | ||
| fn panic(_: &core::panic::PanicInfo) -> ! { | ||
| unsafe { libc::abort() } |
There was a problem hiding this comment.
please add a SAFETY comment here, on the write() and on the exit() too
| if let Ok(val) = env::var("_STDBUF_O") { | ||
| set_buffer(unsafe { __stdbuf_get_stdout() }, &val); | ||
| } | ||
| set_buffer_from_env(c"_STDBUF_E", || __stdbuf_get_stderr()); |
There was a problem hiding this comment.
the tests run in debug, so CI never runs the no_std version.
could you please add a CI step running test_stdbuf with --release?
There was a problem hiding this comment.
could we build the no_std version in debug profile as well?
There was a problem hiding this comment.
Added a CI job running test_stdbuf in release, with and without LTO.
Building the no_std version in debug too would mean forcing panic=abort for the nested libstdbuf build in stdbuf's build.rs.
Should I do it here or in a follow-up?
There was a problem hiding this comment.
Since we define our own panic_handler, do we actually need to gate on panic = "abort"?
I'd also prefer to use no_std consistently across all build configurations rather than switching between std in debug and no_std in release. Keeping the configurations consistent would make debugging more predictable. Is there a reason we can't build with no_std in both?
There was a problem hiding this comment.
The gate is still needed: even with our own panic_handler, a no_std cdylib fails to build with panic=unwind, and Cargo doesn't allow setting panic per package.
stdbuf's build.rs now forces panic=abort for the nested build, so the embedded library is no_std in every profile,debug included.
Only the external library built in an unwinding profile keeps std.
|
@Ecordonnier wdyt ? :) |
|
|
||
| use core::ffi::{c_char, c_int}; | ||
| // Release builds (no debug assertions, panics abort, LTO) leave std out: the | ||
| // library is preloaded into every process stdbuf runs, and embedded in stdbuf |
There was a problem hiding this comment.
"and embedded in stdbuf itself" is not true when building with feat_external_libstdbuf (which I recommend using for all cases except when installing from cargo), so I would remove this sentence.
| // library is preloaded into every process stdbuf runs, and embedded in stdbuf | ||
| // itself. A no_std cdylib needs panics to abort and LTO on stable Rust, so | ||
| // other builds keep std. | ||
| #![cfg_attr(all(panic = "abort", not(debug_assertions)), no_std)] |
There was a problem hiding this comment.
I've checked locally and a build with CARGO_PROFILE_RELEASE_LTO=off fails to link with undefined reference to rust_eh_personality. Something like this could be added to avoid the issue:
#[cfg(all(panic = "abort", not(debug_assertions)))]
#[unsafe(no_mangle)]
extern "C" fn rust_eh_personality() {}
This looks good. I would however change the PR title and description. Unless I am missing something, this is mostly about making binaries smaller. The few microseconds gained to load libstdbuf are nice to have, but aren't that relevant on any process doing serious work. |
The following result also look quite significant:
@charly-mp That being said, could you compare against GNU 9.12 please. |
Just keep in mind the use case for stdbuf is long running processes which are fully buffered / block buffered, so saving a few ms in the context of spawning 100 processes isn't very significant IMO. On the other hand the significant size reduction is quite critical to drive adoption on small embedded targets. |
b10fa18 to
9ed5681
Compare
|
Agreed, size is the main point, especially for small embedded targets: I retitled the PR and updated the description. |
| # Its std feature would pull std back into libstdbuf, which leaves it out. | ||
| ctor = { version = "1.0.0", default-features = false, features = [ | ||
| "proc_macro", | ||
| ] } |
There was a problem hiding this comment.
| ] } | |
| ctor = { version = "1.0.0", default-features = false, features = ["proc_macro"] } |
I don't think the comment is necessary, it seems more like a narration of the diff. You could explain in the PR description instead.
There was a problem hiding this comment.
Removed the comment.
Kept the wrapped form: the one-line version fails the taplo check.
| println!("cargo:rustc-link-arg=-lc"); | ||
|
|
||
| // On ARM, unwind tables, including those of the prebuilt core, refer to | ||
| // routines of libgcc_s, which std would link too. |
9ed5681 to
af77eff
Compare
| if let Ok(val) = env::var("_STDBUF_O") { | ||
| set_buffer(unsafe { __stdbuf_get_stdout() }, &val); | ||
| } | ||
| set_buffer_from_env(c"_STDBUF_E", || __stdbuf_get_stderr()); |
There was a problem hiding this comment.
the getters aren't unsafe anymore, so could you pass __stdbuf_get_stderr directly instead of the closure?
af77eff to
68fbbe6
Compare
Merging this PR will degrade performance by 15.86%
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ❌ | Simulation | true_consecutive_calls |
294.7 ns | 350.2 ns | -15.86% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing charly-mp:libstdbuf-no-std (1ebe472) with main (6f60c70)2
Footnotes
-
70 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. ↩
-
No successful run was found on
main(48f2a33) during the generation of this report, so 6f60c70 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩
| fi | ||
| echo "OK: ariadne is not in the dependency tree" | ||
|
|
||
| test_stdbuf_release: |
There was a problem hiding this comment.
Can we remove this since no_std is used for all profiles now?
68fbbe6 to
b384781
Compare
| ))] | ||
| core::arch::global_asm!(".hidden rust_eh_personality"); | ||
|
|
||
| /// Unbuffered writes to file descriptor 2, whatever buffering stderr was given. |
There was a problem hiding this comment.
rewrite the comment now that we use libc::STDERR_FILENO
|
error on error[E0658]: inline assembly is not stable yet on this architecture
--> src/uu/stdbuf/src/libstdbuf/src/libstdbuf.rs:40:1
|
40 | core::arch::global_asm!(".hidden rust_eh_personality");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: see issue #93335 <https://github.com/rust-lang/rust/issues/93335> for more information |
libstdbuf is preloaded into every process stdbuf runs, and into their children. Linking std made it much larger than needed, which also slows down the start of each of these processes. Build it as no_std, which brings its size and load cost close to GNU's. no_std applies when panics abort, which stdbuf's build script forces for the embedded copy: a no_std cdylib cannot unwind on stable Rust, so other builds keep std. A hidden stub personality routine lets it link without LTO. libstdbuf's build script links the C library, as std did, and libgcc_s for the unwind tables of ARM. ctor's std feature is off. The stream getters lose their unsafe marker: they have no safety precondition. CI runs test_stdbuf in release, with and without LTO.
b384781 to
1ebe472
Compare
libstdbuf is preloaded into every process stdbuf runs, and into their children.
Linking std made it much larger than needed, which also slows down the start of each of these processes.
Build it as no_std, which brings its size and load cost in line with GNU's.
no_std applies when panics abort, which stdbuf's build script forces for the embedded copy; other builds keep std, since a no_std cdylib cannot unwind on stable Rust.
A hidden
rust_eh_personalitystub lets it link without LTO, and libstdbuf's build script links the C library (as std did) and libgcc_s on ARM.ctor's std feature is off, and the stream getters lose their unsafe marker.
CI now runs test_stdbuf in release, with and without LTO.
Benchmarks
libstdbuf.sosizecoreutilsmulticall sizegrep -qstarted under the librarystdbuf -oL true(embedded library)Medians of 1,500 to 30,000 interleaved runs on an idle pinned core (Ryzen 9 9950X, 4.3 GHz locked).