upvote
A large part of optimization is understanding the hardware architecture in detail and then making your software architecture mirror that hardware architecture as closely as possible. A compiler can't do this for you. Much of "performance engineering" is applying your understanding of how the hardware components work and are connected to the software design.

Most software introduces a large number of unnecessary stalls where one part of the hardware is waiting on or bottlenecked by a different piece of hardware. Optimization is often about removing or minimizing these stalls to the extent possible. But to do this you need to understand why specific code choices cause the hardware to stall in the first place. The most basic form of this is CPU cache locality but there are many levels.

Compilers are great at localized micro-optimizations, except for SIMD. Most idiomatic code can be detected and transformed by the compiler into something that takes advantage of the hardware design. You don't need to do tricky bit-twiddling stuff, the compiler is better at it than you are in most cases (this wasn't always true). I always recommend people interested in optimization experiment running snippets of code through the compiler using godbolt with optimization turned on. You can learn a lot about how the compiler sees and understands your code by studying this. It is also a good way to became familiar with assembly.

Leveraging SIMD and vector ISAs is a mess. Different hardware architectures rely on different idioms and compilers cannot auto-vectorize most code that can be vectorized. You need to learn the idioms of each vector ISA and how to write them using intrinsics. A great way to learn this is reading other peoples' SIMD code. Modern SIMD code is wide in that you need to memorize a lot of things but conceptually pretty shallow. It mostly comes down to learning the idiomatic tricks and gadgets for an ISA -- code is composed from these conceptual primitives.

reply
These are concepts I've come across in books I've read, and your explanation is easy to understand. But I can tell my understanding is lacking in some parts because I haven't actually tried it in practice. As you said, I think using Godbolt to disassemble things myself is the right approach.

Thanks for the advice.

reply
You could read compiler books, but I would actually recommend reading about CPUs and computer architecture directly. If you understand how the hardware works, then the optimizations are all very natural and fit into the picture perfectly, instead of being some arcane compiler magic that you have to take as a disconnected fact.

Personally I actually haven't read too many books on optimizations, I just absorbed information over years one thing at a time, but something like Computer Organization and Design is a pretty good intro to the low-level picture. If you want to drown in extremely dense technical topics that will give you a lot of jumping off points to search, read Agner Fog's microarchicture optimization guide (https://www.agner.org/optimize/). It won't tell you what LLVM is doing, but it'll tell you why it's doing it. Fair warning, it's dense and pretty dry.

Then it depends how interested you are in doing low-level nonsense. If you spend a lot of time writing performance oriented systems code, you'll come to use profiling tools that show you the assembly. If you stare at it long enough, you sometimes start to question why the compiler wrote it this way. And you're naturally led as you try to optimize your code to wonder how LLVM is coming up with this ASM that it spits out and why it sometimes gets it wrong.

There's nothing magical or that requires innate talent. You can learn all of this very naturally if you work close to the metal and take the time to question how the abstraction layer below you actually works. If you keep doing this, you eventually find out it's not that deep, it's just a lot of stuff accumulated over time, but none of it particularly difficult or inaccessible.

reply
I also agree that computer architecture is more important - it grounds your understanding of how to write efficient code regardless of platform since most machines today share very similar ideas (OOO execution, caches, NUMA etc).

How ever, I will disagree slightly that all the optimizations compilers do are about optimizing for a given architecture; some transformations are just weird algorithmic black magic about optimizing the underlying code itself. Knowing how to make sure the compiler sees through a given construct to give you the low level expression you want is too much art and randomness; we need better ways to express optimization expectations so that if the compiler fails to match expectations it becomes a loud compiler error.

reply
Right. You won't learn from a computer architecture book or a uarch guide about SSA form, or LICM, or other famous compiler principle like the central role of inlining decisions ("the mother of all optimizations"). I don't have a good resource to recommend here, this is where my lack of formal training bites. "Go read a hundred blog articles by compiler experts" doesn't feel like very useful advice.

>Knowing how to make sure the compiler sees through a given construct to give you the low level expression you want is too much art and randomness; we need better ways to express optimization expectations so that if the compiler fails to match expectations it becomes a loud compiler error.

There's a parallel with hardware there. Verilog is a kind of hardware language designed for an abstract simulator, in the same way than C is designed for a standard abstract machine for the sake of portability. You end up with an idea of the assembly/RTL you want the compiler/synthetizer to generate in your head, and then it's a game of writing the right pattern that will be recognized and generate the output you want.

I think this is partially unavoidable, because we're inherently asking the compiler to generate a non-portable target-specific output in what is supposed to be a portable high-level language. If you start injecting compiler hints or requirements in your "portable" code, it all becomes a bit of a mess. Part of the problem is also that the high-level languages we're using were designed at a time were many questions were still unsettled. Things like signed integers being two's complements is a recent change in C and C++. But I think some of it is intrinsic impedance mismatch between high-level code and machine code.

I'm not sure I would like a proliferation of annotations that direct exactly how the compiler should optimize (like "must use cmov/csel here"), because if internal optimizer choices become public API, people will rely on internals in their large legacy codebases. I expect this would be a force that ossifies the compiler and prevent optimizations from improving. The "register" and "inline" keywords in C used to mean something to the compiler. But they were misused, having them be a requirement would have held back performance more than anything.

Then again I accepted the same justification against Postgres planner hints, and now that the idea has been recast as a plan stability feature I'm actually very happy with that idea. I'm uncomfortable with letting old calcified codebases hold back compiler internal, but at the same time once you find a way to have the compiler generate what you want, there's a real need to not have it break silently when you upgrade.

reply
Less about calcifying the specific optimization and more like “this loop is expected to be vectorized”. That way if someone changes something subtle that prevents it from being vectorized it’s a compiler error. Striking that balance and how to express those constraints in a flexible way is the hard bit.

As you say register and inline were wrong, but we have force inline and force inline so clearly the pendulum swung back a little bit because the compiler completely ignoring is also not good. We have ways to force the compiler to do an unconditional move because source level heuristics are completely incorrect for making such a decision. The die is already cast, we just keep living with a shitty status quo instead of something a bit more robust.

reply
Thee optimization in question here was not obvious at all. It's a bs clang codegen quirk
reply
I'm not sure it's a "technique" but the general insight worth taking away from this is that compiler authors often write optimizers to recognize specific patterns so writing your code in a more idiomatic form increases the odds an optimizer will be able to optimize it.

In this specific instance, at the hardware level it helps to understand how the branch predictor works and why quicksort in particular is essentially the worst case for the branch predictor, and then you'll understand why the cmov/csel optimization is such a big win.

reply
About that kind of 'technique', I guess I should make it a habit to dig into the compiler, which is still a black box to me. I should study a few techniques myself. Have a good day
reply
While the article is interesting, it's the case of a missed optimization by the compiler and (possibly) something that a future version of the compiler will catch.

This sort of optimization is one that'd I'd not spend too much time trying to fix or catch, unless you are doing it for fun or you have a specific piece of code in a hot path that needs to go fast.

Where I'd spend time if I were trying to write very fast code which a compiler is unlikely to get right is SIMD optimizations. Specifically with floating point values.

One thing compilers can't and won't do is reorganize floating point optimizations (well, unless you explicitly give them permission to do that). That means the way you write your floating point code can really nerf performance and exclude you from much faster assembly.

The sure fire way to actually make such code faster is learning and using SIMD expressions. The compilers can sometimes get this right, but it's quite fragile.

reply
Thank you for giving me the keywords.
reply
First important step is the need to make your application faster.

Second step is to actually make it faster.

And I would say 99% of the time you can make applications faster without going down so deep on this low-level (which is still not easy) but sometimes you can only get faster via low-level optimizations.

reply
thanks!
reply
Casey Muratori's https://computerenhance.com is a good resource
reply
thanks!
reply
I don't have a book, but my best recommendation would be: Learn how to measure. Whatever language you are using should have a profiler. Learn how to use it and look at how your code holds up. Look for surprises; those are optimization opportunities and learning opportunities. Make a change that you think has a reasonable shot at optimizing performance, look at how the profile and benchmarks change. If you're using a compiled language, look at what the generated code looks like. Is it what you'd expect, or did the compiler do something that blindsided you? Can you get rid of that overhead? Can you use a faster algorithm? Can you make things faster without making it an unmaintainable mess?

In general, you must expect to try ten things and then one of them will help you; fewer if your ideas are bad. :-) Occasionally, you learn new things (either about your machine or your language or your code base) and then you can try that elsewhere in the code (but don't go overboard, every technique has its limits).

DO NOT FALL PREY TO SUPERSTITION. Always measure in one way or the other. Don't do stuff blindly just because someone on the Internet told you (there's a _lot_ of bad performance advice out there).

reply
[dead]
reply
In order of priority, I’d say:

1. If you write CRUD apps, make sure that the database does the heavy lifting.

2. Take note of algorithm complexities, use hashtables as appropriate, and write good hash functions when you start using hashtables/dictionaries.

3. Avoid pointer-heavy datastructures. In high level languages like Java, an object reference is a pointer dereference that can stall the CPU waiting for memory. This is sometimes optimized, but you can’t depend on it. The true zealots call this “data oriented design”.

4. If you write C/C++,rust, or the like, you might want to learn to read assembly. Godbolt.com is a fun way to learn. Note that not all instructions are equally fast: Long division and trigonomic functions are slower than integer adds, even when they are both a single instruction.

5. The next level is probably going for vectorized instructions: SIMD (ARM Neon, AVX). The most original applications can be found at lemire.me: a professor exploring optimizing things like JSON parsing using the latest processor features.

reply
I personally learned a lot from cpp conference videos. I would highly recommmend it.
reply
I should study that. Thank you
reply
Got to my first job out of college and they gave me core dumps and had me debug the kernel for a year. Not my kind of fun, but definitely got me skilled in the art of low level.
reply
I did Nand2Tetris and then I understod why I need to vectorize. You get a view from nand-gate to software and get to see all the interfaces. Its a wonderful course.
reply
Expose yourself to lower level technologies (compilers and optimization techniques, hardware history and design) and let curiosity guide you. Learn how to profile and analyze program performance.
reply