22 ms·
Building Tiny Rust Binaries for Embedded Linux
- jamesmunns 9y agoHey all, I was able to attend the Rust All Hands this year, and this is a write up of a cool project I worked on there. Let me know if you have any questions!
- Lerc 9y agoThe article mentions the Flash storage size at 8meg. What was the ram size of the target? UPX can be a bit of mixed bag. It doesn't necessarily cause load delays, If decompression is faster than data loading it can actually speed things. You can't mmap it though which could be an issue depending on RAM. If using an underlying compressed filesystem it might be providing on-paper-only gains. It would be interesting to see breakdowns of the size of the text and data section at each stage of the size optimization.
- rhn_mk1 9y agoWhat is left in such a binary? 800K of code seems on the high side even for a http framework.
- steveklabnik 9y agoOne thing that rocket ends up including is some uncompressed literal html; jamesmunns was planning on reporting that upstream.
- the_mitsuhiko 9y agoI assume it just ends up. It will likely ship a regex engine with unicode support. If it does I assume that’s a big part of it.
- zokier 9y agoWhile not answering the question directly, you can peek into Cargo.lock to see what all libraries are used by the project: https://github.com/spacekookie/tinyrocket/blob/master/Cargo.lock https://github.com/spacekookie/tinyrocket/blob/master/Cargo.... Of course what of those end up in the final binary is an open question. It would be kinda cool to see a dependency tree with size annotations, but I suppose that might be bit tricky to produce. But I did find "cargo-bloat"[1] tool that gives following output: File .text Size Name 4.8% 19.5% 98.2KiB core 4.0% 16.3% 81.9KiB std 3.9% 15.8% 79.4KiB rocket 2.4% 9.9% 49.9KiB hyper 2.0% 8.1% 40.6KiB [Unknown] 1.7% 7.0% 35.3KiB alloc 1.6% 6.5% 32.7KiB toml 1.1% 4.5% 22.5KiB url 1.0% 4.3% 21.4KiB yansi 0.5% 2.0% 9.9KiB tinyrocket 0.5% 1.8% 9.2KiB time 0.3% 1.3% 6.4KiB ring 0.2% 0.8% 3.8KiB ordermap 0.1% 0.5% 2.4KiB idna 0.1% 0.4% 1.8KiB percent_encoding 0.1% 0.4% 1.8KiB unicode_normalization 0.1% 0.3% 1.3KiB serde 0.1% 0.2% 1.2KiB std_unicode 0.0% 0.2% 880B pear 0.0% 0.1% 576B log 0.0% 0.1% 338B cookie 0.0% 0.1% 307B unwind 0.0% 0.0% 198B unicode_bidi 0.0% 0.0% 161B state 0.0% 0.0% 160B httparse 0.0% 0.0% 124B memchr 0.0% 0.0% 94B alloc_system 0.0% 0.0% 77B typeable 0.0% 0.0% 52B smallvec 0.0% 0.0% 45B compiler_builtins 0.0% 0.0% 28B rustc_tsan 0.0% 0.0% 25B unicase 0.0% 0.0% 11B panic_abort 0.0% 0.0% 11B panic_unwind 24.6% 100.0% 502.6KiB .text section size, the file size is 2.0MiB When the stripped binary on my system is 979K. So it manages to account for about half of the binary size. From what is accounted, I suppose yansi (ANSI terminal lib) is the most superfluous, followed by toml parsing. Other than those, most of the stuff seems kinda relevant for a web framework. I'm now bit curious what the 979 - 502.6 = 476.4 remaining kilobytes are.. and totally nerdsniped. Running size gets me following: [zokier@zarch tinyrocket]$ size -A -d target/release/tinyrocket target/release/tinyrocket : section size addr .interp 28 624 .note.ABI-tag 32 652 .note.gnu.build-id 36 684 .gnu.hash 28 720 .dynsym 2568 752 .dynstr 1532 3320 .gnu.version 214 4852 .gnu.version_r 288 5072 .rela.dyn 40320 5360 .rela.plt 2328 45680 .init 23 48008 .plt 1568 48032 .plt.got 8 49600 .text 519185 49664 .fini 9 568852 .rodata 294652 568864 .eh_frame_hdr 15580 863516 .eh_frame 82544 879096 .tdata 48 3060064 .tbss 136 3060112 .init_array 8 3060112 .fini_array 8 3060120 .data.rel.ro 34984 3060128 .dynamic 576 3095112 .got 872 3095688 .data 201 3096576 .bss 464 3096784 .comment 69 0 Total 998309 So almost 290k in .rodata, that seems suspicious. Quick look at it shows about 17k of html as steveklabnik mentioned, but other than that I can't really identify any major blocks. I suppose this is the end of the dive, unless someone knows some tools to get more insight into .rodata data. I suppose in theory it should be possible to track down where in the code each bit of .rodata is accessed from, but that seems bit of a stretch. [1] https://github.com/RazrFalcon/cargo-bloat https://github.com/RazrFalcon/cargo-bloat
- haberman 9y ago> I suppose this is the end of the dive, unless someone knows some tools to get more insight into .rodata data. I suppose in theory it should be possible to track down where in the code each bit of .rodata is accessed from, but that seems bit of a stretch. My tool Bloaty (https://github.com/google/bloaty https://github.com/google/bloaty) attempts to do exactly this. It even disassembles the binary looking for instructions that reference other sections like .rodata. It doesn't currently know anything about Rust's name mangling scheme. I'd be happy to add this, though I suppose Rust's mangling is probably written in Rust and Bloaty is written in C++.
- Serow225 9y agoThat looks like a very cool tool, thanks for sharing! Bookmarked :)
- mnemonik 9y agoGNU libiberty includes a Rust symbol demangler that you can link: https://github.com/gcc-mirror/gcc/blob/master/libiberty/rust-demangle.c https://github.com/gcc-mirror/gcc/blob/master/libiberty/rust...
- zokier 9y agoCool. So I ran bloaty (with -d sections,segments,rawsymbols) on tinyrocket and used rustfilt[1] to demangle the symbols, and we have numbers for .rodata: demangled Filesize (KiB) idna::uts46::find_char 89.25 unicode_normalization::normalize::d 69.52 [384 Others] 43.48 rocket::config::RocketConfig::override_from_env 23.59 unicode_bidi::char_data::bidi_class 15.16 idna::uts46::decode_slice 12.23 unicode_normalization::normalize::compose 10.29 core::num::dec2flt::algorithm::power_of_ten 5.97 unicode_normalization::tables::normalization::canonical_comb 3.91 [section .rodata] 1.99 idna::uts46::validate 1.95 <hyper::status::StatusCode as core::fmt::Debug>::fmt 1.79 tinyrocket::main 1.46 core::num::flt2dec::strategy::grisu::CACHED_POW10 1.27 rocket::config::config::Config::set_raw 1.15 time::display::parse_type 0.99 hyper::server::listener::spawn_with::{{closure}} 0.96 percent_encoding::percent_encode_byte 0.76 <hyper::status::StatusCode as core::fmt::Display>::fmt 0.71 rocket::catcher::defaults::get::handle_431 0.66 rocket::config::init::{{closure}} 0.65 Lots of unicode and idna stuff there. I think the binary size would be reduced significantly if we could drop those somehow. Most likely those embedded HTML (error) pages are accounted in "rocket::config::RocketConfig::override_from_env", which sort of makes sense. > It doesn't currently know anything about Rust's name mangling scheme. I'd be happy to add this, though I suppose Rust's mangling is probably written in Rust and Bloaty is written in C++. I suppose you don't want to have a (optional) dependency to Rust code? It should be pretty easy to provide C interface to rustc-demangle[2] which would be usable from Bloaty. [1] https://github.com/luser/rustfilt https://github.com/luser/rustfilt [2] https://github.com/alexcrichton/rustc-demangle https://github.com/alexcrichton/rustc-demangle
- ajross 9y agoThis gets confused right off the bat between file size and mapped size. The whole "strip" nonsense could have been skipped if the author looked at .text/.data/.rodata (and .bss too -- often the resource you're trying to conserve is RAM and not storage!) sizes instead. Likewise the playing with upx isn't really relevant to a rust article per se. That's a generic executable compressor that will work with anything. But some stuff here was really interesting. I honestly had no idea that rust was statically linking its own heap implementation into every binary it created! Come on, guys, jemalloc isn't even in rust and doesn't benefit from static linkage. If you're going to distribute your own runtime (and if you're using your own heap, you're distributing your own runtime!) at least distribute it in a shared-by-default way.
- steveklabnik 9y agoWe are interested in switching the default back to the system allocator, but since swapping in your own isn't stable yet, it would be a regression to not be able to include jemalloc instead. That work is underway, and when it's stable, we're likely to change the default, for exactly this kind of reason.
- jamesmunns 9y agoHey ajross, I originally included the output of `size` for each step, but I thought it made the text output too large. However in this case, the actual size of binary on "disk" is important, as we were trying to fit this binary on a small embedded linux device, so if we had shipped all of that debuginfo, it would have taken up useful space on the device (which has a total storage size of 8MB). Regarding UPX, yup, not Rust specific at all, but still relevant when talking about producing tiny binaries. Re: the allocator, steveklabnik wrote a good response, though I would add that just because something isn't written in Rust, doesn't mean it can't still be well tested, performant code. Why rewrite it in Rust if you don't have to? :) Anyway thanks for reading and thanks for the feedback!
- ajross 9y ago> I would add that just because something isn't written in Rust, doesn't mean it can't still be well tested, performant code. I know. My point was that unlike rust binaries (which can benefit from significant static optimization) jemalloc is just a blob and doesn't need to be included in every binary.
- arghwhat 9y agoI find the number for xargo, without stripping or UPX to be by far the most interesting. It almost gets you all the way (1/6th of the default release size), while not breaking anything. Stripping removes useful things from your binary, and UPX tends to be a no-go for many things.
- steveklabnik 9y agoWe've been talking about "unforking xargo", and treating libstd/etc more like any other crate, which would mean that you could do this in a more easy way. It's gonna take some time though.
- nixpulvis 9y agoThis sounds like a really good idea! Would there still be prebuilt versions for the sake of faster builds out of the box?
- steveklabnik 9y agoI haven’t been following the exact details, but that would make sense.
- kibwen 9y agoDetails are still up in the air. All we know for sure (decided at the Rust All-Hands last week) is that we definitely want to extend cargo in a way that obviates xargo (a plan which xargo's maintainer has enthusiastically endorsed).
- JoshTriplett 9y agoBased on the discussions in the Rust All-Hands last week: yes, there will still be pre-built versions of std for common platforms.
- rahikainen 9y agoWow. Already 7 comments and still not a single ”Why not with Go?”-message.
- ronsor 9y agoTo be fair, it said tiny binaries. I love go, but it does not make tiny binaries.
- jug 9y agoIt does compared to all inclusive .NET Core! 30 MB unzipped Hello world for Linux or Windows targets! But boy, that Hello world has some huge shoes to grow in...
- rqs 9y agoI think if somebody write enough Go, they will naturally realized that Go is not for everything (I mean it has limitation). For example, the GC: I been struggling to reduce the memory consumption of my tiny TCP keep-alive server knowing that if I made it in C, it could only use few hundreds of KB tops. But in Go, it's 10MB just for holding Go Routines and replying ping. So I reimplemented it with Rust, voilà! 500KB RES. Now I can put it on my router :D
- _o_ 9y agoI know it is about rust but I just don't see the point. If you want for whatever reason small binaries, rust is just wrong, take c (or asm). The closest you go to processor instructions, the smaller the binary is and rust is compleatly wrong lang. for that. Actually I think go has beaten rust at anything relevant... (I am c/c++ fan so I have no preference to either. Rust has become hacker news click bait.)
- buster 9y agoCan you explain why? How is C closer to processor instructions then Rust?
- ShroudedNight 9y agoMy experience with using C was that, while it isn't necessarily closer to processor instructions, the instructions it does generate are far more predictable than the ones generated by Rust. And when it does surprise you, it's usually either a pleasant occurrance, or a compiler defect. This is a combination of many things, the big ones being: 1) My temporal experience with C dwarfs my temporal experience with Rust by at least an order of magnitude 2) The Rust ABI is [still - last I checked] opaque. 3) Every operating system environment I have interacted with has been deeply C-oriented, so the platform utilities for exploring the C to ELF / PE transition are much more accessible. I would love to be able to have the same familiarity, predictability, and indegenous feeling with Rust that I have with C, but each time I attempt to interact with the ecosystem, I come away feeling alienated and like I've just interacted with a 'the compiler knows best' cult.
- _o_ 9y agoThe only problem is that for some, rust is a silver bullet that is going to bridge lack of system dev. by learning another language. Sorry it does not work this way. C is just fine, we don't need rust, it can probably be implemented as another preprocessor layer of c. And quite frankly, I am failing to see, what benefits it provides if I throw c++ in equation.
- dom96 9y agoFor comparison's sake I just compiled the Jester hello world example using Nim: import asyncdispatch import jester routes: get "/": resp "Hello World" runForever() Command: nim c -d:release --opt:size file.nim The size on my Mac is 349K and that's with very little tweaking.
- steveklabnik 9y agoThis comparison is impossible without knowing the relative sizes of jester vs rocket, and how much space they take up of their effective binaries.[1] As I mentioned below, Rocket isn't really size-optimized yet, maybe it would be significantly smaller if it were. Maybe Jeseter is just far more lean-and-mean than Rocket is. You can't draw whole conclusions between two languages with just one tiny command; even the OP, comparing a language to itself, ran a whole lot more in-depth analysis to figure out what costs were being paid where. It is cool it’s that small so easily though! 1: shout-out to https://crates.io/crates/cargo-bloat https://crates.io/crates/cargo-bloat which is very cool
- dom96 9y agoI agree to a certain extent, but it's still a useful look at how Nim's leading web framework compares to Rust's leading web framework. I'm not saying we should draw conclusions based on just this comparison. But it is an additional nugget of information and an invitation for others to also give Nim a try when targeting embedded systems :)
- kbenson 9y ago> it's still a useful look at how Nim's leading web framework compares to Rust's leading web framework. I'm not sure Rocket is Rust's leading framework. It certainly looks nicer than some, but still requires Rusts's nightly build, IIRC. I'm sure that means a lot of people opt for something like actix-web, or just fall back on hyper. My impression is that Rust's users are pretty widely spread across the frameworks that exist, so it may not be that you picked the wrong framework, but that isn't really a "leading" framework yet. Someone might jump in with better info on the usage rates than me and correct me though, since this is mostly observational on my part. :)
- kibwen 9y agoNote that one of these steps, the one where you replace jemalloc with the system allocator, is quite likely to become the default behavior sometime after the global allocator API stabilizes (which should happen this year). It's a bit of a shame because jemalloc really is faster than the system allocator, often by a significant margin, but in practice jemalloc is a very large burden to package in-tree, plus it largely contributes to binary bloat as seen here, and also tools like valgrind don't play nicely with anything other than the system allocator. Of course, we won't make the switch until we have an easy way to get jemalloc back via crates.io, for our users who really want jemalloc's performance benefits.
- GrayShade 9y agoI think GLIBC 2.26 added a per-thread cache, so it might no longer be that slow when compared to jemalloc.
- kibwen 9y agoI'm under the impression that what also makes jemalloc fast is that it has a richer API than glibc malloc (in a way that isn't drop-in compatible, so libc-compatible malloc can't ever provide it), thereby giving it more info that it can use for optimization (but also stymying tools like valgrind).
- GrayShade 9y agoThat's sized deallocation, right? Unfortunately, GLIBC doesn't seem to provide it.