[HN Gopher] Both GCC and Clang generate strange/inefficient code
___________________________________________________________________
Both GCC and Clang generate strange/inefficient code
Author : rsf
Score : 71 points
Date : 2026-02-06 22:11 UTC (5 days ago)
(HTM) web link (codingmarginalia.blogspot.com)
(TXT) w3m dump (codingmarginalia.blogspot.com)
| the_fall wrote:
| It's common for compilers to generate mildly unusual code because
| they translate high-level code into an abstract intermediate
| notation, run a variety optimization steps on that notation, and
| then emit machine-specific code to perform whatever the
| optimizations yielded. There's no constraint along the lines of
| "but select the most logical opcode for this task".
|
| The claim that the code is inefficient is really not
| substantiated well in this blog post. Sometimes, long-winded
| assembly actually runs faster because of pipelining, register
| aliasing, and other quirks. Other times, a "weird" way of zeroing
| a register may actually take up less space in memory, etc.
| rsf wrote:
| > The claim that the code is inefficient is really not
| substantiated well in this blog post.
|
| I didn't run benchmarks, but in the case of clang writing zeros
| to memory (which are never used thereafter), there's no way
| that particular code is optimal.
|
| For the gcc output, it seems unlikely that the three versions
| are all optimal, given the inconsistent strategies used. In
| particular, the code that sets the output value to 0 or 1 in
| the size = 3 version is highly unlikely to be optimal in my
| opinion. I'd be amazed if it is!
|
| Your point that unintuitive code is sometimes actually optimal
| is well taken though :)
| its_magic wrote:
| Stefan Kanthak has previously noted that GCC's code generator
| is quite horrible, in these extensive investigations:
|
| https://skanthak.hier-im-netz.de/gcc.html
| DannyBee wrote:
| It's also rarely worth being optimal in scalar code anymore,
| particularly at compilation speed cost. The exception here is
| memory accesses and branches that will miss. So the writing of
| useless zeros is egregious but other stuff just isn't usually
| worth caring about these days. It's "good enough" in an age
| where even in embedded land I can run a 48mhz cortex m0 for 10
| years on a battery and not worry about a few extra ANDS. I'm
| much more likely to hit size than speed limitations.
|
| Not to mention for anything not super battery limited you can
| get a m55 running at 800mhz with a separate 1ghz npu, hardware
| video encoders, etc.
|
| This is before you move into the rockchip/etc space.
|
| We really just aren't scalar compute limited in tons of places
| these days. There are certainly places but 10-15 years ago
| missing little scalar optimizations could make very noticeable
| differences in the performance of lots of apps and now it just
| doesn't anymore
| magicalhippo wrote:
| > The claim that the code is inefficient is really not
| substantiated well in this blog post. Sometimes, long-winded
| assembly actually runs faster because of pipelining, register
| aliasing, and other quirks.
|
| I had a case back in the 2010s where I was trying to optimize a
| hot loop. The loop involved an integer division by a factor
| which was common for all elements, similar to a vector
| normalization pass. For reasons I don't recall, I couldn't get
| rid of the division entirely.
|
| I saw the compiler emitted an "idiv [mem]" instruction, and I
| thought surely that was suboptimal. So I reproduced the
| assembly but changed the code slightly so I could have "idiv
| reg" instead. All it involved was loading the variable into an
| unused register before the loop and use that inside the loop.
|
| So I benchmarked it and much to my surprise it was a fair bit
| slower.
|
| I thought I might have been due to loop target alignment, so I
| spent some time inserting no-ops to align things in various
| supposedly optimal ways, but it never got as fast. I changed my
| assembly to mirror what the compiler had spit out and voila,
| back to the fastest speed again...
|
| Tried to ask around, and someone suggested it had to do with
| some internal register load/store contention or something along
| those lines.
|
| At that point I knew I was done optimizing code by writing
| assembly. Not my cup of tea.
| sidewndr46 wrote:
| Not to suggest you weren't competent, but did you consider
| and try and control for the fact that your measurement could
| be the problem?
| magicalhippo wrote:
| Not going to dismiss it, but I did try to not do stupid
| stuff. I used QueryPerformanceCounter outside the loop,
| pinned the benchmark thread to a single core, and the array
| of elements it processed was fairly large. So I don't think
| overhead and throttling was an issue. The measurements were
| very consistent and repeatable.
| sidewndr46 wrote:
| Fair enough, I've only really ever found assembly level
| optimization on embedded microcontrollers to make any
| degree of sense. Performance optimization usually means
| something along the lines of "convince co-workers not to
| implement their own bubble sort" in my lines of work
| magicalhippo wrote:
| Yeah, I've also come across a lot of assembly code which
| was faster 10 years ago, but where the compiler now beats
| it. So for a while now my take has been to mostly avoid
| asm, but if needed always have a compiled version, and
| always do runtime performance detection to select optimal
| version.
| meisel wrote:
| If you're doing enough divisions with the same divisor, it'd
| be faster to do what compilers do for division by a known
| constant, where they multiply by an integer reciprocal and
| shift
| magicalhippo wrote:
| Yea that can work well. I have extensive fixed-point math
| experience from my days of coding 3D graphics on my 286 and
| up, but for some reason I can't recall I didn't consider
| that viable in this case.
| anon291 wrote:
| idiv [mem], and I assume mem is staying the same means it's
| in the lowest level cache... As fast as registers at that
| point. You're just adding to register contention attempting
| to move things into registers.
| btdmaster wrote:
| In my experience C++ abstractions give the optimizer a harder job
| and thus it generates worse code. In this case, different code is
| emitted by clang if you write a C version[0] versus C++
| original[1].
|
| Usually abstraction like this means that the compiler has to emit
| generic code which is then harder to flow through constraints and
| emit the same final assembly since it's less similar to the
| "canonical" version of the code that wouldn't use a magic `==`
| (in this case) or std::vector methods or something else like
| that.
|
| [0] https://godbolt.org/z/vso7xbh61
|
| [1] https://godbolt.org/z/MjcEKd9Tr
| pjmlp wrote:
| Except that the C++ version doesn't need to be like that.
|
| Abstractions are welcome when it doesn't matter, when it
| matters there are other ways to write the code and it keeps
| being C++ compliant.
| maccard wrote:
| To back up the other commenter - it's not the same.
| https://godbolt.org/z/r6e443x1c shows that if you write
| imperfect C++ clang is perfectly capable of optimizing it.
| cogman10 wrote:
| What's strange is I'm finding that gcc really struggles to
| correctly optimize this.
|
| This was my function for (auto v : array) {
| if (v != 0) return false; }
| return true;
|
| clang emits basically the same thing yours does. But gcc ends
| up just really struggling to vectorize for large numbers of
| array.
|
| Here's gcc for 42 elements:
|
| https://godbolt.org/z/sjz7xd8Gs
|
| and here's clang for 42 elements:
|
| https://godbolt.org/z/frvbhrnEK
|
| Very bizarre. Clang pretty readily sees that it can use SIMD
| instructions and really optimizes this while GCC really
| struggles to want to use it. I've even seen strange output
| where GCC will emit SIMD instructions for the first loop and
| then falls back on regular x86 compares for the rest.
|
| Edit: Actually, it looks like for large enough array sizes,
| it flips. At 256 elements, gcc ends up emitting simd
| instructions while clang does pure x86. So strange.
| secondcoming wrote:
| I;ve had to coerce gcc to emitting SIMD code by using int
| instead of bool. Also, the early return may be putting it
| off.
| abbeyj wrote:
| Doing both of those things does seem to help:
| https://godbolt.org/z/1vv7cK4bE
|
| GCC trunk seems to like using `bool` so we may eventually
| be able to retire the hack of using `int`.
| btdmaster wrote:
| I see yeah that makes sense. I wanted to highlight that
| "magic" will, on average, give the optimizer a harder time.
| Explicit offset loops like that are generally avoided in many
| C++ styles in favor of iterators.
| delta_p_delta_x wrote:
| Even at a higher level of abstraction, the compiler seems
| to pull through: https://godbolt.org/z/1nvE34YTe
| btdmaster wrote:
| It emits a cmp/jmp still when arithmetic would be fine
| though which is the difference highlighted in the article
| and examples in this thread. It's nice that it simplifies
| down to assembly, but the assembly is somewhat
| questionable (especially that xor eax eax branch target
| on the other side).
| rwmj wrote:
| The OP should try with -march=native so the compiler can use
| vector instructions.
|
| Slightly off-topic but I like this way to test if memory is all
| zeroes: https://rusty.ozlabs.org/2015/10/20/ccanmems-memeqzero-
| itera... (see "epiphany #2" at the bottom of the page) I really
| wish there was a standard libc function for it.
| rsf wrote:
| > The OP should try with -march=native so the compiler can use
| vector instructions.
|
| I just tried "-O3 -march=znver5" as well as "-O3 -march=native"
| and it didn't seem to make any difference.
| gspr wrote:
| With `u32` as the element type, rustc 1.93 (with `-O`) does the
| correct thing for size=1, checks both elements separately (i.e.
| worse than in the article) for size=2, checks all three elements
| separately (i.e. not being crazy like in the article) for size=3,
| and starts doing SIMD at size=4.
|
| https://godbolt.org/z/5PETM5bbn
| usamoi wrote:
| This code is not equivalent to the C++ version. You can
| directly use `*x == [0_u32; SIZE]`. The code generated by the
| two is different. (But the iterator version not producing
| optimal code is also an issue.)
| gspr wrote:
| Very good point! Thanks!
|
| With the correction, it interestingly enough produces the
| good behavior also at size=2. It also delays SIMD until
| size=5. But then it bizarrely stops doing SIMD again after
| size=64.
|
| https://godbolt.org/z/P979nY4nf
|
| The iterator version stays SIMD-y also after size=64, but
| stops at some point. What?! I don't know enough to understand
| what's going on. Anyone?
| ceteia wrote:
| Might it have something to do with compiler heuristics?
| Compilers cannot analyze everything, since the more
| compilation time is spent on analysis, the slower
| compilation will be. So compilers might use heuristics to
| guess when to do analysis and how much, and when not to. As
| part of trade-offs between better optimization of compiled
| programs and decreasing compilation times.
| newpavlov wrote:
| Compilers also like to unnecessarily copy data to stack:
| https://github.com/llvm/llvm-project/issues/53348 Which can be
| particularly annoying in cryptographic code where you want to
| minimize number of copies of sensitive data.
| rerdavies wrote:
| Sure, the code is strange, but it is not necessarily inefficient.
| The only way to determine whether it is inefficient is to profile
| the generated code. And perhaps, compare the performance of
| compiler-generated code with tweaked or hand-generated assembler
| code that you think might be better.
|
| GCC and Clang both have highly detailed models of processor
| execution pipelines that are used to perform optimization and
| instruction scheduling. This allows them to perform optimizations
| that mere mortals can only do with assistance from tools like
| Intel VTune, which provides insight into how execution pipelines
| are running, and where and when they are stalling.
|
| It's entirely possible that the multiple memory fetches may fuse
| in the execution pipeline, and that seemingly unnecessary
| instructions may dual issue, and execute in parallel. Or that
| minor variations in generated instructions may allow four
| instructions to be decoded in parallel instead of three at a
| critical moment in the code. These are the kind of insights that
| GCC and Clang have into how the code will actually execute that
| you do not.
|
| Both GCC and Clang have highly detailed models of the processor's
| execution pipeline for literally hundreds of processors. These
| models allow the compiler to determine which instructions execute
| in parallel, and to predict and avoid stalls at various places in
| the processor's execution pipeline.
|
| Counter-intuitively, many code optimization problems rely not
| upon the the instructions being executed, and not even on how
| many instructions are being executed, since pretty much every
| instruction in passably well-optimized code will execute in
| parallel with at least one other instruction. The actual problem
| becomes one of predicting whether any operation in a 7- to
| 20-stage execution pipeline will stall or not, and whether there
| are ways to schedule instructions so that the stalls either don't
| occur at all, or don't matter at all.
|
| Optimizations that are dependent on memory access are
| particularly perilous. Modern processors have elaborate and
| sometimes unpredictable methods for optimizing memory accesses:
| not just cache optimizations, but also fusing of reads and
| writes, optimizations for streaming reads and writes, single-
| cycle reads and writes for memory operations that look like they
| are stack-related, strategies for scheduling reads and writes to
| avoid bus-turnaround time, and probably others. Very often, the
| only thing that matters is the memory access stalls, with all
| other instructions operating in parallel in the time that it
| takes for the memory reads and writes to complete. (Does your
| processor have handling in the execution pipeline that prevents a
| potentially expensive branch misprediction in that tight code
| loop? I don't know. But GCC and Clang do!).
|
| For a human to compete with GCC or Clang code, intuition about
| how code executes isn't sufficient. If you are not using
| sophisticated profiling tools like Intel VTune, you really won't
| have insight into whether your hand-generated assembler is
| stalling in the execution pipeline. And that is typically the
| problem that determines how well code executes. How the data must
| flow is invariant from input to output. In this case, the input
| array must be read, and a register must be set to zero or one on
| output. And both compilers, and processor execution pipelines are
| capable of doing quite extraordinary things to maximize
| opportunites for parallel execution and pipelining. So. The ONLY
| way to tell whether any of that generated code is inefficient is
| to benchmark it. Intuition is not remotely sufficient.
|
| As far as I can see, both compilers have done quite heroic and
| spectacular jobs of optimizing code. It is not at all clear
| whether the compilers know something about how memory operations
| fuse in the instruction pipeline that you don't. The only oddity
| is the extra memory write to initialize the zero array that shows
| up in a single case, which, in fairness, occurs because you have
| introduced a faux optimization in the original code. One of the
| compilers heroically (and probably correctly) optimized the bulk
| of the code, and tragically missed an opportunity to remove a
| faux optimization that YOU have introduced. Even then, it's still
| not clear that an extra memory write is going to execute slower.
| (A write to l0 cache (either one or two cpu clock cycles),
| followed by a bunch of reads from l0 cache -- does the cache
| controller allow parallel reads and writes or does it not? I
| don't know, but GCC and Clang do! Obviously not a good thing, but
| does it ACTUALLY impact performance? I don't know. And the only
| way to tell, is for you to actually profile the code.
|
| Also worth mentioning in passing: if you are not compiling with
| --march=native, all your code is being optimized for some
| prehistoric ancient least-common-denominator Intel processor,
| probably a 1990's-era 486, that nobody actually has anymore that
| has god-only-knows what inadequacies in its execution pipeline.
| So make sure you are.
|
| - Credentials: professional programmer with 45 years experience,
| including extensive experience optimizing and profiling high-
| performance graphics device drivers, and audio plugin code, some
| of which was done in the era where humans actually could speed up
| compiler-generated code by (typically) 2 or 3%, in an industry
| when 2 or 3% improvements in benchmark scores could increase
| profits by millions of dollars. Currently of the opinion that any
| optimization that produces less than a 25% performance
| improvement is just not worth the extra effort and risk.
| rsf wrote:
| > Sure, the code is strange, but it is not necessarily
| inefficient.
|
| Out of the 6 pieces of Assembly code in the article, 2 of them
| are definitely inefficient - specifically, the 2 clang ones
| that contain irrelevant writes to the stack. Even if a CPU was
| smart enough to ignore those instructions with no performance
| penalty (which in itself is doubtful), at the very least those
| instructions take up space in memory/caches unnecessarily.
|
| The gcc output when arraySize is 3 is almost certain to be
| inefficient as well, when you look at portions such as:
| mov eax, 1 test eax, eax sete
| al ret
|
| All this code is doing is to set eax to 0 and then returning.
| This could be done by simply replacing it with "xor eax, eax ;
| ret" or "mov eax, 0 ; ret" if there's a reason to avoid "xor" -
| there's already a mov there. The code as present also has the
| side effect of changing the CPU's flags, but this side effect
| can't be relied on as we return immediately, and flag values
| are not part of the returned values with this ABI.
|
| So yes, in general benchmarking is the only way to be sure. But
| when you look at the specifics of the generated code, we can
| see that at best 4 of the 6 snippets of Assembly code are
| optimal, and the actual number of optimal snippets is probably
| lower than 4 (my best guess is 2 here).
|
| All that said, I might benchmark everything later on and post a
| new article about it.
|
| > Also worth mentioning in passing: if you are not compiling
| with --march=native, all your code is being optimized for some
| prehistoric ancient least-common-denominator Intel processor,
| probably a 1990's-era 486, that nobody actually has anymore
| that has god-only-knows what inadequacies in its execution
| pipeline. So make sure you are.
|
| Yep: See https://news.ycombinator.com/item?id=46978577
| hulitu wrote:
| > Both GCC and Clang generate strange/inefficient code
|
| At the same time Anthropic anounces its own compiler. How sweet.
| rsf wrote:
| For the avoidance of doubt, I neither work for Anthropic nor do
| I have a horse in this race in any other way. I didn't even
| know about the Anthropic compiler when I wrote the blog post.
| ndesaulniers wrote:
| Was looking at the llvm case with the dead store to the stack.
|
| https://godbolt.org/z/Kb736onb4 \*\* IR Dump
| After Expand memcmp() to load/stores (expand-memcmp) \*\* ;
| Function Attrs: mustprogress nofree norecurse nounwind willreturn
| memory(argmem: read) uwtable define dso_local noundef
| zeroext i1 @isAllZeros(ptr noundef nonnull readonly align 4
| captures(none) dereferenceable(8) %0) local_unnamed_addr #0 {
| %2 = alloca %"struct.std::array", align 8 call void
| @llvm.lifetime.start.p0(ptr %2) store i64 0, ptr %2,
| align 8 %3 = load i64, ptr %0, align 4 %4 = load
| i64, ptr %2, align 8 %5 = icmp ne i64 %3, %4 %6 =
| zext i1 %5 to i32 %7 = icmp eq i32 %6, 0 call
| void @llvm.lifetime.end.p0(ptr %2) ret i1 %7 }
|
| It looks like expand-memcmp in the backend converts a call to
| bcmp to multiple loads/stores. Perhaps expand-memcmp should then
| do a round of store-to-load forwarding.
|
| Filed https://github.com/llvm/llvm-project/issues/180991 which I
| think is what the author should do in cases like this (rather
| than a blog post).
| nitwit005 wrote:
| Strange is fine. Strange what we expect to see if the compiler is
| working, as some optimizations are unintuitive.
|
| Inefficient is not fine, but there's no attempt to measure that
| here.
___________________________________________________________________
(page generated 2026-02-11 23:01 UTC)