0

I'm making a library for Arduino Uno in Rust from scratch. Currently, there's a basic Serial.write print example in the src/main.rs, which on compilation is around 500 bytes (cargo b --release).

$ avr-size target/atmega328p/release/rustymetal.elf # that's the name of bin
   text    data     bss     dec     hex filename
    488      24       1     513     201 target/atmega328p/release/rustymetal.elf
#                           ^^^ this number is the total size

Which I'd like to port to examples/serial-print.rs, so that I could in future add more examples in different files rather in a single main.rs.

BUT, the same example merely copy-pasted into examples/serial_print.rs directory and compiling via cargo b --example serial-print --release blows up the size immensely, to nearly 10000 bytes!

$ avr-size target/atmega328p/release/examples/serial-print.elf
   text    data     bss     dec     hex filename
   7972    2922       1   10895    2a8f target/atmega328p/release/examples/serial-print.elf

I checked out the cargo bloat output for both, and it was pretty evident there's some kind of dead-code elimination that's NOT happening for the example version (LTO is on for both though):

Here's the output for src/main.rs:

$ cargo bloat --release # this generates the output for bin
    Analyzing target/atmega328p/release/rustymetal.elf

File  .text Size     Crate Name
3.6%  61.9% 302B [Unknown] main
0.2%   2.9%  14B smolduino __vector_22
0.0%   0.8%   4B      core core::result::unwrap_failed
0.0%   0.4%   2B      core core::panicking::panic_fmt
5.7% 100.0% 488B           .text section size, the file size is 8.3KiB

Warning: it seems like the `.text` section is nearly empty. Try removing `strip = true` from Cargo.toml

And here's the output for examples/serial-print.rs(!):

$ cargo bloat --example serial-print --release
    Analyzing target/atmega328p/release/examples/serial-print.elf

 File  .text   Size      Crate Name
 6.7%  21.2% 1.7KiB       core core::char::methods::<impl char>::escape_debug_ext
 3.1%   9.7%   772B       core <core::fmt::builders::PadAdapter as core::fmt::Write>::write_str
 2.6%   8.2%   650B      core2 <&T as core::fmt::Debug>::fmt
 2.5%   8.0%   634B       core core::fmt::write
 2.0%   6.4%   510B       core core::escape::EscapeIterInner<_>::unicode
 1.7%   5.5%   436B       core core::fmt::builders::DebugStruct::field_with
 1.5%   4.8%   384B       core core::fmt::builders::DebugTuple::field_with
 1.4%   4.4%   354B       core <core::str::iter::Chars as core::iter::traits::iterator::Iterator>::next
 1.3%   4.2%   332B      core2 <core2::io::error::Error as core::fmt::Debug>::fmt
 1.3%   4.1%   330B  [Unknown] main
 1.1%   3.5%   278B       core core::unicode::printable::check
 1.0%   3.1%   248B       core core::str::slice_error_fail_rt
 0.7%   2.1%   164B smolduino? <smolduino::error::Error as core::fmt::Debug>::fmt
 0.6%   1.9%   152B       core core::fmt::builders::DebugTuple::finish
 0.6%   1.7%   138B       core <core::fmt::builders::PadAdapter as core::fmt::Write>::write_char
 0.4%   1.1%    90B       core core::str::<impl str>::floor_char_boundary
 0.3%   1.1%    86B       core core::str::traits::<impl core::ops::index::Index<I> for str>::index
 0.3%   1.0%    82B       core <core::char::EscapeDebug as core::fmt::Display>::fmt
 0.2%   0.8%    60B       core core::fmt::Arguments::as_statically_known_str
 0.2%   0.7%    58B       core core::fmt::Write::write_fmt
 1.4%   4.5%   356B            And 18 smaller methods. Use -n N to show more.
31.8% 100.0% 7.8KiB            .text section size, the file size is 24.5KiB

For completeness' sake, here's Cargo.toml, src/main.rs and examples/serial-print.rs:

Cargo.toml:

[package]
name = "smolduino"
version = "0.1.0"
edition = "2021"

[lib]
name = "smolduino"
path = "src/lib.rs"
# I'm just doing this to remove the unnecessary error by Rust-analyzer
# I do not need tests rn, but might have to find an alterative soon when I do..
test = false
bench = false

[[bin]]
name = "rustymetal"
path = "src/main.rs"
# I'm just doing this to remove the unnecessary error by Rust-analyzer
# I do not need tests rn, but might have to find an alterative soon when I do..
test = false
bench = false

[[example]]
name = "serial-print"
path = "examples/serial-print.rs"

[profile.dev]
codegen-units = 1
panic = "abort"
opt-level = "z"
lto = true
# strip = true

[profile.release]
codegen-units = 1
panic = "abort"
opt-level = "z"
lto = true
strip = true

[dependencies]
avrd = { version = "1.0.0" }
core2 = { version = "0.4.0", default-features = false, features = ["nightly"] }
paste = "1.0.15"

src/main.rs:

#![no_std]
#![no_main]
#![feature(asm_experimental_arch)]
#![feature(abi_avr_interrupt)]

pub mod error;
pub mod io;
pub mod sync;
pub mod sys;
pub mod timing;
pub mod utils;

use core::{panic::PanicInfo, time::Duration};
use core2::io::Write;
use io::serial::Serial;
use sys::interrupt;
use timing::delay;

#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}

#[no_mangle]
extern "C" fn main() -> ! {
    unsafe {
        interrupt::enable_intr();
    }

    let mut serial = Serial::with_baud_rate(9600).unwrap();
    loop {
        // Rust flex, UTF-8 text now works on Arduino!
        serial.write_all("नमस्ते ॐ\n".as_bytes()).unwrap();
        delay::delay(Duration::from_secs(5));
    }
}

examples/serial-print.rs (as you can see it's literal copy-paste, with import statements adjusted):

#![no_std]
#![no_main]

use core::{panic::PanicInfo, time::Duration};
use core2::io::Write;
use smolduino::io::serial::Serial;
use smolduino::sys::interrupt;
use smolduino::timing::delay;

#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}

#[no_mangle]
extern "C" fn main() -> ! {
    unsafe {
        interrupt::enable_intr();
    }

    let mut serial = Serial::with_baud_rate(9600).unwrap();
    loop {
        // Rust flex, UTF-8 text now works on Arduino!
        serial.write_all("नमस्ते ॐ\n".as_bytes()).unwrap();
        delay::delay(Duration::from_secs(5));
    }
}

Here's the target .json atmega328p.json:

{
    "arch": "avr",
    "atomic-cas": false,
    "cpu": "atmega328p",
    "data-layout": "e-P1-p:16:8-i8:8-i16:8-i32:8-i64:8-f32:8-f64:8-n8-a:8",
    "eh-frame-header": false,
    "exe-suffix": ".elf",
    "late-link-args": {
        "gcc": [
            "-lgcc"
        ]
    },
    "linker": "avr-gcc",
    "llvm-target": "avr-unknown-unknown",
    "max-atomic-width": 8,
    "no-default-libraries": false,
    "pre-link-args": {
        "gcc": [
            "-mmcu=atmega328p"
        ]
    },
    "relocation-model": "static",
    "target-c-int-width": "16",
    "target-pointer-width": "16"
}

It is worth mentioning, that for AVR targets, you need to be on nightly.

EDIT: As requested, here's the memory map (by avr-ld) for the binary output, and here's the memory map (by avr-ld) for the example output.

EDIT 2: Hmm.. it doesn't seem to be an issue with avr-ld. All the same arguments are being passed to avr-ld in both cases, the issue is originating from Rust/LLVM side I guess.

9
  • Are you sute this is using GCC and not Clang/LLVM? Commented Dec 21, 2024 at 13:09
  • This uses avr-gcc for linking, but the code compilation is by LLVM only. Commented Dec 21, 2024 at 13:09
  • In that case, avr-gcc is just used as a front-end for avr-ld. Any code generation is up to clang/llvm. Commented Dec 21, 2024 at 13:12
  • avr-ld is not a valid tag, and it would be nice to get opinion of someone that knows avr-ld details, so avr-gcc tag should stay. Commented Dec 21, 2024 at 13:14
  • Maybe you can show which commands are issued in either case and the map file from avr-ld as of -Map <file>. Commented Dec 21, 2024 at 13:50

2 Answers 2

-1

As you can see in the provided map files, the "expample" code links the very expensive serial_print stuff, whereas the "binary" version doesn't:

LOAD /home/sandstorm/programming/rust_projects/smolduino/target/atmega328p/release/deps/libcompiler_builtins-53536203e97e3aab.rlib
...
 .text.main     0x0000000000000154      0x14a /home/sandstorm/programming/rust_projects/smolduino/target/atmega328p/release/examples/serial_print-6d99d3d1a77c291e.serial_print.4870af5642120e99-cgu.0.rcgu.o
                0x0000000000000154                main
 .text.unlikely._ZN4core9panicking18panic_bounds_check17hd13217aa5dfac71eE
                0x000000000000029e        0x4 /home/sandstorm/programming/rust_projects/smolduino/target/atmega328p/release/examples/serial_print-6d99d3d1a77c291e.serial_print.4870af5642120e99-cgu.0.rcgu.o
 .text.unlikely._ZN4core9panicking9panic_fmt17h01ee33661989cdf9E
                0x00000000000002a2        0x2 /home/sandstorm/programming/rust_projects/smolduino/target/atmega328p/release/examples/serial_print-6d99d3d1a77c291e.serial_print.4870af5642120e99-cgu.0.rcgu.o
 .text._ZN4core3str6traits110_$LT$impl$u20$core..slice..index..SliceIndex$LT$str$GT$$u20$for$u20$core..ops..range..RangeTo$LT$usize$GT$$GT$3get17h32cc1852084acfddE
                0x00000000000002a4       0x26 /home/sandstorm/programming/rust_projects/smolduino/target/atmega328p/release/examples/serial_print-6d99d3d1a77c291e.serial_print.4870af5642120e99-cgu.0.rcgu.o
 .text._ZN81_$LT$core..str..iter..Chars$u20$as$u20$core..iter..traits..iterator..Iterator$GT$4next17h90f63ffe0c2a679eE
                0x00000000000002ca      0x162 /home/sandstorm/programming/rust_projects/smolduino/target/atmega328p/release/examples/serial_print-6d99d3d1a77c291e.serial_print.4870af5642120e99-cgu.0.rcgu.o
 .text.unlikely._ZN4core9panicking5panic17h06f76f8b63189e3bE
                0x000000000000042c        0x4 /home/sandstorm/programming/rust_projects/smolduino/target/atmega328p/release/examples/serial_print-6d99d3d1a77c291e.serial_print.4870af5642120e99-cgu.0.rcgu.o
 .text._ZN4core3fmt5write17hd14cd572f3551a7cE
                0x0000000000000430      0x27a /home/sandstorm/programming/rust_projects/smolduino/target/atmega328p/release/examples/serial_print-6d99d3d1a77c291e.serial_print.4870af5642120e99-cgu.0.rcgu.o
 .text._ZN4core3fmt8getcount17hc7d18c7a3a8890f8E

etc.

Presumably, this is for some debug, logging output. Only you know the sources and how they are built. In C/C++, it's not unusual to have something like

#ifdef DEBUG
serial_print ("Debug output ...");
#endif

so you have to go search for similar features in you Rust sources. Or perhaps there is a serial_print, but in the binary version it is just a no-op.

Unfortunately, Arduino makes building quite complicated by first building static libraries from the sources, and then link them into a final executable. So you have to peel these layers to find where debigging / logging has been turned on.

Sign up to request clarification or add additional context in comments.

2 Comments

LMAOOO no no no, that serial-print is just the name of the example. I'm pretty sure I mention this several times in the post as well as the Cargo.toml file. If you see the actual functions linked/included in under that name, they're just part of the core crate, i.e, Rust's stdlib. It is however indicative of the problem I mentioned, since it's weird how all of this core code was inlined in bin code, but not for example.
You did not supply enough information, so that's the best guess.
-2

Binaries in examples/ use different default build settings compared to src/. This can disable optimizations like LTO, leading to larger binaries.

Make sure your Cargo.toml has LTO enabled for all builds:

[profile.release]
lto = true

This should reduce the binary size

1 Comment

If you look into the post, I've attached my Cargo.toml file. You can see that lto is already set to true for [profile.release]. I've checked, examples uses the same [profile.release] options if compiled with --release as the binary option.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.