47 ms·
The Performance Impact of C++'s `final` Keyword
- 2genders44672 2y ago[flagged]
- SEXMCNIGGA38614 2y ago[flagged]
- SEXMCNIGGA16826 2y ago[flagged]
- SEXMCNIGGA40201 2y ago[flagged]
- 2genders38019 2y ago[flagged]
- 2genders3032 2y ago[flagged]
- SEXMCNIGGA37663 2y ago[dead]
- SEXMCNIGGA43361 2y ago[dead]
- sexmc16971 2y ago[flagged]
- SEXMCNIGGA33795 2y ago[dead]
- SEXMCNIGGA1200 2y ago[dead]
- SEXMCNIGGA27791 2y ago[dead]
- pcvarmint 2y agoEach of the test cases measured needs to be run at least 3 times in a row, to warm caches (not just CPU but OS too) and to detect and remove noise. In fact, I would run the same test repeatedly, keeping track of the k fastest times (k being ~3-7), and only stopping when the first and the kth fastest times are within a certain tolerance (as low as 1%). This ensures repeatability. One sample of performance data for each test is not enough. This study provides no new insights. Performance analyst
- SEXMCNIGGA4613 2y ago[flagged]
- SEXMCNIGGA9342 2y ago[flagged]
- SEXMCNIGGA38416 2y ago[flagged]
- SEXMCNIGGA28174 2y ago[flagged]
- mgaunard 2y agoWhat final enables is devirtualization in certain cases. The main advantage of devirtualization is that it is necessary for inlining. Inlining has other requirements as well -- LTO pretty much covers it. The article doesn't have sufficient data to tell whether the testcase is built in such a way that any of these optimizations can happen or is beneficial.
- i80and 2y agoIf you already have LTO, can't the compiler determine this information for devirtualization purposes on its own?
- nickwanninger 2y agoAt the level that LLVM's LTO operates, no information about classes or objects is left, so LLVM itself can't really devirtualize C++ methods in most cases
- nwallin 2y agoYou appear to be correct. Clang does not devirtualize in LTO, but GCC does. Personally I consider this very strange. $ cat animal.h cat.cpp main.cpp // animal.h #pragma once class animal { public: virtual ~animal() {} virtual void speak() = 0; }; animal& get_mystery_animal(); // cat.cpp #include "animal.h" #include <cstdio> class cat final : public animal { public: ~cat() override{} void speak() override{ puts("meow"); } }; static cat garfield{}; animal& get_mystery_animal() { return garfield; } // main.cpp #include "animal.h" int main() { animal& a = get_mystery_animal(); a.speak(); } $ make clean && CXX=clang++ make -j && objdump --disassemble=main -C lto_test rm -f *.o lto_test clang++ -c -flto -O3 -g cat.cpp -o cat.o clang++ -c -flto -O3 -g main.cpp -o main.o clang++ -flto -O3 -g cat.o main.o -o lto_test lto_test: file format elf64-x86-64 Disassembly of section .init: Disassembly of section .plt: Disassembly of section .plt.got: Disassembly of section .text: 00000000000011b0 <main>: 11b0: 50 push %rax 11b1: 48 8b 05 58 2e 00 00 mov 0x2e58(%rip),%rax # 4010 <garfield> 11b8: 48 8d 3d 51 2e 00 00 lea 0x2e51(%rip),%rdi # 4010 <garfield> 11bf: ff 50 10 call *0x10(%rax) 11c2: 31 c0 xor %eax,%eax 11c4: 59 pop %rcx 11c5: c3 ret Disassembly of section .fini: $ make clean && CXX=g++ make -j && objdump --disassemble=main -C lto_test|sed -e 's,^, ,' rm -f *.o lto_test g++ -c -flto -O3 -g cat.cpp -o cat.o g++ -c -flto -O3 -g main.cpp -o main.o g++ -flto -O3 -g cat.o main.o -o lto_test lto_test: file format elf64-x86-64 Disassembly of section .init: Disassembly of section .plt: Disassembly of section .plt.got: Disassembly of section .text: 0000000000001090 <main>: 1090: 48 83 ec 08 sub $0x8,%rsp 1094: 48 8d 3d 75 2f 00 00 lea 0x2f75(%rip),%rdi # 4010 <garfield> 109b: e8 50 01 00 00 call 11f0 <cat::speak()> 10a0: 31 c0 xor %eax,%eax 10a2: 48 83 c4 08 add $0x8,%rsp 10a6: c3 ret Disassembly of section .fini:
- andrewla 2y agoI'm surprised that it has any impact on performance at all, and I'd love to see the codegen differences between the applications. Mostly the `final` keyword serves as a compile-time assertion. The compiler (sometimes linker) is perfectly capable of seeing that a class has no derived classes, but what `final` assures is that if you attempt to derive from such a class, you will raise a compile-time error. This is similar to how `inline` works in practice -- rather than providing a useful hint to the compiler (though the compiler is free to treat it that way) it provides an assertion that if you do non-inlinable operations (e.g. non-tail recursion) then the compiler can flag that. All of this is to say that `final` can speed up runtimes -- but it does so by forcing you to organize your code such that the guarantees apply. By using `final` classes, in places where dynamic dispatch can be reduced to static dispatch, you force the developer to not introduce patterns that would prevent static dispatch.
- deleted 2y ago[deleted]
- bgirard 2y ago> The compiler (sometimes linker) is perfectly capable of seeing that a class has no derived classes How? The compiler doesn't see the full program. The linker I'm less sure about. If the class isn't guaranteed to be fully private wouldn't an optimizing linker have to be conservative in case you inject a derived class?
- GuB-42 2y ago"inline" is confusing in C++, as it is not really about inlining. Its purpose is to allow multiple definitions of the same function. It is useful when you have a function defined in a header file, because if included in several source files, it will be present in multiple object files, and without "inline" the linker will complain of multiple definitions. It is also an optimization hint, but AFAIK, modern compiler ignore it.
- wredue 2y agoI believe the wording I’ve seen is that compilers may not respect the inline keyword, not that it is ignored.
- bluGill 2y agoI use final more for communication. Don't look for deeper derived classes as there are none. that it results in slower code is an annoying surprise.
- p0w3n3d 2y agoI would say the most performance impact would give `constexpr` followed by `const`. I wouldn't bet any money on `final` which in C++ is a guard of inheritance, and C++ function invocation address is resolved the `vtable` hence final wouldn't change anything. Maybe the author was mistaken with `final` keyword in Java
- adrianN 2y agoIn my experience the compiler is pretty good at figuring out what is constant so adding const is more documentation for humans, especially in C++, where const is more of a hint than a hard boundary. Devirtualization, as can happen when you add a final, or the optimizations enabled by adding a restrict to a pointer, are on the other hand often essential for performance in hot code.
- bayindirh 2y agoSince "const" makes things read-only, being const correct makes sure that you don't do funny things with the data you shouldn't mutate, which in turn eliminates tons of data bugs out of the gate. So, it's an opt-in security feature first, and a compiler hint second.
- Lockal 2y agoHow does const affects code generation in C/C++? Last time I checked, const was purely informational. Compilers can't eliminate reads for const pointer data, because const_cast exists. Compilers can't eliminate double calls to const methods, because inside function definition such functions can still legally modify mutable variables (and have many side effects). What actually may help is __attribute__((pure)) and __attribute__((const)), but I don't see them often in real code (unfortunately).
- account42 2y agoConst affects code generation when used on variables. If you have a `const int i` then the compiler can assume that i never changes. But you're right that this does not hold true for const pointers or references. > What actually may help is __attribute__((pure)) and __attribute__((const)), but I don't see them often in real code (unfortunately). It's disppointing that these haven't been standardized. I'd prefer different semantics though, e.g. something that allows things like memoization or other forms of caching that are technically side effects but where you still are ok with allowing the compiler to remove / reorder / eliminate calls.
- ein0p 2y agoYou should use final to express design intent. In fact I’d rather it were the default in C++, and there was some sort of an opposite (‘derivable’?) keyword instead, but that ship has sailed long time ago. Any measurable negative perf impact should be filed as a bug and fixed.
- deleted 2y ago[deleted]
- cesarb 2y ago> In fact I’d rather it were the default in C++, and there was some sort of an opposite (‘derivable’?) keyword instead Kotlin (which uses the equivalent of the Java "final" keyword by default) uses the "open" keyword for that purpose.
- josefx 2y agoIntent is nice and all that, but I would like a "nonwithstanding" keyword instead that just lets me bypass that kind of "intent" without having to copy paste the entire implementation just to remove a pointless keyword or make a destructor public when I need it.
- jbverschoor 2y agoIn general, I think things should be strict by default. Way easier to optimize and less error prone.
- leni536 2y agoC++ doesn't have the fragile base problem, as members aren't virtual my default. The only concern with unintended inheritance is with polymorhpic deletion. "final" on class definition disables some tricks thag you can do with private inheritance. Having said that "final" on member functions is great, and I like to see that instead of "override".
- pjmlp 2y agoAll OOP languages have it, the issue is related to changing the behaviour of the base class, and the change introducing unforceen consequences on the inheritance tree. Changing an existing method way of calling (regular, virtual, static), changing visibility, overloading, introducing a name that clashes downstream, introducing a virtual destructor, making a data member non-copyable,...
- jey 2y agoI wonder if LTO was turned on when using Clang? Might lead to a performance improvement.
- pineapple_sauce 2y agoWhat should be evaluated is removing indirection and tightly packing your data. I'm sure you'll gain a better performance improvement. virtual calls and shared_ptr are littered in the codebase. In this way: you can avoid the need for the `final` keyword and do the optimization the keyword enables (de-virtualize calls). >Yes, it is very hacky and I am disgusted by this myself. I would never do this in an actual product Why? What's with the C++ community and their disgust for macros without any underlying reasoning? It reminds me of everyone blindly saying "Don't use goto; it creates spaghetti code". Sure, if macros are overly used: it can be hard to read and maintain. But, for something simple like this, you shouldn't be thinking "I would never do this in an actual product".
- sfink 2y agoMacros that are giving you some value can be ok. In this case, once the performance conclusion is reached, the only reason to continue using a macro is if you really need the `final`ity to vary between builds. Otherwise, just delete it or use the actual keyword. (But I'm worse than the author; if I'm just comparing performance, I'd probably put `final` everywhere applicable and then do separate compiles with `-Dfinal=` and `-Dfinal=final`... I'd be making the assumption that it's something I either always or never want eventually, though.)
- bluGill 2y agoMacros in C are a text replace and so it is hard to see from a debugger how th code got like that.
- pineapple_sauce 2y agoYes, I'm well aware of the definition of a macro in C and C++. Macros are simpler than templates. You can expand them with a compiler flag.
- bluGill 2y agowhen things get complex templete error messages are easier to follow. nobody makes complex macros but if you tried. (template error messeges are legendary for a reason. nested macros are worse)
- gpderetta 2y ago1% is nothing to scoff of. But I suspect that the variability of compilation (specifically quirks of instruction selection, register allocation and function alignment) more than mask any gains. The clang regression might be explainable by final allowing some additional inlining and clang making an hash of it.
- jcalvinowens 2y agoThat's interesting. Maybe final enabled more inlining, and clang is being too aggressive about it for the icache sizes in play here? I'd love to see a comparison of the generated code. I'm disappointed the author's conclusion is "don't use final", not "something is wrong with clang".
- ot 2y agoOr "something is wrong with my benchmark setup", which is also a possibility :) Without a comparison of generated code, it could be anything.
- indigoabstract 2y agoIf it does have a noticeable impact, that would be surprising, a bit like going back to the days when 'inline' was supposed to tell the compiler to inline the designated functions (no longer its main use case nowadays).
- sfink 2y agotldr: sprinkled a keyword around in the hopes that it "does something" to speed things up, tested it, got noisy results but no miraculous speedup. I started skimming this article after a while, because it seemed to be going into the weeds of performance comparison without ever backing up to look at what the change might be doing. Which meant that I couldn't tell if I was going to be looking at the usual random noise of performance testing or something real. For `final`, I'd want to at least see if it changing the generated code by replacing indirect vtable calls with direct or inlined calls. It might be that the compiler is already figuring it out and the keyword isn't doing anything. It might be that the compiler is changing code, but the target address was already well-predicted and it's perturbing code layout enough that it gets slower (or faster). There could be something interesting here, but I can't tell without at least a little assembly output (or perhaps a relevant portion of some intermediate representation, not that I would know which one to look at). If it's not changing anything, then perhaps there could be an interesting investigation into the variance of performance testing in this scenario. If it's changing something, then there could be an interesting investigation into when that makes things faster vs slower. As it is, I can't tell what I should be looking for.
- sgerenser 2y agoThis is what I was waiting for too. Especially with the large regression on Clang/Ubuntu. Maybe he uncovered a Clang/LLVM codegen bug, but you’d need to compare the generated assembly to know.
- akoboldfrying 2y ago>changing the generated code by replacing indirect vtable calls with direct or inlined calls It can't possibly be doing this, if the raytracing code is like any other raytracer I've ever seen -- since it must be looping through a list of concrete objects that implement some shared interface, calling intersectRay() on each one, and the existence of those derived concrete object types means that that shared interface can't be made final, and that's the only thing that would enable devirtualisation -- it makes no difference whether the concrete derived types themselves are final or not.
- drivebycomment 2y ago
- SEXMCNIGGA11323 2y ago[dead]
- jeffbee 2y agoIt's difficult to discuss this stuff because the impact can be negligible or negative for one person, but large and consistently positive for another. You can only usefully discuss it on a given baseline, and for something like final I would hope that baseline would be a project that already enjoys PGO, LTO, and BOLT.
- tombert 2y agoI don't do much C++, but I have definitely found that engineers will just assert that something is "faster" without any evidence to back that up. Quick example, I got in an argument with someone a few years ago that claimed in C# that a `switch` was better than an `if(x==1) elseif(x==2)...` because switch was "faster" and rejected my PR. I mentioned that that doesn't appear to be true, we went back and forth until I did a compile-then-decompile of a minimal test with equality-based-ifs, and showed that the compiler actually converts equality-based-ifs to `switch` behind the scenes. The guy accepted my PR after that. But there's tons of this stuff like this in CS, and I kind of blame professors for a lot of it [1]. A large part of becoming a decent engineer [2] for me was learning to stop trusting what professors taught me in college. Most of what they said was fine, but you can't assume that; what they tell you could be out of date, or simply never correct to begin with, and as far as I can tell you have to always test these things. It doesn't help that a lot of these "it's faster" arguments are often reductive because they only are faster in extremely minimal tests. Sometimes a microbenchmark will show that something is faster, and there's value in that, but I think it's important that that can also be a small percentage of the total program; compilers are obscenely good at optimizing nowadays, it can be difficult to determine when something will be optimized, and your assertion that something is "faster" might not actually be true in a non-trivial program. This is why I don't really like doing any kind of major optimizations before the program actually works. I try to keep the program in a reasonable Big-O and I try and minimize network calls cuz of latency, but I don't bother with any kind of micro-optimizations in the first draft. I don't mess with bitwise, I don't concern myself on which version of a particular data structure is a millisecond faster, I don't focus too much on whether I can get away with a smaller sized float, etc. Once I know that the program is correct, then I benchmark to see if any kind of micro-optimizations will actually matter, and often they really don't. [1] That includes me up to about a year ago. [2] At least I like to pretend I am.
- BurningFrog 2y agoEven if one of these constructs is faster it doesn't matter 99% of the time. Writing well structured readable code is typically far more important than making it twice as fast. And those times can rarely be predicted beforehand, so you should mostly not worry about it until you see real performance problems.
- JackYoustra 2y agoI really wish he'd listed all the flags he used. To add on to the flags already listed by some other commenters, `-mcpu` and related flags are really crucial in these microbenchmarks: over such a small change and such a small set of tight loops, you could just be regression on coincidences in the microarchitecture scheduler vs higher level assumptions.
- j_not_j 2y agoAnd he didn't repeat each test case 5 or 9 times, and take the median (or even an average). There will be operating system noise that can be in the multi-percent range. This is defined as various OS services that run "in the background" taking up cpu time, emptying cache lines (which may be most important), and flushing a few translate lookaside entries. Once you recognize the variability from run to run, claiming "1%" becomes less credible. Depending on the noise level, of course. Linux benchmarks like SPECcpu tend to be run in "single-user mode" meaning almost no background processes are running.
- mgraczyk 2y agoThe main case where I use final and where I would expect benefits (not covered well by the article) is when you are using an external library with pure virtual interfaces that you implement. For example, the AWS C++ SDK uses virtual functions for everything. When you subclass their classes, marking your classes as final allows the compiler to devirtualize your own calls to your own functions (GCC does this reliably). I'm curious to understand better how clang is producing worse code in these cases. The code used for the blog post is a bit too complicated for me to look at, but I would love to see some microbenchmarks. My guess is that there is some kind of icache or code side problem. where inlining more produces worse code.
- cogman10 2y agoCould easily just be a bad optimization pathway. `final` tells the compiler that nothing extends this class. That means the compiler can theoretically do things like inlining class methods and eliminate virtual method calls (perhaps duplicating the method)? However, it's quite possible that one of those optimizations makes the code bigger or misaligns things with the cache in unexpected ways. Sometimes, a method call can bet faster than inlining. Especially with hot loops. All this being said, I'd expect final to offer very little benefit over PGO. Its main value is the constraint it imposes and not the optimization it might enable.
- lpapez 2y ago> For example, the AWS C++ SDK uses virtual functions for everything. When you subclass their classes, marking your classes as final allows the compiler to devirtualize your own calls to your own functions (GCC does this reliably). I want to ask, and I sincerely mean no snark, what is the point? When working with AWS through an SDK your code will spend most of the time waiting on network calls. What is the point of devirtualizing your function calls to save an indirection when you will be spending several orders of magnitude more time just waiting for the RPC to resolve? It just doesn't seem like something even worth thinking about at all.
- mgraczyk 2y agoYeah that's was just the first public C++ library with this pattern that popped into my head. I just make all my classes final out of habit and don't think about it. I remove final if I want to subclass, but that almost never happens.
- jeffbee 2y agoI profiled this project and there are abundant opportunities for devirtualization. The virtual interface `IHittable` is the hot one. However, the WITH_FINAL define is not sufficient, because the hot call is still virtual. At `hit_object |= _objects[node->object_index()]->hit` I am still seeing ` mov (%rdi),%rax; call *0x18(%rax)` so the application of final here was not sufficient to do the job. Whatever differences are being measures are caused by bogons.
- gpderetta 2y agoI haven't looked at the code, but if you have multiple leaves, even marking all of them as final won't help if the call is through a base class.
- jeffbee 2y agoYeah the practical cases for devirtualization are when you have a base class, a derived class that you actually use, and another derived class that you use in tests. For your release binary the tests aren't visible so that can all be devirtualized. In cases where you have Dog and Goose that both derive from Animal and then you have std::vector<Animal>, what is the compiler supposed to do?
- kccqzy 2y agoThe compiler simply knows that the actual dynamic type is Animal because it is not a pointer. You need Animal* to trigger all the fun virtual dispatch stuff.
- lanza 2y agoIf you're measuring a compiler you need to post the flags and version used. Otherwise the entire experiment is in the noise.
- LorenDB 2y agoMan, I wish this blog had an RSS feed.
- SEXMCNIGGA45380 2y ago[flagged]
- SEXMCNIGGA17776 2y ago[flagged]
- SEXMCNIGGA10733 2y ago[flagged]
- SEXMCNIGGA22363 2y ago[flagged]
- SEXMCNIGGA5 2y ago[flagged]
- SEXMCNIGGA7400 2y ago[flagged]
- SEXMCNIGGA8655 2y ago[flagged]
- SEXMCNIGGA7452 2y ago[flagged]
- SEXMCNIGGA49672 2y ago[flagged]
- SEXMCNIGGA21866 2y ago[flagged]
- SEXMCNIGGA38183 2y ago[flagged]
- SEXMCNIGGA9660 2y ago[flagged]
- SEXMCNIGGA20645 2y ago[flagged]
- SEXMCNIGGA44397 2y ago[flagged]
- SEXMCNIGGA12616 2y ago[flagged]
- SEXMCNIGGA31533 2y ago[dead]
- SEXMCNIGGA9611 2y ago[flagged]
- SEXMCNIGGA7303 2y ago[flagged]
- SEXMCNIGGA21472 2y ago[flagged]
- SEXMCNIGGA8338 2y ago[dead]
- SEXMCNIGGA29511 2y ago[dead]
- SEXMCNIGGA12882 2y ago[dead]
- magnat 2y ago> I created a "large test suite" to be more intensive. On my dev machine it needed to run for 8 hours. During such long and compute-intensive tests, how are thermal considerations mitigated? Not saying that this was case here, but I can see how after saturating all cores for 8 hours, the whole PC might get hot to the point CPU starts throttling, so when you reboot to next OS or start another batch, overall performance could be a bit lower.
- Bene592 2y ago8 hours should be enough to let the temperatures settle
- lastgeniusua 2y agohaving recently done similar day-and-night long suites of benchmarks (on a laptop in heat dissipation conditions worse than on any decent desktop), I've found that there is no correlation between the order the benchmarks are run in and their performance (or energy consumption!). i would therefore assume that a non-overclocked processor would not exhibit the patterns you are thinking of here
- leni536 2y agoThis is the gist of the difference in code generation when final is involved: https://godbolt.org/z/7xKj6qTcj https://godbolt.org/z/7xKj6qTcj edit: And a case involving inlining: https://godbolt.org/z/E9qrb3hKM https://godbolt.org/z/E9qrb3hKM
- SEXMCNIGGA35526 2y ago[flagged]
- SEXMCNIGGA34015 2y ago[dead]
- SEXMCNIGGA5633 2y ago[dead]
- SEXMCNIGGA20888 2y ago[dead]
- SEXMCNIGGA22716 2y ago[dead]
- SEXMCNIGGA40830 2y ago[dead]
- SEXMCNIGGA10221 2y ago[dead]
- SEXMCNIGGA814 2y ago[dead]
- SEXMCNIGGA32916 2y ago[dead]
- SEXMCNIGGA39299 2y ago[dead]
- SEXMCNIGGA24284 2y ago[dead]
- SEXMCNIGGA12324 2y ago[flagged]
- SEXMCNIGGA43790 2y ago[dead]
- fransje26 2y agoI'm actually more worried about Clang being close to 100% slower than GCC on Linux. That doesn't seem right. I am prepared to believe that there is some performance difference between the two, varying per case, but I would expect a few percent difference, not twice the run time..
- fransje26 2y agoFor posterity, this is due to a Clang bug. See [1] for more information. [1] https://news.ycombinator.com/item?id=40156196 https://news.ycombinator.com/item?id=40156196
- deleted 2y ago[deleted]
- SEXMCNIGGA4814 2y ago[flagged]
- SEXMCNIGGA12763 2y ago[dead]
- SEXMCNIGGA27640 2y ago[flagged]
- SEXMCNIGGA5473 2y ago[dead]
- SEXMCNIGGA28742 2y ago[dead]
- SEXMCNIGGA15272 2y ago[dead]
- SEXMCNIGGA3042 2y ago[dead]
- SEXMCNIGGA19432 2y ago[dead]
- SEXMCNIGGA15704 2y ago[dead]
- 2genders30059 2y ago[dead]
- mastax 2y agoChanges in the layout of the binary can have large impacts on the program performance [0] so it's possible that the unexpected performance decrease is caused by unpredictable changes in the layout of the binary between compilations. I think there is some tool which helps ensure layout is consistent for benchmarking, but I can't remember what it's called. [0]: https://research.facebook.com/publications/bolt-a-practical-binary-optimizer-for-data-centers-and-beyond/ https://research.facebook.com/publications/bolt-a-practical-...
- 2genders20897 2y ago[dead]
- 2genders15281 2y ago[dead]
- 2genders36213 2y ago[dead]
- 2genders23148 2y ago[dead]
- 2genders39967 2y ago[dead]
- 2genders2516 2y ago[flagged]
- 2genders9277 2y ago[flagged]
- 2genders46844 2y ago[flagged]
- 2genders30392 2y ago[dead]
- 2genders38019 2y ago[dead]
- 2genders38257 2y ago[flagged]
- 2genders19091 2y ago[flagged]
- 2genders10037 2y ago[flagged]
- 2genders47456 2y ago[flagged]
- akoboldfrying 2y agoI would expect "final" to have no effect on this type of code at all. That it does in some cases cause measurable differences I put down to randomly hitting internal compiler thresholds (perhaps one of the inlining heuristics is "Don't inline a function with more than 100 tokens", and the "final" keyword pushes a couple of functions to 101). Why would I expect no performance difference? I haven't looked at the code, but I would expect that for each pixel, it iterates through an array/vector/list etc. of objects that implement some common interface, and calls one or more methods (probably something called intersectRay() or similar) on that interface. By design, that interface cannot be made final, and that's what counts. Whether the concrete derived classes are final or not makes no difference. In order to make this a good test of "final", the pointer type of that container should be constrained to a concrete object type, like Sphere. Of course, this means the scene is limited to spheres. The only case where final can make a difference, by devirtualising a call that couldn't otherwise be devirtualised, is when you hold a pointer to that type, and the object it points at was allocated "uncertainly", e.g., by the caller. (If the object was allocated in the same basic block where the method call later occurs, the compiler already knows its runtime type and will devirtualise the call anyway, even without "final".)
- koyote 2y ago> (perhaps one of the inlining heuristics is "Don't inline a function with more than 100 tokens", and the "final" keyword pushes a couple of functions to 101). That definitely is one of the heuristics in MSVC++. We have some performance critical code and at one point we noticed a slowdown of around ~4% in a couple of our performance tests. I investigated but the only change to that code base involved fixing up an error message (i.e. no logic difference and not even on the direct code path of the test as it would not hit that error). Turns out that: int some_func() { if (bad) throw std::exception("Error"); return some_int; } Inlined just fine, but after adding more text to the exception error message it no longer inlined, causing the slow-down. You could either fix it with __forceinline or by moving the exception to a function call.
- Maxatar 2y agoSince the inlining is performed in MSVC's backend, as opposed to its frontend, and hence operates strictly on MSVC's intermediate representation which lacks information about tokens or the AST, it's unlikely due to tokens. std::exception does not take a string in its constructor, so most likely you used std::runtime_error. std::runtime_error has a pretty complex constructor if you pass into it a long string. If it's a small string then there's no issue because it stores its contents in an internal buffer, but if it's a longer string then it has to use a reference counting scheme to allow for its copy constructor to be noexcept. That is why you can see different behavior if you use a long string versus a short string. You can also see vastly different codegen with plain std::string as well depending on whether you pass it a short string literal or a long string literal.
- 2genders49493 2y ago[flagged]
- 2genders24636 2y ago[flagged]
- SEXMCNIGGA37282 2y ago[flagged]
- 2genders46002 2y ago[flagged]
- 2genders47311 2y ago[flagged]
- 2genders11380 2y ago[flagged]
- 2genders15635 2y ago[flagged]
- 2genders21619 2y ago[flagged]
- 2genders47485 2y ago[flagged]
- 2genders34679 2y ago[dead]
- 2genders47311 2y ago[flagged]
- SEXMCNIGGA4548 2y ago[dead]
- 2genders8333 2y ago[flagged]
- ndesaulniers 2y agoAs an LLVM developer, I really wish the author filed a bug report and waited for some analysis BEFORE publishing an article (that may never get amended) that recommends not using this keyword with clang for performance reasons. I suspect there's just a bug in clang.
- saagarjha 2y agoBug, misunderstanding, weird edge case…
- fransje26 2y agoIs there any logical reason why Clang is 50% slower than GCC on Ubuntu?
- ijan1 2y agoHi, I had a look today because it was bugging me and no one had investigated it, so I created an issue[1] with an explanation. But it basically boils down to uniform_real_distribution having a bunch of uninlined calls to 'logl' when compiled with Clang. Otherwise, Clang beats GCC at least on the configuration I tested. (I am the author of the issue) [1] https://gitlab.com/define-private-public/PSRayTracing/-/issues/85 https://gitlab.com/define-private-public/PSRayTracing/-/issu...
- fransje26 2y agoOh, very nice find! Coincidentally, I happened to be playing around yesterday with a small performance test case using uniform_real_distribution, and for some strange reason Clang was 6x slower than GCC. I put it down to some weird clang bug on my LTS version of Ubuntu. As my installed version was clang-14, I decided it possibly had been noticed and fixed a long time ago. After reading your message I replaced uniform_real_distribution by uniform_int_distribution, and lo and behold, Clang was indeed faster than GCC, as expected. Thank you for coming back to me with your findings.
- SEXMCNIGGA18229 2y ago[flagged]
- SEXMCNIGGA39329 2y ago[dead]
- SEXMCNIGGA9711 2y ago[flagged]
- SEXMCNIGGA48968 2y ago[dead]
- SEXMCNIGGA33183 2y ago[dead]
- SEXMCNIGGA22017 2y ago[dead]
- SEXMCNIGGA25735 2y ago[dead]
- SEXMCNIGGA41363 2y ago[dead]
- SEXMCNIGGA7321 2y ago[dead]
- SEXMCNIGGA47781 2y ago[dead]
- SEXMCNIGGA18638 2y ago[dead]
- SEXMCNIGGA7003 2y ago[dead]
- indianmilf42562 2y ago[dead]
- sexmc8699 2y ago[flagged]
- sexmc26749 2y ago[flagged]
- sexmc39112 2y ago[flagged]
- sexmc11774 2y ago[flagged]
- sexmc49391 2y ago[flagged]
- sexmc11753 2y ago[flagged]
- sexmc12130 2y ago[flagged]
- sexmc10428 2y ago[flagged]
- sexmc29089 2y ago[flagged]
- sexmc41797 2y ago[flagged]
- sexmc10652 2y ago[flagged]
- pklausler 2y agoMildly related programming language trivia: Fortran has virtual functions ("type bound procedures"), and supports a NON_OVERRIDABLE attribute on them that is basically "final". (FINAL exists but means something else.). But it also has a means for localizing the non-overridable property. If a type bound procedure is declared in a module, and is PRIVATE, then overrides in subtypes ("extended derived types") work as usual for subtypes in the same module, but can't be affected by overrides that appear in other modules. This allows a compiler to notice when a type has no subtypes in the same module, and basically infer that it is non-overridable locally, and thus resolve calls at compilation time. Or it would, if compilers implemented this feature correctly. It's not well described in the standard, and only half of the Fortran compilers in the wild actually support it. So like too many things in the Fortran world, it might be useful, but it's not portable.
- MathMonkeyMan 2y agoI think it was Chandler Carruth who said "If you're not measuring, then you don't care about performance." I agree, and by that measure, nobody I've ever worked with cares about performance. The best I'll see is somebody who cooked up a naive microbenchmark to show that style 1 takes fewer wall nanoseconds than style 2 on his laptop. People I've worked with don't use profilers, claiming that they can't trust it. Really they just can't be bothered to run it and interpret the output. The truth is, most of us don't write C++ because of performance; we write C++ because that's the language the code is written in. The performance gained by different C++ techniques seldom matters, and when it does you have to measure. Profiler reports almost always surprise me the first few times -- your mental model of what's going on and what matters is probably wrong.
- scottLobster 2y agoIt matters to some degree. If it's just a simple technique you can file away and repeat as muscle memory, well that means your code is that much better. From a user perspective it could be the difference between software that's pleasant to use and software that's annoying to use. From a philosophical perspective it's the difference between software that functions vs software that works well. Of course it depends on your context as to whether this is valued, but I wouldn't dismiss it. Once person's micro-optimization is another person's polish.
- chris_wot 2y agoSurely "final" is a conceptual thing... in other words, you don't want anyone else to derive from the class for good reasons. It's for conceptual understanding, surely?
- manlobster 2y agoThis seems like a reasonable use of the preprocessor to me. I've seen similar use in high-quality codebases. I wonder why the author is so disgusted by it.
- headline 2y agore: final macro > I would never do this in an actual product what, why?
- alex_smart 2y agoOne thing that wasn't mentioned in the article that I wished it did was the size of the compiled binary with and without final. Only reason I would expect the final version to be slower is that we are emitting more code because of inlining and that is resulting in a larger portion of instruction cache misses. Also, now that I think of it, they should have run the code under perf and compared the stats.
- account42 2y agoYeah, really unsatisfying that there was no attempt to explain why it might be slower since it just gives the compiler more information to decide on optimizations which in theory should only make thins faster.
- kasajian 2y agoI'm surprised by this article. the author genuinely believes that a language construct to benefit performance was added to the language without anyone ever running any metrics to verify. "just trust me bro", is the quote. It's is an insane level of ignorance about how these things are decided by the standards committee.
- kreetx 2y agoAnd yet, results from current compilers show that results are mixed, in summary not making programs faster.
- kookamamie 2y ago> And probably, that reason is performance. That's the first problem I see with the article. C++ isn't a fast language, as it is. There are far too many issues with e.g. aliasing rules, lack of proper vectorization (for the runtime arch), etc. If you wish to have a relatively good performance for your code, try ISPC, which still allows you to get great performance with vectorization up to AVX-512, without turning to intrisics.
- chipdart 2y ago> That's the first problem I see with the article. C++ isn't a fast language, as it is. There are far too many issues with e.g. aliasing rules, lack of proper vectorization (for the runtime arch), etc. That's a bold statement due to the way it heavily contrasts with reality. C++ is ever present in high performance benchmarks as either the highest performing language or second only to C. It's weird seeing someone claim with a straight face that "C++ isn't a fast language, as it is". To make matters worse, you go on confusing what a programming language is, and confusing implementation details with language features. It's like claiming that C++ isn't a language for computational graphics just because no C++ standard dedicates a chapter to it. Just like every engineering domain,you need to have deep knowledge on details to milk the last drop of performance improvements out of a program. Low-latency C++ is a testament of how the smallest details can be critical of performance. But you need to be completely detached from reality to claim that C++ isn't a fast language.
- kookamamie 2y ago> That's a bold statement due to the way it heavily contrasts with reality. I'm ready to back this up. And no, I'm not confusing things - I work in HPC (realtime computer vision) and in reality the only thing we'd use C++ for is "glue", i.e. binding implementations of the actual algorithms implemented in other languages together. Implementations could be e.g. in CUDA, ISPC, neural-inference via TensorRT, etc.
- jpc0 2y ago"We use extreme vectorisation and can't do it in native C++ therefore the language is slow" You a junior or something? For 99% of use cases C++ autovectorisation does plenty and will outperform the same code written in higher level languages. You are literally in the 1% and conflating your use case for that of the general case...
- teeuwen 2y agoI do not see how the final keyword would make a difference in performance at all in this case. The compiler should be able to build an inheritance tree and determine by itself which classes are to be treated as final. Now for libraries, this is a different story. There I can imagine final keyword could have an impact.
- connicpu 2y agoBut dynamically loaded libraries exist, so even if it knows the class is the most derived version out of all classes that exist in all of the statically-linked code through LTO or something, unless it can see the instantiation site it won't be able to devirtualize the function calls without the class being marked as final.
- pjmlp 2y agoOnly if the complete source code is available to the compiler.
- juliangmp 2y ago>Personally, I'm not turning it on. And would in fact, avoid using it. It doesn't seem consistent. I feel like we'd have to repeat these tests quite a few times to get to a decent conclusion. Hell small variations in performance could be caused by all sorts of things outside the actual program.
- kreetx 2y agoAFAIU, these tests were ran 30 times each and apparently some took minutes to run, so it's unlikely that you'll get any different conclusions.
- lionkor 2y agoThe only thing worse than no benchmark is a bad benchmark. I don't think this really shows what `final` does, not to code generation, not to performance, not to the actual semantics of the program. There is no magic bullet - if putting `final` on every single class would always make it faster, it wouldn't be a keyword, it'd be a compiler optimization. `final` does one specific thing: It tells a compiler that it can be sure that the given object is not going to have anything derive from it.
- opticfluorine 2y agoNot disagreeing with your point, but it couldn't be a compiler optimization, could it? The compiler isn't able to infer that the class will not be inherited anywhere else, since another compilation unit unknown to the class could inherit.
- ftrobro 2y agoI assume it could be or is part of link time optimization when compiling an application rather than a library?
- vedantk 2y agoPossibly not in the default c++ language mode, but check out -fwhole-program-vtables. It can be a useful option in cases where all relevant inheritance relationships are known at compile time. https://reviews.llvm.org/D16821 https://reviews.llvm.org/D16821
- bluGill 2y agoWhich is good, but may not apply. I have an application where I can't do that because we support plugins and so a couple classes will get overridden outside of the compilation (this was in hindsight a bad decision, but too late to change now). Meanwhile most classes will never be overriden and so I use final to saw that. We are also a multi-repo project (which despite the hype I think is better for us than mono-repo), another reason why -f-whole-program-vtables would be difficult to use - but we could make it work with effort if it wasn't for the plugins.
- account42 2y agoI'm amused at the AI advert spam in the comments here that can't even be bothered to make the spam even vaguely normal looking comments.
- AtNightWeCode 2y agoMost benchmarks are wrong. I doubt this is correct. Final should have been the default in the lang I think though. There are tons of these suggestions. Like always using sealed in C# or never use private in Java.