I’m trying to use Rust for a bare-metal x86 program that must fit in a 14KiB space. (It’s part of a bootloader.) So far, I’ve been successful, and my program is about 10KB, but core::panicking is giving me trouble. rustc really wants to output a bunch of libcore formatting code (about 6KB) into my binary, and when it does, my program exceeds its budget.
(FWIW, I’m compiling with -C lto -C opt-level=2.)
Many Rust language and library features call a panic function, which eventually calls Formatter::pad or Formatter::pad_integral. The format argument is always "{}", so width and precision are None, and pad exits early on a fast path. The compiler isn’t smart enough, though, so it outputs the entirety of pad and pad_integral. pad needs to truncate and/or pad UTF-8 strings, and ultimately pulls in functions for UTF-8 encoding and decoding.
I’ve found a few ways to suppress Formatter::pad[_integral], but they’re some combination of ugly, unmaintainable, or incomplete. For example:
-
Use LTO with a panic_fmt function that ignores its core::fmt::Arguments parameter. Try to get rustc to delete the format_args!(...) code. I had been doing this two months ago, but then the parameter type changed from &core::fmt::Arguments to core::fmt::Arguments, and the trick stopped working. I’ve found a few ways to get it working again, e.g.:
-
Declare the panic_fmt lang_item with no parameters. I can also crash the compiler with such a function, though, so I don’t expect it to continue working.
-
Refactor panicking such that each of core::panicking::{panic, panic_bounds_check, panic_fmt} calls the panic_fmt lang_item directly. Declare the lang_item with #[inline(always)].
-
Avoid calling Formatter::pad[_integral] in libcore. e.g. use Arguments::new_v1(&[expr], &[]) in core::panicking::panic(). Or, wrap the message in a new type, PanicMsg, whose Display implementation calls Formatter::write_str instead of Formatter::pad.
-
Move the panic and panic_bounds_check lang_items out of libcore. This completely solves the problem for those two panic entry points, but what about panicking::panic_fmt? Maybe it could be an inline function that just calls the panic_fmt lang_item? I could only see this working if the two had exactly the same signature. Currently, the ABIs are different and one of the functions bundles the file/line info into a tuple.
Alternatively, maybe we can make Formatter::pad[_integral] smaller? For example, I’d guess we could eliminate the encode_utf8 calls:
- Change
core::fmt::rt::v1::FormatSpec::fill from a char to a [u8; 4] (There’s a Formatter::fill() function, though, and also, this changes the ABI. Are ABI changes still allowed?)
- The call in
Formatter::pad_integral seems unnecessary.
Adding some kind of -Os/-Oz flag to rustc would help, and it might be good enough for my particular use case. I hacked one in at one point, but it bit rot.
For tiny programs, I think there’d ideally be a way to suppress not just the format strings but also the filenames.
Questions:
- Do any of the
libcore changes above seem reasonable to anyone else?
- Is there a better way to avoid this overhead?
- Is a minimal-overhead
libcore something the project cares about?