25 ms·
How to make smaller C and C++ binaries
- wyldfire 3y ago> In C++, use -fno-exceptions if your code doesn't use exceptions. > In C++, use -fno-rtti if your code doesn't use RTTI (run-time type identification) or dynamic_cast. Note that "your code" here generally must encompass "your code" and all of your C++ archives/shared object dependencies (and their dependencies).
- lldb 3y agoYou can usually compile each library independently with/without exceptions and rtti and it works as expected.
- mhh__ 3y agoWorks as expected or silently fails to destruct things properly during unwinding?
- mort96 3y agoHaven't tried, but it almost certainly fails in fun and unexpected ways I'd guess. Code which happens to be in your dependencies' source files probably works as expected, code which happens to be in your dependencies' source files will be compiled without RTTI/exceptions and therefore possibly break in fun and interesting ways. And I wonder what happens if your code creates a class which inherits from a base class defined in a library and that library expects anything which inherits from that base class to have runtime type info.
- Kranar 3y agoNothing breaks in fun or interesting ways. All that happens is std::terminate is called.
- mort96 3y agoEven when a template or inline function depends on RTTI but is called in a source file compiled with -fno-rtti? And if a template or inline function depends on catching an exception, you're right that your app unexpectedly crashing due to std::terminate isn't "fun or interesting" but it's not really great either.
- Kranar 3y agoTemplates and RTTI do not have anything to do with each other. An inline function that depends on RTTI will fail to compile when -fno-rtti is enabled. There is also nothing unexpected about an application immediately terminating due to an exception. That's the most expected outcome in an application that has made an explicit choice to not handle exceptions and is what happens in any application that does not have a try/catch handler.
- mort96 3y agoTemplates and inline functions are relevant because they're in headers and therefore compiled as part of your project's translation units. So if a function from a library depends on RTTI, and it's compiled as part of your project's translation units with -fno-rtti, something will break (or maybe you'll have an ODR violation). > There is also nothing unexpected about an application immediately terminating due to an exception. That's the most expected outcome in an application that has made an explicit choice to not handle exceptions and is what happens in any application that does not have a try/catch handler. If I call a function from a library, and that library function depends on catching and handling exceptions (maybe it uses a stdlib function which throws, such as vector::at), it's unexpected that my application crashes due to that exception. Yet, if that library function happens to be in a header, calling it will crash my program (or, again, you have an ODR violation and anything could happen).
- Kranar 3y agoYou get a compiler error.
- Calavar 3y agoIt's similar to slapping noexcept on every function in a compilation unit. Unwinding will still happen as usual in compilation units where exceptions were enabled. When a function in an -fno-exceptions compilation unit calls a function that might throw exceptions, the compiler inserts an exception handler that calls std::terminate. std::terminate won't call any destructors further up the chain, but if all you're doing in destructors is deallocating memory and releasing handles to system resources, this shouldn't matter.
- inetknght 3y ago> if all you're doing in destructors is deallocating memory and releasing handles to system resources, this shouldn't matter. Note that closing system resources is often abortive. On the other hand, object destructors might wait for a flush.
- wyldfire 3y ago> if all you're doing in destructors is deallocating memory and releasing handles to system resources, this shouldn't matter. That "if" is doing some heavy lifting. C++ places a lot of stock in RAII, so failing to call destructors could cause lots of interesting failure modes. I don't think it's fair to dismiss this fundamental part of the language so easily.
- Calavar 3y agoIf the user kills your program with Ctrl-C, you don't have any destructor guarantees. If your program is killed by the OS because the user logged off or powered down their machine, you don't have any destructor guarantees. If your program fails an assertion, you don't have any destructor guarantees. If your program segfaults, you don't have any destructor guarantees. This really isn't that exotic of a scenario. My personal opinion is that using RAII to do anything that you wouldn't trust the OS do for you in the event of an early termination is setting yourself up for disaster.
- hamilyon2 3y agoDo you have a newsletter that I can sign up for?
- jcelerier 3y ago-fno-exceptions does not disable unwinding support afaik, for this you need -fno-unwind-tables / -fno-asynchronous-unwind-tables ; if I'm not mistaken unwind table generation is enabled even in C with GCC and clang
- pjmlp 3y agoIt works by chance more than anything.
- lelanthran 3y agoAgreed, but how certain can you be that the program generates no exceptions? Simply instantiating a pointer to class using new uses exceptions. Without exceptions all constructors that fail turn into undefined behaviour. The caller will never know that the instance they are using is filled with random bytes.
- ridiculous_fish 3y agoI'm not sure that a failing constructor is UB under -fno-execptions. The libstdc++ manual suggests that these will instead abort: https://gcc.gnu.org/onlinedocs/libstdc++/manual/using_exceptions.html#intro.using.exception.no https://gcc.gnu.org/onlinedocs/libstdc++/manual/using_except...
- zabzonk 3y ago> I'm not sure that a failing constructor is UB under -fno-execptions the c++ standard does not allow for disabling exceptions, so at best this would be implementation-dependent behaviour, at worst UB. in other words, using -fno-execptions, and similar on other compilers, raises compatibility problems.
- mort96 3y agoYou're right, the C++ standard does not allow for disabling exceptions. So C++ with exceptions disabled is not a language described by the standard. We instead have to look at the documentation of this "C++ with exceptions disabled" language. And this "C++ with exceptions disabled" language defines that exceptions terminate the program. There is no UB anywhere here, everything is well-defined. You're right again though that there are compatibility problems, you could hypothetically use a compiler whose '-fno-exceptions' option actually defines throwing exceptions to be UB. No compiler would do that since it's obviously insane, but it is a possible alternate "C++ with exceptions disabled" language. Make sure to not use such compilers. And it's probably a good idea to keep your code valid C++ anyways, meaning a standard C++ compiler will terminate the program due to an uncaught exception while GCC and Clang with -fno-exceptions will terminate the program by calling abort(). This way, GCC and Clang's -fno-exceptions behavior is just an optimization for when compiling on those compilers.
- charcircuit 3y agoI wish this article included a benchmark of a large project and showed the difference each option made. I have a feeling most of these don't have much of an affect.
- alpaca128 3y agoConsidering one of the listed options talks about "several kilobytes" of reduced size it's definitely not all a huge improvement. A standard stripping of debug information is probably one of the biggest and easiest steps. Using upx can also make a significant difference, but unfortunately not every OS likes the result. Mac OS just immediately killed my program when it was compressed like that.
- zabzonk 3y agoi don't think any of these will greatly, if at all, increase performance. they are of interest for people that are a) obsessive about program size or b) want to fit the compiled code into some very small storage device, such as a rom.
- nrclark 3y agoProgram size has a lot of interaction with performance, by way of your CPU's L2/L3 cache. If your program is too large, you get more cache misses when branching around. Frequent cache misses have a _huge_ effect on your code's overall performance, and can completely negate any of the wins you get from loop unrolling or other optimizations. This effect is very pronounced on small armv7l/aarch64 CPUs, which usually have much smaller caches than a desktop CPU. You can even see it on laptop CPUs too. It's impossible to know where exactly your size cutoffs are, because they depend a lot on what else is running in your system (and CPU core allocation, etc). But they 100% exist, for any program on any system. If your whole program can fit into your CPU cache, you'll see a big performance difference vs having to fetch it from RAM all the time.
- charcircuit 3y agoI never said "performance benchmark." By benchmark I meant you take a few programs like Chromium, clang, etc and measure how much savings each option gives.
- josefx 3y agoOne thing I often do on Linux for so files: default symbol visibility to hidden / windows compatible. A downside is that your dependencies have to set the visibility attribute correctly, but that can still be worked around by wrapping includes between push/pop pragmas.
- mierle 3y agoI work on Pigweed, a suite of embedded C++ tools and libraries. A major focus for us is size optimization, and we've written up some of our learnings: https://pigweed.dev/docs/size_optimizations.html https://pigweed.dev/docs/size_optimizations.html
- stabbles 3y agoIs there any difference between "-ffunction-sections -fdata-sections -Wl,--gc-sections" and gcc's -fwhole-program?
- mgaunard 3y agoYes, they do completely different things. The first affects the layout of object files. The second affects the optimizations done when linking.
- leni536 3y ago-fwhole-program is a compile-time optimization, not link-time. You shouldn't link anything to a translation unit compiled with -fwhole-program, as the whole point of it is to compile the whole application in a single translation unit and tell the compiler to optimize accordingly. It can also eliminate unused data and functions, as it basically treats most entites as internal linkage, and that's one of the optimisations applied there. The mechanism is rather different to GCing sections at link time though. I assume LTO has something similar. It's useful to combine it with unity builds. It's poor man's LTO in a way.
- mgaunard 3y agowhole-program only makes sense when you're building and linking at the same time, so it is typically used together with LTO. But you're right, it's also possible to use it for single-translation-unit build-and-link.
- markus_zhang 3y agoI have heard that embedded developers sometimes build their own libc to minimize the size of the binary file. I'm wondering which is a good example (readable source, good coding techniques) that I can learn from?
- ghotli 3y agoYou're probably looking for something like this. Newlib is the libc in play here. Not exactly what you're asking for, but adjacent at least. https://interrupt.memfault.com/blog/memcpy-newlib-nano https://interrupt.memfault.com/blog/memcpy-newlib-nano
- markus_zhang 3y agoThanks, looks like the thing I'm looking for.
- boomanaiden154 3y agoIf you're using Clang/LLVM you can also enable ML inlining[1] (assuming you build from source) which can save up to around 7% if all goes well. There are also talks of work on just brute forcing the inlining for size problem for embedded releases for smallish applications. It's definitely feasible if the problem is important enough to you to throw some compute at it [2]. 1. https://github.com/google/ml-compiler-opt https://github.com/google/ml-compiler-opt 2. https://doi.org/10.1145/3503222.3507744 https://doi.org/10.1145/3503222.3507744
- Night_Thastus 3y agoThere's also some really terrible advice in here. Not using the STL is really, really bad advice for most. Same with fast-math. Same with "just write C instead lol". If you're in an situation where binary size is absolutely critical, then sure. But most people should avoid most of these suggestions.
- coliveira 3y agoThe stated goal of the article is to reduce binary size. If you want to use C++ and STL you already have decided that binary size is secondary. By the way, the article even mentions rewriting some routines in assembly.
- lozenge 3y agoThis is like saying "how to reduce the load time of your web app - remove all CSS". Technically true, but not useful for most web apps. People can want faster loading without removing all styling.
- mgaunard 3y agoThere are two paths towards building small binaries: - build all of your dependencies yourself (including standard libs) and ensure they're built with LTO, and link everything statically. Everything you don't actually need will be removed. - link everything dynamically. The total image size is huge, but technically it's not part of your binary, and can be shared with other binaries as well.
- vvanders 3y agoThat's actually one thing that I really like about Rust, since everything is static linkage by default it's easy to make pretty sweeping changes, on nightly you can even recompile the stdlib with different configurations(I.E. Oz/Os) if you want.
- menaerus 3y agoStatic linkage is not there only by default but it is actually the only option because Rust doesn't have a stable ABI. I personally don't consider this as an advantage and in C++ you can also statically link against the standard library if you wish to. I don't see many valid reasons to do so but the option is there.
- mgaunard 3y agoin practice ABI compatibility is holding back libstdc++ more than anything.
- rbrown46 3y agoI’ve gotten good insight into what takes up space in binaries by profiling with Bloaty McBloatface. My last profiling session showed that clang’s ThinLTO was inlining too aggressively in some cases, causing functions that should be tiny to be 75 kB+. https://github.com/google/bloaty https://github.com/google/bloaty
- chc4 3y agoIf you can run PGO, it will take the profiling information into account when doing inlining heuristics, which can help a lot in some cases. Technically that is general optimization for speed and not size, though, so if you really care specifically for binary size you'd probably still have to muck about with noinline attributes and such.
- mananaysiempre 3y agoUnfortunately, PGO done the default way is antithetical to reproducible builds. You can avoid that by putting the profiling data in your VCS, but then you suffer of all the consequences of a version-controlled binary blob, one heavily dependent on other files at that. Perhaps it should be possible use profiling data to keep human-managed {un,}likely or {hot,cold} annotations up to date? How valuable are PGO’s frequencies compared to these discrete-valued labels? (I know GCC allows you to specify frequencies in the source, but that sounds less than convenient.)
- ghotli 3y agoI spent a lot of time with bloaty for our embedded application and found I had more actionable output from something like this... nm -B -l -r --size-sort --print-size -t d ./path/to/compiler/output{.so} | c++filt > /tmp/by_size Just a lot of flags that show you size by symbol in decimal with unmangled symbols. Run it before you run `strip` in your CI pipeline or whatever preps a build for proper release.
- menaerus 3y agoI agree, bloaty seems to be good in giving a good (and quick) overview but the difficult part is drilling through the symbols to find out what the heck is happening. In that case nm/objdump/readelf are irreplaceable.
- z303 3y agoLots of resource from the demoscene, especially 1K, 4K and 64K intros - How a 64k intro is made [1] - in4k site creation of demoscene 1kb, 4kb and 8kb intros [2] - SizeCoding.org is a wiki dedicated to the art of creating very tiny programs [3] [1] http://www.lofibucket.com/articles/64k_intro.html http://www.lofibucket.com/articles/64k_intro.html [2] https://in4k.github.io/wiki/about https://in4k.github.io/wiki/about [3] http://www.sizecoding.org/wiki/Main_Page http://www.sizecoding.org/wiki/Main_Page
- GuB-42 3y agoCame to say this. The main thing is to use a good packing tool. There are also templates to set up your project correctly, usually under Visual Studio. For 64k: kkcrunchy or squishy For 1-8k: crinkler, which is both a packer and a linker For 256b or less: not enough space for a packer, use 16-bit x86 assembly and DOS .COM files And of course, find a way to get interesting music and visuals with very little data, usually, it means maths.
- charcircuit 3y agoThe main thing is arguably to use libraries or the OS as much as possible since code there doesn't typically count for demos.
- GuB-42 3y agoFor what I've seen, reusing built-in libraries or the OS is certainly one of the tools in a sizecoder toolbox, but it is not as important as I thought initially. First thing, you are usually not allowed to use libraries that are not included in a fresh install of the chosen OS. A fresh install will be done and only your executable which must be within the size limit will be copied to the "compo machine". Often, the only library functions being used are related to hardware access. A modern PC is not an Amiga, you can't just write at some address and expect the GPU to understand that it needs to set some pixel, you can't setup a DMA for your sound card to read some sound, you have to use some kind of libraries and drivers even if the only thing you want is show a dot on the screen. After setting up your display and sound, often, your code is doing all the work, with fonts being the only external asset typically used, if needed. There used to be more OS-related tricks. Like CAB-droppers for decompression, but now, you have context modeling based unpackers that are way better than the compression used in CAB, and take a few hundred bytes at most. Extremely slow, but we are talking 64k or less files here. For sound, you now have 4klang, a tiny soft synth that will generate code for interesting music in 1k or less, and that can interface with a DAW as a VST for composition. No need to mess with MIDI or whatever the OS has to offer besides "play wav".
- waynecochran 3y agoI imagine this is w huge win in large, multiple file linked binaries.: In C++, use as few template types as possible But if I am not using the STL and many other C++ features, I'll just write the code in C.
- pjmlp 3y agoEven when writing C like code, C++ has the edge with stronger type system, RAII, ability to use/implement types with bounds checking and fat pointers.
- laurentlb 3y agoOn my free time, I make demos in less than 64kB (sometimes 8kB). I use many of the advices described in this article, except that I work on Windows with Visual Studio (I'd like to compare with other compilers in the future). In particular, I disable exceptions, I compile without any standard library, and I avoid virtual methods. These are the most important things, in my experience. Regarding compression, demoscene tools like kkrunchy, Squishy (https://logicoma.io/squishy/ https://logicoma.io/squishy/), and Crinkler can give much better results than UPX. They come with their own downsides, e.g. made for Windows, decompression can be slow, outputs might trigger antivirus systems, etc. Another important advice is to check the binary size regularly. It can be difficult to predict how a code change will affect the binary (especially after compression!), so you need to test it often. > If it's feasible, rewrite your C++ code as C. Many C++ features are just free syntactic sugar, so I've decided to use them (e.g. namespaces, classes without virtuals).
- frozenport 3y agoHard to make sense without a relative scale. Are they trying to make a 20kb program 15kb?
- synergy20 3y agoI tested static build the other day,it seems zig has a much smaller static executable comparing to c, did not dive in but was quite surprised
- jeffbee 3y agoOddly, identical code folding goes unmentioned. This can be a large reduction for C++ programs that turn out to have many identical small functions like constructors of classes that are isomorphic from the machine's perspective.
- nurettin 3y agoI love the blog comment at the end that casually suggests overriding __cxa_demangle to save 100kb
- rossy 3y agoThe disclaimer at the start doesn't mention that some of these flags, like -fno-stack-protector and -Wl,-z,norelro, also affect safety. Yeah, in some cases that doesn't matter, but there are developers out there who use options like this to shave a couple of kilobytes off the download size of their normal desktop applications, and that's a very bad idea.
- stevefan1999 3y agoThat's why I prefer Zig and Rust nowadays
- pjmlp 3y agoExcept at least Rust generates quite big executables unless no_std is used.
- 1vuio0pswjnm7 3y ago(2013)