When I was still programming in C++ that tool was the method to discuss compiler internals and language semantics with colleagues. Just setup a minimal example and share it with your colleagues. Impressive and sad at the same time. Impressive for obvious reasons. Sad, because the language is so confabulated that there is no easy concise way to talk about its semantics.
I also use it for this purpose, however I have come to hate the way other people use it for this. I have a colleague who has really no idea what he's talking about with respect to machine performance, and who did not have the requisite knowledge of how to peep at the assembly code of a given function with the standard tools like objdump, who now loves to send everyone godbolt links in slack, along with his suppositions about which function will be faster, based entirely on vibes (mostly, instruction count). This drives me up the wall. I wish there was some minimum height needed to ride godbolt.
I go to some pain in my talks to say "instruction count is not a good proxy for performance", but unfortunately folks do still use it. It's handy to say "hey, there's no loop in this output" or "this loop does 3 multiplies; the alternative does two and an add" or similar. It's a mixed blessing to have brought the compiler output to the masses, I can only hope it starts a useful learning process!
Now any load from the L3 cache memory or from the main memory takes much more time than any other instruction (not counting exceptions generated by instructions, which include many memory accesses that slow them down, or deprecated instructions that are kept for backwards compatibility and that are executed by long microcode sequences).
Assuming that "caring about performance" = "you are in a tight loop": Here's a tool that simulates/visualizes instruction flow and data dependencies over multiple loop iterations.
Paste in assembly code, check "Trace Table" and run, then "Open Trace". Not sure if it will help with your annoying colleague, but it gives a much more concrete idea about how a processor will execute any given code.
Or, if you want to channel their energy into something slightly more direct, there's also https://quick-bench.com/ which allows easy micro-benchmarking. Still not guaranteed to be relevant to any real-world scenario, but more data-driven than "vibes".
Using Compiler Explorer to see how different compilers interpret the same code, or understanding the generated ABI, or if various pragmas are or are not working, etc., is a very good use of it--I suspect most compiler developers at this point more or less have a tab of Compiler Explorer permanently open at this point.
> I have a colleague who has really no idea what he's talking about with respect to machine performance, and who did not have the requisite knowledge of how to peep at the assembly code of a given function with the standard tools like objdump, who now loves to send everyone godbolt links in slack, along with his suppositions about which function will be faster, based entirely on vibes (mostly, instruction count).
It's hard to measure performance in realistic situations; it's easy to measure code size. I recently found myself wasting time doing micro-optimizations, encouraged by the feedback loop of measuring and reducing code size (in my case, not with Compiler Explorer, but with "cargo bloat", since I was working on a Rust project.)
I know you know this but instructions are basically free in the face of memory access and random memory access is the worst. Linear scans over contiguous memory (per thread) generally optimizes performance.
Instruction counts are only useful if everything is guaranteed to be in registers.
It can be tough even without the impact of the memory hierarchy. I've seen code where adding an extra instruction to the calculation made it faster. The extra instruction implicitly eliminated denormals, thus resulting in faster execution with some workloads on systems where operations on denormal values were slower than operations on other values.
It was a completely unnecessary instruction from a correctness perspective, because it had no effect on the answer. However, it was important for performance; removing the instruction made the calculation slower.
How $lang maps to assembly is half the picture: how assembly maps to CPU is the other half. We shouldn't blame ignorance of the latter on a tool for exploring the former.
I do totally get how some people learn just enough to be annoying. Generally I still think that's not a good reason to gatekeep them.
objdump sucks, source annotations via coloring makes it at least 5x faster to read assembly, I don’t care how smart you are or how fluent you are in assembly. If your colleague is wrong for other reasons, that’s orthogonal.
yes but there's real value in exploration. I haven't touched c or assembly for a long time. here's a cold read.
push rbp
this is going to take the contents of rbp and push it onto the top of the stack - this will probably also change the stack pointer
mov rbp, rsp
move goes left <-, like a = 5, not 5 = a.
so, copy the updated stack pointer into rbp
mov DWORD PTR [rbp-4], edi
now, I'm not 100% sure, but I believe this guy puts edi just under the value we pushed to the top of the stack
mov eax, DWORD PTR [rbp-4]
Take that value, and put it into eax, I'm not 100% sure why it's not just mov eax edi.
imul eax, eax
integer multiply, this is the part that does the double.
pop rbp
restore rbp (which we messed with)
ret
and we're done.
there are at least three holes in my understanding - but those three are not _that_ hard to track down.
1, does the stack pointer actually auto increment? (I think it does)
2, imul overflow and setting sign flags and such. - that shouldn't be hard to run down.
3, what is the c calling convention? it looks like the argument is top of stack, but also in edi - is that shuffling really needed? I think there's a bucket of implicit behavior there that's kinda scary.
I would _hope_ unless linking to a library, whatever called this, just did the imul eax eax.
My understanding may be deeply flawed, but explaining my assumptions and my understanding does two things.
1, it helps me learn.
2, it helps others re-evaluate their assumptions and possibly see from a different viewpoint.
I'm not saying spam compiler lists. But a clear and well thought out question can certainly advance discussion. It forces people to formalize their assumptions.
The default godbolt page runs the compiler with no flags, which means without any optimizations. This explains why the code unnecessarily shuffles stuff to the stack and back. Unoptimized clang/llvm output spills everything to the stack, and register allocation is an optimization.
With -O3, the code is:
imul edi, edi
mov eax, edi
ret
Yep, the calling convention for x86-64 on Linux and macOS passes the first six integer arguments in rdi, rsi, rdx, rcx, r8, and r9, and then spills to stack.
Having originally learned the basics of assembly on the chronically register-deprived x86, it took me a while to get used to the fact that standard CCs now pass things in registers (and rsi and rdi in particular, retaining their ancient names while being completely general-purpose these days).
And user netch on stack overflow wrote this which explains more:
notice also there is a 128-byte space ("red zone") before %rsp that keeps its contents between function calls but preserved by OS during interrupts. So, very temporary values (between function calls) can be used with negative offsets to %rsp. Not all compilers utilize this.
About this code (note opposite order of register movement - there are two main styles of displaying x86 assembly code):
pushq %rbp
movq %rsp, %rbp
subq $16, %rsp
the comment was:
Compiler allocates some space for local values on function enter. That's why it subtracts value from %rsp on enter. This doesn't depend on whether %rbp is used as frame pointer. After that, this place is used with positive offsets upon %rsp. Also, if this function calls another one, %rsp shall be aligned on 16-byte boundary for each call, so, in that case compiler shall subtract 8 from %rsp on each enter.
1. You can figure out things about the assembly even without understanding assembly (e.g. lines of source translating into 0 lines of output vs many lines of output).
2. You have labels.
3. You can figure out some of the assembly on your own. Say: `mov %r1 %r2` - it probably moves what's in entity %r1 into a similar entity %r2, or vice-versa. etc.
4. You can see what the executable outputs
5. and most importantly: You can read compiler warnings and errors...
Most compiled languages have offered switches to generate Assembly.
Even on JVM and .NET there are ways to dump it, while on the various JVM implementations it requires a plugin if not using a debug build, on the .NET side, you can use show Assembly on Visual Studio, or make use of WinDBG with SOS plugin.
I think it has a few other languages supported now too. And/or there's equivalents for other languages.
I think most of the confabulation of C++ is necessary to get the semantics needed for it to work right. Especially with all of the optimizations that compilers are expected to make. I found the reasoning behind switching from just rvalues/lvalues to the 5+ types they have now to be fascinating, for example.