Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What significant exceptions are there to the zero overhead principle, if any? [closed]

Tags:

c++

As a (possible) example, the LLVM coding standard forbids using standard RTTI or exceptions: http://llvm.org/docs/CodingStandards.html#do-not-use-rtti-or-exceptions

Is this a good idea, or is that coding standard outdated or unreasonable for most programs?

Are there any other such features in C++ which significantly worsen the program's speed, memory usage or executable size even if you don't use them?

like image 290
Mets Avatar asked Dec 26 '22 09:12

Mets


1 Answers

Is this still a good idea, or is that coding standard outdated?

The RTTI is most definitely the most notorious violation of the zero-overhead principle, because it incurs a static cost (executable size, and initialization code) that is proportional to the number of polymorphic classes (i.e., classes with at least one virtual function), and it does not depend on how much you use it, if at all. But there is no way to really provide RTTI without some per-class overhead. That's why you can disable RTTI on most compilers if you don't need it at all, or if you want replace it with an RTTI system you have more control over (as the LLVM guys did). Nevertheless, if you have RTTI enabled, and you are not using it, the overhead is only in the form of code bloat (larger executable, larger memory space needed, larger spread of code) and loading / unloading time, and so, the run-time execution overhead is almost non-existent. But in resource-deprived environments, or for small utility programs (e.g., invoked repetitively in shell scripts), that static overhead can be too much to bear. Moreover, there aren't that many practical situations where you need a high-performance RTTI, most of the time you don't need it at all, and at other times you need it in a few special places that are usually not performance-critical in comparison to other things. LLVM is an exception here because writing a compiler involves dealing with abstract syntax trees and similar descriptive class hierarchies, which is hard to do without lots of down-casting, and since analysing these structures is the core operation that compilers do, performance of the down-casts (or other RTTI invocations) is critical. So, don't take the "don't use the RTTI" as a general rule, just know what overhead it entails, and know if it is acceptable for your application in terms of its cost / benefits.

C++ exceptions are certainly next on the list of things that could have more overhead than you would be prepared to bargain for. This is a much more controversial issue, particularly when it comes to actually characterising the overhead of exceptions overall. Evaluating the overhead of exceptions empirically is a very difficult task because it is highly dependent on usage patterns, i.e., there are different ways to use exceptions, different levels of severity (Do you use exceptions for bugs, fatal errors, exceptional conditions, or to replace every if-statement?), and there are different levels of care for error-handling (whether with or without using exceptions). And then, of course, different compilers can implement exceptions differently. The current implementations, so-called "zero-cost exceptions", is geared towards having zero run-time cost during normal execution, but that leaves quite a bit of static overhead and makes throw-to-catch execution path slower. Here is a nice overview of that. As far as exceptions being in violation of the "you only pay for what you use" principle, it is true (unless you disable them), but they are often justifiable. The basic assumption with exceptions is that you, as a programmer, intend to write robust error-handling code, and if you do handle all errors appropriately, then the overhead of exceptions will pale in comparison to the error-handling code (catch-blocks and destructors), and you will probably have a smaller and faster program than an equivalent C-style error-code implementation of the same amount of error-handling. But if you don't intend to do much error-handling (e.g., the "if anything goes wrong, just crash!" approach), then exceptions will incur significant overhead. I'm not exactly sure why LLVM banned exceptions (if I had to be blunt, I would say it's because they aren't really serious about error-handling, as far as I can see from the code I have seen from that project). So, long story short, the guideline should be "if you intend to seriously handle errors, use exceptions, otherwise, don't". But remember, this is a hotly debated topic.

Are there any other such features which violate "you only pay for what you use"?

You have named to two obvious ones, and unsurprisingly, they are the two main features that most compilers have options to disable (and are often disabled when it is appropriate to do so). There are certainly other more minor violations of the zero-overhead principles.

One notorious example is the IO-stream library (<iostream>) from the standard library. This library has often been criticized for having too much overhead for what most people need and use it for. The IO-stream library tends to pull in a lot of code, and require quite a bit of load-time initialization. And then, many of its classes like std::ostream or std::ofstream have too much run-time overhead, both for construction / destruction and for read/write performance. I think they packed a bit too many features into that library, and since most of the time, IO-streaming tasks are very simple, those features are often left unused and their overhead unjustified. But generally, I find the overhead is often acceptable, especially since most IO tasks are already very slow regardless. BTW, LLVM also bans the use of the IO-stream library, and again, that is because the target of LLVM is for writing lean and mean command-line utilities that do a lot of file IO (like a compiler or its related tools).

There might be other standard libraries that have more overhead than some might wish to have for particular situations. Library code often has to make compromises that fall somewhere that won't please everyone. I would suspect that some of the newer libraries like thread, chrono, regex, and random, provide a bit more features or robust guarantees than are necessary in many applications, and therefore, pull in some undue overhead. But then again, many applications do benefit from those features. This is the very meaning of compromise.

As for language rules that put undue overhead, there are many small issues that impose some overhead. For one, I can think of several places where the standard has to make conservative assumptions that prevent optimizations. One notable example is the inability to restrict pointer aliasing, which forces compilers to assume that any memory could be aliased by any pointer (even though, in practice, pointer aliasing is rare), limiting the opportunities for optimization. There are many similar cases where the compiler has to make the "safe" assumption, limiting optimizations. But most of these are rather small in scope and potential benefits, and they are often justified in terms of being able to guarantee correctness of the behaviour (and repeatability, robustness, portability, etc.). Also, note that in the vast majority of those cases, it doesn't really get any better in other languages, maybe marginally better in C, but that's about it. Some of those issues can also be circumvented with compiler extensions or platform-specific features, or as a last resort, with inline assembly code, that is, if you really need to optimize down to that level.

One example that is no longer valid is the problem of requiring the compiler to produce exception-handling (stack unwinding) code even for functions that will never throw. Now, that can be solved by specifying noexcept on the functions in question.

But other than those microscopic issues, I can't really think of any other major source of undue overhead in C++ (aside from RTTI and exceptions). I mean, there are things in C++ that can create overhead of different kinds, but they are all "opt-in" features (per-use) like virtual functions, virtual inheritance, multiple inheritance, templates, etc... but those mostly obey the "you only pay for what you use" principle. For an example of the rules imposed for a low-overhead subset of C++, check out Embedded C++.

like image 165
Mikael Persson Avatar answered Mar 24 '23 20:03

Mikael Persson