Is auto-vectorizing two loads into one an optimization?

From: anton@mips.complang.tuwien.ac.at (Anton Ertl)
Subject: Re: Optimization by programmers
Newsgroups: comp.arch
Date: Mon, 21 Sep 2026 15:16:15 GMT
Message-ID: <2026Sep21.171615@mips.complang.tuwien.ac.at>

Thomas Koenig  writes:
>All such choices are the result of heuristics.  Bubble sort has a
>very special memory access pattern.

Ok, what would a memory pattern look like that this vectorization was
designed for?  The big slowdown comes from the fact that in the
reordering case the vectorized version performs a wide store, and in
the next iteration it performs a wide load that partially overlaps the
store.  I buy it that the analysis will have trouble seeing that (not
that it's impossible in this case, but it requires effort).

>A straightforward patch would
>very likely pessimize a lot of existing code which profits from
>auto-vectorization.

How can we test this claim?  What would such code look like that would
suffer from disabling the combining of loads that tree-slp-vectorize
performs?

I decided to test the claim by continuing to use bubble-sort, but
avoiding the stores and thus the big slowdown: Bubble-sort a
pre-sorted array.  Apart from changing the array size, initializing
the array with pre-sorted values, changing the indentation and
condensing the code to one benchmark only (stan.c has a number of
benchmarks in one file, probably due to Pascal origins), the code is
unchanged; the original code already performs N iterations of the
outer loop (rather than doing an early-out on an already-sorted
array), so one does not need to change that aspect.

The inner loop is compiled by gcc-14.2 to:

novect (-O3 -fno-tree-slp-vectorize) vect (-O3)
70: mov   (%rax),%ecx                90: movq   (%rax),%xmm0      
    mov   0x4(%rax),%esi                 pshufd $0xe5,%xmm0,%xmm1 
                                         movd   %xmm0,%esi        
                                         movd   %xmm1,%ecx        
    cmp   %esi,%ecx                      cmp    %ecx,%esi         
    jle   7e                             jle    ae      #always taken
    mov   %esi,(%rax)                    pshufd $0xe1,%xmm0,%xmm0  #x
    mov   %ecx,0x4(%rax)                 movq   %xmm0,(%rax)       #x
7e: add   $0x4,%rax                  ae: add    $0x4,%rax         
    cmp   %rax,%rdi                      cmp    %rdi,%rax         
    jne   70                             jne    90                

In the presorted benchmark, the jle branch is always taken, and the
store instructions marked #x are never executed, so the
microarchitectural pitfall never occurs.

The novect code executes 7 instructions per iteration, the vect code
9, but maybe the vectorization manages to make the code execute faster
for some reason.  Let's see (on a Xeon E-2388G (Rocket Lake):

cycles        instructions
1,867,053,886 12,602,009,442 novect
3,603,774,972 16,202,621,392 vect

So the novect variant is twice as fast as the vect variant even
without the slowdown from the store-to-partially-overlapping loads.
If there is a speedup to be had from combining loads with
tree-slp-vectorize, it hides itself well.

The performance of the Rocket Lake for the novect variant is
remarkable.  It manages 6.75 IPC on average, which is very close to
the speed of light for this microarchitecture, with 5 renamer slots,
with compare-and-branch macro-instructions occupying only one renamer
slot (so the speed of light for a loop with 5 non-branches and 2
branches is 7 IPC).  Top-down microarchitectural analysis reports:

TopdownL1                 #      2.7 %  tma_backend_bound      
                          #      1.6 %  tma_bad_speculation    
                          #     -0.0 %  tma_frontend_bound     
                          #     95.7 %  tma_retiring           

The ideal is that 100% of the rename slots are consumed by retiring,
but the normal case is that many of the slots are lost due to the
other three reasons, and retiring numbers >50% are already pretty
good.

The Rocket Lake manages two taken branches per cycle here, while I had
thought it was only capable of doing one non-taken and one taken
branch per cycle.

- anton
-- 
'Anyone trying for "industrial quality" ISA should avoid undefined behavior.'
  Mitch Alsup, 
[ICO]NameLast modifiedSizeDescription

[PARENTDIR]Parent Directory  -  
[   ]perf.data.old2026-09-21 18:31 93K 
[   ]perf.data2026-09-21 18:33 152K 
[TXT]bubble-presorted.c2026-09-21 17:37 1.5K 
[   ]bubble-presorted-vect2026-09-21 17:39 16K 
[   ]bubble-presorted-novect2026-09-21 17:39 16K 
[   ]Makefile2026-09-21 17:35 426  

Apache/2.4.67 (Debian) OpenSSL/3.0.18 Server at www.complang.tuwien.ac.at Port 80