Going faster by duplicating code
voidstar.tech
voidstar.tech
GCC is actually good at that, -O3 has no issue recognizing it. In fact there even is a very explicit option (-funswitch-loops) responsible for extracting loop invariants. It is not enabled on -O2 because it has a space-speed tradeoff. If this optimization is truly desirable even on -O2, `#pragma GCC optimize("-funswitch-loops")` can be used to force it.
There are other reasons for not using -O3 which people already mentioned.
As a matter of fact, Gentoo specifically says in their docs that -O3 breaks some packages[2].
[1] https://stackoverflow.com/questions/57889116/different-evalu...
O3 does more aggressive inlining and generally expensive optimizations and optimizations that are not necessarily profitable.
Edit: found example where code works on -O2 but breaks on -O3, related to using abnormal amounts of stack space https://stackoverflow.com/questions/47058978/g-optimization-...
Likely because your codebase had UB in it that didn't show itself until a certain optimization level. The solution is to fix all instances of UB. See my comment above.
Forces me to use a better algorithm when it matters. Which more often than not, does not.
;-)
imo, if you have to go to O3 or enable a pragma to get an "obvious" optimization then this is undesirable and probably something the programmer still needs to be conscious of, especially since we spend so much time doing development builds that are not at the maximum release optimization level.
void withSwitch(vector<int>& Values, bool v) {
if (v) {
multiply1(Values, 2.0);
} else {
multiply1(Values, 3.0);
}
}
Then it actually inlines the code and optimises each one correctly, as it has context about which special cases are available. (Doesn't even need the `inline` keyword for this at `-O2`)You can see the code here: https://godbolt.org/z/5beeYe77a
My goal was to not rely on site-specific optimization and instead have one separately compiled function body that can be improved for common cases. Certainly, once the compiler has a full view of everything it can take advantage of information as it pleases but this is less controllable. If I were really picky about optimizing for each use I would make it a template.
>Doesn't even need the `inline` keyword for this at `-O2`
The inline keyword means little in terms of actually causing inlining to happen. I would expect the majority of inlining compilers do happens automatically on functions that lack the "inline" keyword. Conversely, programmers probably add "inline" as an incantation all over the place not knowing that compilers often ignore it.
Funnily, the inline keyword actually has a use, but that use isn't to tell the compiler to inline a function. The use is to allow a function (or variable) to be defined in multiple translations units without being an ODR violation.
[0] https://learn.microsoft.com/en-us/cpp/build/reference/ob-inl...
to me modern compilers are like voodoo magic
[1] https://github.com/lifthrasiir/j40/blob/252e7987d36d50f617f2...
There's a CPPCast interview with one of the people who worked on the Intel C Compiler where he talks about the things they do to get it producing such fast binaries. At least from what he said, it sounds like the vast majority of the unique optimizations they put into the compiler were about minimizing cache misses, not CPU cycles.
Not surprising, since inline has nothing to do with inlining. :)
This is a fact claim. I'm having trouble parsing the "IMO" that's a preface to your comment.
In this particular case, regardless of how often anyone has personally seen the term used to mean something more like "I think that...", taking that to be the intended meaning is the most friendly interpretation, and therefore the preferable one. It's an informal forum, we type fast, and don't necessarily carefully proof-read comments before hitting reply. Nor should we -- this place is a more pleasant space to occupy for everyone involved if we give each other the benefit of the doubt about choice of words.
Why are you responding as if it's a foregone conclusion that my intent was to be uncharitable/unfriendly?
Someone wrote a comment with unusual phrasing, and I asked for help understanding the message. It contains neither an explicit nor an underhanded attempt to be an asshole to someone. The adversarial reading you're going with is exactly that—something that you're bringing to it, and is (perversely) wholly uncharitable in and of itself...
But it's not explicit in the code; you'd need a comment in the code to explain your reasons. Comments rot. And your tuned "high-level" code (yeah, C++ isn't generally considered a high-level language) is now dependent on some specific optimizer, which might change in the future; code that appears to be portable, and compiles on multiple compilers, only performs properly if you use the right optimizer.
I don't like this advice; but I'm not a C++ programmer. I don't like macros, and I'm not at all keen on the merciless optimizers that are shipped with modern compilers.
Because sometimes this kind of careful performance tuning, even at the cost of readability, is necessary and desirable. And, for the times when that happens, this kind of information needs to be out there, available, and being discussed, so that people can learn the techniques and understand how to use them properly and responsibly.
Yes, that does include knowing that a change to the compiler's optimizer could mean that things that used to make the code faster now make it slower. For that matter, changing the CPU microarchitecture could have the same effect. But that's something we all learn in Optimizing 101. It's so well-known that the very first setting on Godbolt's output pane is a toggle to select which compiler and version it should use to generate said output.
Agreed; I think TFA is a useful article. Highly-tuned code is going to be hard to read (it used to be written in assembler, which is at least explicit).
I was simply commenting on the opacity of having two branches in the source that appear to do the same thing, and rely on something outside the code (or the language specification) to achieve the desired performance.
If we did have a principle saying that you can't get clever with optimization or take advantage of your language's optimizer, that would imply that the past 15-20 years of growth of Web technology was based on an invalid foundation: the v8 JavaScript engine and people's efforts to learn how to use it effectively and push what's possible in a browser.
You'd also have much less impressive video games, and your smartphone would have worse battery life.
1. using `restrict` to tell that compiler that src and dest are sure not to overlap
2. Converting your two increment and test blocks to add+mod to allow for uninterrupted pipelining
Neither might make a difference, but either could.
Writing an optimal composite function is of course not really the goal, nor of much educational/entertainment value. For any additional speed I already have a function which slices up compositing tasks into chunks and puts them on a thread pool.
Well, that's because they did it wrong. Tiny math function like this should be defined in the header file and force-inlined, problem solved. No need for that template monstrosity suggested at the end.
But if our performance knowledge is outdated, how do we know which cases to separate the code into? Using the toy example here, how do we know that multiplying by 2 is a special case we should add a branch for? What about multiplying by 3 or 4 or 5? It seems like you still need to use up-to-date performance knowledge for this technique to work, and in that case just write the left shift into your code so that when this code gets read later it will be more clear why it's there.
He didn't say it explicitly, but the implication that I took was that things that are meant, in essence, to make code more configurable can sometimes (often?) do more harm than good. (SOLID, I'm looking at you.) Using them successfully often requires oracular knowledge about how the code might evolve in the future.
I've found that doing my job got a lot easier after I started following this idea. The code might be more repetitive and boilerplate-y, but there's actually less of it, so it's still easier to read, understand, and maintain.
The DRY should be obvious when to refactor, not because of some compulsive behavior.
And then once you need more than 2 branches, please break the DRY that tend to stay forever.
DRY, by comparison, is an almost offensively crude heuristic.
It takes practice. For this you really just have to read the assembly, and profile the code. Then try separating it one way and see if it helps, and if not then another. In my experience, it takes many tries to update your perf knowledge, and optimizations sometimes don’t work even when your knowledge is current. All this is why the article noted people should be “Giving the compiler an opportunity to do something is useful, but measure carefully before forcing it.”
Case separation is less of a commitment than deciding on the specific optimization yourself. You know empirically which cases are used in your application and can separate those to see if there's a benefit without any manual optimization knowledge, like I did in the image scaling example, without having much of a theory in mind as to why it might be faster.
I think what makes it so great is that your examples hit a sweet spot of simplicity while still being motivating and you show how to get a "good enough" solution very quickly.
On its own, the C++ "inline" keyword does not cause inlining to happen, although compilers may treat it like a hint depending on optimization level. GCC does not inline an "inline" function on O0.
"Inline" in the C++ standard means "multiple definitions permitted" so the same entity can exist in multiple translation units without upsetting the linker. This is why C++17 added "inline" variables, which can be initialized in a header that's included in multiple places, even though the inlining concept has no applicability to a variable. The keyword was adopted for this purpose because of the primary association with affecting linkage behavior.
It’s not quite the same.
Plus, another benefit of duplication is that you can more freely hand-tune your implementation once you’ve decided its private. Memory alignment, pointer aliasing hints, clever loop structures, SSE stuff, etc can all be used more freely when you know nothing else needs to use this version.
The article is a good teaser around how unintuitive optimization can be, but it only scratches the surface.
Calling memcpy with an arbitrary value always has to call out to libc but a specific value can be inlined into a single instruction or two for smaller values, so you can have an if-else chain or switch with a few common ones for your program and it's a noticeable speedup.
Sometimes it really is faster just to do what a rookie would, dump all the work in a single function.
"A compound assignment of the form E1 op= E2 differs from the simple assignment expression E1 = E1 op (E2) only in that the lvalue E1 is evaluated only once." (C99, 6.5.16.2p3)
It only matters when evaluating E1 has side effects. For example, `a[i++] += 1;` which is equivalent to `a[i] = a[i] + 1; i++;` rather than `a[i++] = a[i++] + 1;`.
This is probably not what you want, as "i" is increased twice.
How could this post get to the front page of Hacker News?
1. A pushback against code standards a lot of organizations have
2. The ego a lot of individuals have that is incompatible with #1
again, just guessing