Haven'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.
Even 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.
Templates 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.
Templates 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).
>If I call a function from a library, and that library function depends on catching and handling exceptions
This doesn't make any sense. If a function from a library depends on catching and handling an exception then it can continue to do so. Using -fno-rtti doesn't change the semantics of functions that were compiled without that flag, they can continue working as usual.
All -fno-rtti does is treat your program as if any exception thrown goes unhandled, which in C++ means that std::terminate is called.
If you're saying you have a function that throws an exception and that function expects someone else to catch it, then the problem isn't with -fno-rtti, the problem is with your function's expectations. That means your function is using exceptions as a form of control flow, which is not a recommended practice. The primary goal and intention of exceptions is that the function that throws it is agnostic about how the exception is handled, or whether the exception is handled at all.
A function fails to meet its post-condition can throw an exception. If that exception gets caught then the receiver of that exception makes the decision on to proceed (not the thrower), if that exception does not get caught then std::terminate is called. All -fno-rtti does is guarantee that any exception thrown goes uncaught.
This is the least surprising behavior one can expect. What possible alternative behavior would you want from a function that throws an exception that goes uncaught?
You seem to be completely misunderstanding what I'm saying. Of course a function compiled without -fno-rtti or -fno-exceptions can keep using those features. There's a reason I keep talking about functions defined in headers. Those functions are compiled in your program's translation units, with your program's compiler options.
And you seem to completely misunderstand how -fno-rtti works but you speak as if you're knowledgeable about it despite the fact that it's absolutely obvious you've never even bothered to try it yourself or read the documentation for it.
It's very hard to understand someone who is repeatedly making false claims but doesn't realize it.
If you don't know how something works, please avoid making claims about it as if you did know such as:
>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).
That's a false claim and saying it does nothing but create confusion and spread misinformation. Especially since you're referencing ODR violations which to someone who doesn't know better might be fooled into thinking your claim is more credible than it really is (there is no ODR violation).
I didn't know that certain compilers (though not all) will error in that situation. I'm sorry. You will only have fun unexpected behaviors on some compilers.
Maybe we could've gotten to this point faster though if you didn't keep responding as if I wasn't talking about library functions defined in headers.
>An inline function that depends on RTTI will fail to compile when -fno-rtti is enabled.
Note that you completely ignored that point.
>You will only have fun unexpected behaviors on some compilers.
I made it very clear, as did OP, that we were discussing the use of -fno-rtti. There is not a single compiler where that flag does not behave the way as I described.
You are discussing MSVC which does not have an -fno-rtti but instead uses a /GR- which behaves differently from -fno-rtti.
When discussing detailed matters involving C++ compilers and toolchains, precision matters. Especially since clang has a compatibility mode with MSVC where it too implements both /GR- and -fno-rtti and treats them as separate flags with different behaviors.
That's assuming you don't include a header that will somewhere have some
#if defined(__cpp_rtti)
Some ABI- or ODR- breaking code
#endif
Given how much of these ifdef I can find by grepping on the repos I have cloned on my hard drive, I would in practice really not be too confident about mixing -fno-rtti with -frtti code
It'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.
> 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.
If 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.
-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
Agreed, 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.
You'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.
Correct! It's a language defined ad-hoc by GCC and Clang. GCC's man page describes its behavior with '-fno-exceptions' to be essentially: act like a normal C++ compiler, except produce code which calls abort() where it would otherwise have engaged the exception throwing machinery. That defines a kind of "C++ with exceptions disabled" language, but it's not a standard.
> And this "C++ with exceptions disabled" language defines that exceptions terminate the program.
Except that's not a language that is standardized, so there's nothing that defines anything here. GCC and Clang may call std::terminate() when exceptions are disabled and some library/dependency throws, but unless your build process aborts when a compiler other than GCC or Clang is used, you can't rely on that behavior in general.
As I wrote in response to your sibling comment which pointed out exactly the same thing:
Correct! It's a language defined ad-hoc by GCC and Clang. GCC's man page describes its behavior with '-fno-exceptions' to be essentially: act like a normal C++ compiler, except produce code which calls abort() where it would otherwise have engaged the exception throwing machinery. That defines a kind of "C++ with exceptions disabled" language, but it's not a standard.
I 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.
Considering 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.
i 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.
Program 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.
I 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.
I agree. There are multiple thresholds where you care: the program is so large it cannot be linked; the program is too large to economically distribute; the program doesn't fit on the target media; the program would be faster if all hot code fit in L1i; the program is intended for competitive demos. Which are we discussing?
One 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.
-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.
I 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?
If 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].
There'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.
The 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.
This 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.
I don't see how your claim follows or would be true. Replacing CSS with the equivalent JavaScript would most certainly not reduce load times. In fact you should use CSS as a means of reducing load times.
That's a different discussion and frankly not something for developers to decide. If a product is designed to behave and look a certain way, then as an engineer you have the job of determining the best way to accomplish that objective. If your decision is to forgo using CSS for an alternative, fine but no one has provided what that alternative is.
I'd much rather use CSS instead of Javascript or going back to the days of hacking <table> to get a certain layout.
I think that's quite unfair. The article contains many different tips on how to reduce size. Rewriting in a different language was just one of them, pretty far down on the list.
"Write in C instead of C++" is also clearly not the same as "remove all your code"...
He also recommends disabling exceptions which is a valid approach for size constrained applications. STL throws, so it can never be used safely with exceptions disabled. When you do this you're committing to writing C-with-classes rather than modern C++.
There have been implementations of most of the stl without exceptions here and there for embedded stuff. You can do modernish c++ without rtti or the stl, you will just have to do some extra work.
I agree with the sentiment but in practice I’ve found that most C++ STL exceptions throw in a “fatal error” type of scenario like a bad allocation and generally not an “expected error”. For example, basic_ifstream::open() sets a fail bit on error, and doesn’t throw an exception.
This is in contrast to python or Swift for example, their standard libraries are more “throw-prone”. Building off the previous example Swift’s String.init(contentsOf:encoding:) throws on error on failure.
So in practice, IMO it is usually safe to disable exceptions in C++. Though, I have run into tricky ABI breaks when you link multiple libraries in a chain of exceptions->noexcept->exceptions and so on! You’re of course at the mercy of nonstandard behavior so buyer-beware. I definitely wouldn’t advocate for turning them off -just- for a binary size reduction.
You can recover from failed allocations without catastrophic failure. It is a fundamentally lazy programming practice to pretend that error handling has to be an out of band operation that can't be dealt with locally and bubble up progressively.
You can try but realistically, you shouldn't bother in the overwhelming majority of software. Depending on whether you're on a platform that allows for overcommit, you won't necessarily know that an allocation has failed until you attempt to make use of it and the OS tries to back the pages, by which point, you could be far from the source of the allocation.
You're just going to end up with an insane amount of error handling only to discover that in the real world, there's likely nothing you can really do anyway.
On platforms that allow overcommitment, you can guarantee your commit charge is physically backed by writing to each page in a memory pool at allocation time (probably at application startup, or at the end of the main loop), then allocating out of that pool.
Using memory that's been allocated but not committed seems like a recipe for disaster.
This is pretty uncontroversial in an embedded system context. As others have said in this thread, nothing spectacular happens if STL throws, it just boils down to a std::terminate. You can mitigate this by being careful with what you do with STL.
Also, in a real-time system context, exceptions can be undesirable since they might cause non-deterministic behaviour.
Catching an exception can be surprisingly costly. Did some benchmarking a while ago on the embedded, real-time system I work on and saw that throwing and catching a std::runtime_error had about the same execution time as a rather slow CRC32 calculation (no pre-calculated tables, no special instructions) of a 256 bytes input array. (Of course, this depends a lot of the CPU architecture, compiler, etc.)
I haven't read the article yet, but it's standard operating procedure for embedded.
* No exceptions, ever
* No RTTI, ever
* Compile with -Os
* Limit STL to the non-allocating, non-throwing parts
* Don't allow heap usage
* Link against the C standard library so you don't get accidental heap allocations from lambdas with large capture specs and static construction and destruction fail to link
* Use alternatives to std::function (which while easy is very large and very slow)
* Limit the use of virtual functions (some people suggest eliminating them altogether but I feel like that's a bridge too far).
* Avoid inlining functions if you're not sure that they reduce down to something trivial. I've saved a kilobyte or two by moving some particularly large, commonly used functions into separate translation units so that the compiler couldn't inline them.
* Look at and track your space utilization commit by commit. If the data segment goes up unexpectedly, revisit what you're doing. You may have unwittingly allocated a big block for something. Similar for the code segment.
You get used to the constraints. Type safety, templates, and compile time programming make it 10 times better than C for the purpose in my experience. The only reason to use C is if your C++ compiler is garbage (which is often true for the tiny low powered processors). But if it's ARM you probably have access to a modern compiler and the only reason to stick with C is inertia.
I've been programming with embedded C++ for ten years and every time I have to poke into the C part of the codebase I end up hating life.
> STL throws, so it can never be used safely with exceptions disabled.
With exceptions disabled you still get a panic (e.g. the program immediately terminates) in places where an exception is thrown, this should be at least as safe as having exceptions enabled.
Not using the STL in and of itself is not terrible advice, it's a trade-off. For example, I tend to avoid the STL because of how inconsistent it is across the various compilers in terms of performance, bugs, and behavior. The standard leaves a great deal of ambiguity in how the STL behaves and those inconsistencies can be frustrating to deal with when you're writing cross platform software. Furthermore plenty of embedded systems also cut out the STL entirely due to disabling exceptions. Using fast-math is also a perfectly acceptable option for numerous domains that do not need strict IEEE conformance, such as graphics programming or training neural networks.
If people were to listen to your advice, no one would ever post any kind of article on how to achieve certain specific goals, all we'd ever discuss are very generic topics and constantly rehash the same content over and over again. It's good to be able to learn how certain developers manage to accomplish various goals without having someone come along and call that advice terrible.
> For example, I tend to avoid the STL because of how inconsistent it is across the various compilers in terms of performance and behavior.
What are your targets? This was my experience 10 years ago, but today with modern (last 5-6 years) MSVC, GCC and Clang (and even on modern Xbox and Playstation toolchains) my experience has been that it's close enough.
Even just between x86 and ARM there can be a big enough difference in performance s.t your product straight up does not work on one platform, and that is with the current gcc toolchain.
I have been working with small embedded devices so this may be an outlier here compared to the rest of the C/C++ world.
We target both x64 and arm, and I'm not aware of any restrictions we place on STL usage. That said, we don't target embedded devices, so your world is _definitely_ different to mine!
> If you don't need IEEE-conformat floating point calculations, use -ffast-math .
I mean… if you know enough to know whether you need standard floating-point calculations, you probably know about the -ffast-math flag.
Someone who is more into system specific stuff might correct me, but I believe fast math produces a binary which can flip bits in the MXCSR if it wants. So, you’ll have to make sure that none of your libraries make any assumptions about floating point behavior, not just your code…
> if you know enough to know whether you need standard floating-point calculations, you probably know about the -ffast-math flag.
If you just want physics engine go brr and a small binary, though, maybe you don’t.
I find the FP advice suspect for a different reason: I’ve seen some no good, very bad, seriously terrible, really horrifyingly awful codegen from Clang for x86-64 things involving `long double`, and without checking first I wouldn’t assume that it doesn’t extend to all x87 stuff in general.
(Clang’s preferred optimized way to copy a naturally-aligned 16-byte union with a `long double` being the longest member is... to copy the first ten bytes with FLD/FSTP, then the following two bytes of padding by a word-sized MOV, then the final four bytes, also of padding, by a doubleword-sized MOV. Apparently it forgot its MOVAPS at home.
When it spills onto the stack a `long double` temporary it itself invented to implement an atomic operation’s inner loop, though, it first clears the padding using a bleeding XORPS+MOVAPS pair before FSTPing it there—inside the loop, twice, to two identical temporaries, because apparently it belongs to the offshore oil rig school of register spilling.
And so on. I lack the words to describe how bad Clang’s codegen is for anything that might have ever briefly brushed against a `long double`.)
Also take care with -Os; while clang’s -Os is generally sane, gcc -Os will e.g. implement division by a constant with IDIV instead of magic multiplication, giving you code that’s maybe three times shorter and easily a dozen or two dozen times slower. (I think I remember Torvalds saying years ago that he gave up on gcc -Os.)
> (Clang’s preferred optimized way to copy a naturally-aligned 16-byte union with a `long double` being the longest member is... to copy the first ten bytes with FLD/FSTP, then the following two bytes of padding by a word-sized MOV, then the final four bytes, also of padding, by a doubleword-sized MOV. Apparently it forgot its MOVAPS at home.
Is that still true? Might warrant a bug filed on clang/llvm.
On second glance, surprisingly (or maybe not), this is not as easy to trigger as I thought. The somewhat messy interpreter loop I’m using still exhibits the problem on Clang 14 today (and I mean, I wrote a workaround for that a month ago, how much could it have changed in the meantime). Yet if I just write out the same union (every scalar type in C, basically) and make a function that assigns some things of that union type to some other things, I can’t get the bad codegen to manifest even on Clang 9.
So I’d need to do some work to make a small reproducer; then I’d have to figure out how to get access to their bug tracker, because apparently letting people register their own accounts is too easy, let alone just send mail to an address. I don’t know if I have that in me.
(On a more positive note, do you know that sometimes, if you add `default: __builtin_unreachable();` to your switch-threaded interpreter loop, Clang will look at it and replicate the dispatch at the end of each bytecode instruction, like in the standard technique for better branch prediction in handwritten assembly-language interpreters? When I noticed that I couldn’t stop imagining it going “It looks you’re writing an interpreter. Would you like some help?”.)
If I'm optimizing for binary size, I'm likely targeting a microcontroller that has no floating point unit, in which case the obvious size optimization is don't use floating point...
Are you aware that "most" game studios nowadays do not use C++ as a primary language anyway? AAA games are really a small portion of all gamedevs anyway, and the stance against modern C++ is not universal even in the gamedev community. Even the linked article states that C++17 is probably fine now, and that's cleanly modern.
So is using binary packers like UPX. Not just will they sound the alarm in a lot of so called security products, but they'll also prevent the operating system from sharing the RAM used to mmap() read-only and read-execute pages from the executable while increasing startup times. Removing random sections will also cause headaches when it comes to tracing and debugging in production. Just accept that those sections fulfil real world requirements and aren't just shiny nerd knobs for knobs to mess with. If your code isn't worth being traceable/debuggable in production it's not worth having it installed on production servers.
Also don't go too far with the build flags or you risk waking the sleeping horrors lurking in the seldom tested dark corners of the compilers and linkers we all rely on e.g. accidentally changing the effective ABI through tricks like smaller enums. Some of those bugs may only trigger on certain architectures e.g. exception handling works differently on amd64 and aarch64. On one of the two uses special an extra section to locate the unwind helpers. Do you know the all implications of removing a section from an executable? sigh
Please keep bloat down, but eager developers shouldn't too far beyond the point of quickly diminishing returns within a given ecosystem they're targeting.
> Not using the STL is really, really bad advice for most.
Not really, the STL is an accumulation of mediocre design decisions (mostly because it needs to be "everything to everybody"), which can't be fixed because of backward compatibility requirements. In many situations it's actually better to write your own specialized helper classes.
I don't think many actually use the STL besides app dev's. Most systems dev's are targeting WIN32, RTL, UEFI, POSIX, BOOST, UNREAL, or something else. Not only this, but STL also breaks alot of things by what it puts in, Kernel, Unreal, and results in alot of usecases to not use the STL.
The only time in my entire career i've ever used the STL was in college. Since most C++ jobs are not Appdevs. All those have moved to C#, Go, or some other higher level language. Or again, just rely on the built in suite like RTL, Posix, Unreal
There 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.
That'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.
Static 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.
I haven't yet found convincing arguments other than cargo cult programming ones. But I'd be glad to hear concrete examples if you have some and I'll be happy to discuss.
if you don't optimize for size, more things get inlined. LTO creates more inlining opportunities, so plain LTO will increase your binary size. A complicating factor is small functions often inline smaller than leaving them outlined, since you no longer need call and return infrastructure.
I’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+.
If 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.
Unfortunately, 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.)
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.
I 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.
When I worked on Matter a couple years ago, we had the problem that its backend http://www.capstone-engine.org/ did not support Xtensa, and produced some Python tools that could take output from bloaty or similar data from readelf or elftools, and produce several kinds of report.
Came 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.
For 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".
On 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/), 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).
Oddly, 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.
The 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.
130 comments
[ 3.3 ms ] story [ 191 ms ] thread> 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).
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.
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.
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.
> 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).
This doesn't make any sense. If a function from a library depends on catching and handling an exception then it can continue to do so. Using -fno-rtti doesn't change the semantics of functions that were compiled without that flag, they can continue working as usual.
All -fno-rtti does is treat your program as if any exception thrown goes unhandled, which in C++ means that std::terminate is called.
If you're saying you have a function that throws an exception and that function expects someone else to catch it, then the problem isn't with -fno-rtti, the problem is with your function's expectations. That means your function is using exceptions as a form of control flow, which is not a recommended practice. The primary goal and intention of exceptions is that the function that throws it is agnostic about how the exception is handled, or whether the exception is handled at all.
A function fails to meet its post-condition can throw an exception. If that exception gets caught then the receiver of that exception makes the decision on to proceed (not the thrower), if that exception does not get caught then std::terminate is called. All -fno-rtti does is guarantee that any exception thrown goes uncaught.
This is the least surprising behavior one can expect. What possible alternative behavior would you want from a function that throws an exception that goes uncaught?
It's very hard to understand someone who is repeatedly making false claims but doesn't realize it.
If you don't know how something works, please avoid making claims about it as if you did know such as:
>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).
That's a false claim and saying it does nothing but create confusion and spread misinformation. Especially since you're referencing ODR violations which to someone who doesn't know better might be fooled into thinking your claim is more credible than it really is (there is no ODR violation).
Maybe we could've gotten to this point faster though if you didn't keep responding as if I wasn't talking about library functions defined in headers.
>An inline function that depends on RTTI will fail to compile when -fno-rtti is enabled.
Note that you completely ignored that point.
>You will only have fun unexpected behaviors on some compilers.
I made it very clear, as did OP, that we were discussing the use of -fno-rtti. There is not a single compiler where that flag does not behave the way as I described.
You are discussing MSVC which does not have an -fno-rtti but instead uses a /GR- which behaves differently from -fno-rtti.
When discussing detailed matters involving C++ compilers and toolchains, precision matters. Especially since clang has a compatibility mode with MSVC where it too implements both /GR- and -fno-rtti and treats them as separate flags with different behaviors.
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.
Note that closing system resources is often abortive. On the other hand, object destructors might wait for a flush.
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.
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.
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.
https://gcc.gnu.org/onlinedocs/libstdc++/manual/using_except...
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.
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.
not a language standard i was previously aware of.
Except that's not a language that is standardized, so there's nothing that defines anything here. GCC and Clang may call std::terminate() when exceptions are disabled and some library/dependency throws, but unless your build process aborts when a compiler other than GCC or Clang is used, you can't rely on that behavior in general.
Correct! It's a language defined ad-hoc by GCC and Clang. GCC's man page describes its behavior with '-fno-exceptions' to be essentially: act like a normal C++ compiler, except produce code which calls abort() where it would otherwise have engaged the exception throwing machinery. That defines a kind of "C++ with exceptions disabled" language, but it's not a standard.
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.
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.
https://pigweed.dev/docs/size_optimizations.html
The first affects the layout of object files. The second affects the optimizations done when linking.
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.
But you're right, it's also possible to use it for single-translation-unit build-and-link.
https://interrupt.memfault.com/blog/memcpy-newlib-nano
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
2. https://doi.org/10.1145/3503222.3507744
If you're in an situation where binary size is absolutely critical, then sure. But most people should avoid most of these suggestions.
I'd much rather use CSS instead of Javascript or going back to the days of hacking <table> to get a certain layout.
"Write in C instead of C++" is also clearly not the same as "remove all your code"...
This is in contrast to python or Swift for example, their standard libraries are more “throw-prone”. Building off the previous example Swift’s String.init(contentsOf:encoding:) throws on error on failure.
So in practice, IMO it is usually safe to disable exceptions in C++. Though, I have run into tricky ABI breaks when you link multiple libraries in a chain of exceptions->noexcept->exceptions and so on! You’re of course at the mercy of nonstandard behavior so buyer-beware. I definitely wouldn’t advocate for turning them off -just- for a binary size reduction.
You're just going to end up with an insane amount of error handling only to discover that in the real world, there's likely nothing you can really do anyway.
Using memory that's been allocated but not committed seems like a recipe for disaster.
It can greatly accelerate sparse datastructures.
Also, in a real-time system context, exceptions can be undesirable since they might cause non-deterministic behaviour.
Catching an exception can be surprisingly costly. Did some benchmarking a while ago on the embedded, real-time system I work on and saw that throwing and catching a std::runtime_error had about the same execution time as a rather slow CRC32 calculation (no pre-calculated tables, no special instructions) of a 256 bytes input array. (Of course, this depends a lot of the CPU architecture, compiler, etc.)
* No exceptions, ever
* No RTTI, ever
* Compile with -Os
* Limit STL to the non-allocating, non-throwing parts
* Don't allow heap usage
* Link against the C standard library so you don't get accidental heap allocations from lambdas with large capture specs and static construction and destruction fail to link
* Use alternatives to std::function (which while easy is very large and very slow)
* Limit the use of virtual functions (some people suggest eliminating them altogether but I feel like that's a bridge too far).
* Avoid inlining functions if you're not sure that they reduce down to something trivial. I've saved a kilobyte or two by moving some particularly large, commonly used functions into separate translation units so that the compiler couldn't inline them.
* Look at and track your space utilization commit by commit. If the data segment goes up unexpectedly, revisit what you're doing. You may have unwittingly allocated a big block for something. Similar for the code segment.
You get used to the constraints. Type safety, templates, and compile time programming make it 10 times better than C for the purpose in my experience. The only reason to use C is if your C++ compiler is garbage (which is often true for the tiny low powered processors). But if it's ARM you probably have access to a modern compiler and the only reason to stick with C is inertia.
I've been programming with embedded C++ for ten years and every time I have to poke into the C part of the codebase I end up hating life.
Lambdas themselves never require heap allocation. I guess you meant std::function?
With exceptions disabled you still get a panic (e.g. the program immediately terminates) in places where an exception is thrown, this should be at least as safe as having exceptions enabled.
If people were to listen to your advice, no one would ever post any kind of article on how to achieve certain specific goals, all we'd ever discuss are very generic topics and constantly rehash the same content over and over again. It's good to be able to learn how certain developers manage to accomplish various goals without having someone come along and call that advice terrible.
What are your targets? This was my experience 10 years ago, but today with modern (last 5-6 years) MSVC, GCC and Clang (and even on modern Xbox and Playstation toolchains) my experience has been that it's close enough.
I have been working with small embedded devices so this may be an outlier here compared to the rest of the C/C++ world.
> If you don't need IEEE-conformat floating point calculations, use -ffast-math .
I mean… if you know enough to know whether you need standard floating-point calculations, you probably know about the -ffast-math flag.
Someone who is more into system specific stuff might correct me, but I believe fast math produces a binary which can flip bits in the MXCSR if it wants. So, you’ll have to make sure that none of your libraries make any assumptions about floating point behavior, not just your code…
If you just want physics engine go brr and a small binary, though, maybe you don’t.
I find the FP advice suspect for a different reason: I’ve seen some no good, very bad, seriously terrible, really horrifyingly awful codegen from Clang for x86-64 things involving `long double`, and without checking first I wouldn’t assume that it doesn’t extend to all x87 stuff in general.
(Clang’s preferred optimized way to copy a naturally-aligned 16-byte union with a `long double` being the longest member is... to copy the first ten bytes with FLD/FSTP, then the following two bytes of padding by a word-sized MOV, then the final four bytes, also of padding, by a doubleword-sized MOV. Apparently it forgot its MOVAPS at home.
When it spills onto the stack a `long double` temporary it itself invented to implement an atomic operation’s inner loop, though, it first clears the padding using a bleeding XORPS+MOVAPS pair before FSTPing it there—inside the loop, twice, to two identical temporaries, because apparently it belongs to the offshore oil rig school of register spilling.
And so on. I lack the words to describe how bad Clang’s codegen is for anything that might have ever briefly brushed against a `long double`.)
Also take care with -Os; while clang’s -Os is generally sane, gcc -Os will e.g. implement division by a constant with IDIV instead of magic multiplication, giving you code that’s maybe three times shorter and easily a dozen or two dozen times slower. (I think I remember Torvalds saying years ago that he gave up on gcc -Os.)
Is that still true? Might warrant a bug filed on clang/llvm.
So I’d need to do some work to make a small reproducer; then I’d have to figure out how to get access to their bug tracker, because apparently letting people register their own accounts is too easy, let alone just send mail to an address. I don’t know if I have that in me.
(On a more positive note, do you know that sometimes, if you add `default: __builtin_unreachable();` to your switch-threaded interpreter loop, Clang will look at it and replicate the dispatch at the end of each bytecode instruction, like in the standard technique for better branch prediction in handwritten assembly-language interpreters? When I noticed that I couldn’t stop imagining it going “It looks you’re writing an interpreter. Would you like some help?”.)
Templates and operators overloading are also quite often frowned up.
https://gist.github.com/raizr/c08922b11b33477a1157156e424342...
It would be terrible advice if it were framed as "do all these things."
Also don't go too far with the build flags or you risk waking the sleeping horrors lurking in the seldom tested dark corners of the compilers and linkers we all rely on e.g. accidentally changing the effective ABI through tricks like smaller enums. Some of those bugs may only trigger on certain architectures e.g. exception handling works differently on amd64 and aarch64. On one of the two uses special an extra section to locate the unwind helpers. Do you know the all implications of removing a section from an executable? sigh
Please keep bloat down, but eager developers shouldn't too far beyond the point of quickly diminishing returns within a given ecosystem they're targeting.
Not really, the STL is an accumulation of mediocre design decisions (mostly because it needs to be "everything to everybody"), which can't be fixed because of backward compatibility requirements. In many situations it's actually better to write your own specialized helper classes.
The only time in my entire career i've ever used the STL was in college. Since most C++ jobs are not Appdevs. All those have moved to C#, Go, or some other higher level language. Or again, just rely on the built in suite like RTL, Posix, Unreal
- 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.
https://github.com/google/bloaty
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.)
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.
When I worked on Matter a couple years ago, we had the problem that its backend http://www.capstone-engine.org/ did not support Xtensa, and produced some Python tools that could take output from bloaty or similar data from readelf or elftools, and produce several kinds of report.
https://github.com/project-chip/connectedhomeip/blob/master/...
- 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
[2] https://in4k.github.io/wiki/about
[3] http://www.sizecoding.org/wiki/Main_Page
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.
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".
https://www.bell-labs.com/usr/dmr/www/vararray.html
Regarding compression, demoscene tools like kkrunchy, 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).