惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

酷 壳 – CoolShell
酷 壳 – CoolShell
P
Privacy & Cybersecurity Law Blog
G
GRAHAM CLULEY
T
The Exploit Database - CXSecurity.com
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Project Zero
Project Zero
S
Security @ Cisco Blogs
TaoSecurity Blog
TaoSecurity Blog
A
Arctic Wolf
Webroot Blog
Webroot Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
Security Latest
Security Latest
H
Heimdal Security Blog
N
News and Events Feed by Topic
N
News | PayPal Newsroom
T
Tor Project blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
GbyAI
GbyAI
The Last Watchdog
The Last Watchdog
Y
Y Combinator Blog
宝玉的分享
宝玉的分享
Scott Helme
Scott Helme
A
About on SuperTechFans
M
MIT News - Artificial intelligence
V
V2EX
V
Visual Studio Blog
Recorded Future
Recorded Future
博客园 - 叶小钗
F
Fortinet All Blogs
L
Lohrmann on Cybersecurity
The GitHub Blog
The GitHub Blog
博客园 - Franky
P
Proofpoint News Feed
MyScale Blog
MyScale Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
S
Secure Thoughts
D
DataBreaches.Net
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
博客园 - 三生石上(FineUI控件)
I
InfoQ
SecWiki News
SecWiki News
Blog — PlanetScale
Blog — PlanetScale
Engineering at Meta
Engineering at Meta
J
Java Code Geeks
B
Blog RSS Feed
AWS News Blog
AWS News Blog
Know Your Adversary
Know Your Adversary
V
Vulnerabilities – Threatpost
H
Help Net Security

Hacker News: Front Page

SPICE simulation → oscilloscope → verification with Claude Code — Lucas Gerads GitHub - GainSec/AutoProber: Hardware hacker’s flying probe automation stack for agent-driven target discovery, microscope mapping, safety-monitored CNC motion, probe review, and controlled pin probing. Introducing Claude Opus 4.7 Qwen Studio The Future of Everything is Lies, I Guess: Where Do We Go From Here? GitHub - SeanFDZ/macmind: Single-layer transformer in HyperTalk for the classic Macintosh Virginia Bans Sale of Geolocation Data Show HN: Agent-cache – Multi-tier LLM/tool/session caching for Valkey and Redis Ancient DNA reveals pervasive directional selection across West Eurasia [pdf] AI cybersecurity is not proof of work Moving a large-scale metrics pipeline from StatsD to OpenTelemetry / Prometheus GitHub - Nightmare-Eclipse/RedSun: The Red Sun vulnerability repository GitHub - SethPyle376/hiraeth: Local AWS emulator focused on fast integration testing, with SQS support, SQLite-backed state, and a debug-friendly web UI. A Better Ludum Dare; Or, How to Ruin a Legacy GitHub - macOS26/Agent: Any AI, replaces Claude Code, Cursor, OpenClaw. Over 18 LLM providers (Claude, OpenAI, Gemini, Ollama, Zai, HF, Qwen) wired into a native Mac app that writes code, builds Xcode projects, bumps versions, manages git, automates Safari, use AppleScript, JS or Accessibility, extend Agent! w/ MCP Servers, run tasks from your iPhone via Messages. YouTube now lets you turn off Shorts I Made a Terminal Pager Burgers | マクドナルド公式 Commands — HackerNews CLI documentation ChatGPT for Excel PiCore - Raspberry Pi Port of Tiny Core Linux Live Nation illegally monopolized ticketing market, jury finds Google Broke Its Promise to Me. Now ICE Has My Data. Founding Engineer at Adaptional | Y Combinator CRISPR takes important step toward silencing Down syndrome’s extra chromosome GitHub - saffron-health/libretto: The AI toolkit for building reliable browser automations US v. Heppner (S.D.N.Y. 2026) no attorney-client privilege for AI chats [pdf] Unexpected €54k billing spike in 13 hours: Firebase browser key without API restrictions used for Gemini requests Fragments: April 14 Cal.com Goes Closed Source: Why AI Security Is Forcing Our Decision | Cal.com - Scheduling Software for Online Bookings Laravel raised money and now injects ads directly into your agent Codex Hacked a Samsung TV Tech Valuations Back to Pre-AI Boom Levels A perfectable programming language — Soter GitHub - halfwhey/claudraband: Claude Code for the Power User Partnership through Play: Investigating How Long-Distance Couples Use Digital Games to Facilitate Intimacy Textbooks and Methods of Note-Taking in Early Modern Europe (2008) Eternity in six hours: Intergalactic spreading of intelligent life (2013) Seven countries now generate 100% of their electricity from renewable energy Tell HN: OpenAI silently removed Study Mode from ChatGPT Pro Max 5x Quota Exhausted in 1.5 Hours Despite Moderate Usage Show HN: Oberon System 3 runs natively on Raspberry Pi 3 (with ready SD card) Tell HN: docker pull fails in spain due to football cloudflare block Bring Back Idiomatic Design No one owes you supply-chain security GitHub - xsawyerx/curl-doom: DOOM, played over cURL Apple update turns Czech mate for locked-out iPhone user The Grand Line Cache TTL silently regressed from 1h to 5m around early March 2026, causing quota and cost inflation Building a Z-Machine in the worst possible language The peril of laziness lost Iran war: We spoke to the man making Lego-style AI videos that experts say are powerful propaganda AI Will Be Met With Violence, and Nothing Good Will Come of It GitHub - duguyue100/midnight-captain: Inspired by Midnight Commander, tailored to my taste. How to build a `git diff` driver · Jamie Tanna | Software Engineer Center for Responsible, Decentralized Intelligence at Berkeley The Local Universe’s Expansion Rate Is Clearer Than Ever, but Still Doesn’t Add Up - A new synthesis of astronomical measurements confirms a persistent mismatch that could point to physics beyond current models The disturbing white paper Red Hat is trying to erase from the internet – OSnews NetBlocks (@netblocks@mastodon.social) The Future of Everything is Lies, I Guess: Annoyances ‘Abhorrent’: the inside story of the Polymarket gamblers betting millions on war Productive procrastination — Max van IJsselmuiden maps, territory and LMs 447 Terabytes per Square Centimetre at Zero Retention Energy: Non-Volatile Memory at the Atomic Scale on Fluorographane Show HN: Pardonned.com – A searchable database of US Pardons 20 Years on AWS and Never Not My Job The Seasons are Wrong The FAA wants gamers to apply for air traffic control jobs Artemis II crew splashes down near San Diego after historic moon mission Why weekends are under threat We gave an AI a 3 year retail lease in SF and asked it to make a profit | Andon Labs How a dancer with ALS used brainwaves to perform live On filing the corners off my MacBooks Installing every* Firefox extension OpenClaw’s memory is unreliable, and you don’t know when it will break Steve Blank Nowhere Is Safe Chimpanzees in Uganda locked in vicious 'civil war', say researchers watgo - a WebAssembly Toolkit for Go linux/Documentation/process/coding-assistants.rst at master · torvalds/linux GitHub - callumlocke/json-formatter: Makes JSON easy to read. Founding Product Engineer at Bild AI | Y Combinator A compelling title that is cryptic enough to get you to take action on it GitHub - Keychron/Keychron-Keyboards-Hardware-Design: Industrial design files for Keychron keyboards and mice. 100+ models with CAD assets in STEP, DXF, DWG, and PDF. Source-available, with commercial use allowed for original compatible accessories within the license terms. [ANNOUNCE] WireGuardNT v0.11 and WireGuard for Windows v0.6 Released 1D-Chess Helium Is Hard to Replace Keeping a Postgres queue healthy — PlanetScale Serenity Forge (@serenityforge.com) Our response to the Axios developer tool compromise Do Americans read print books, e-books or audiobooks more? Uncharted island soon to appear on nautical charts The Problem That Built an Industry Fragments: April 2 Python Release Python install manager 26.1 Bitcoin miners are losing $19,000 on every BTC produced as difficulty drops 7.8% God sleeps in the minerals Harness engineering: leveraging Codex in an agent-first world Apple Silicon and Virtual Machines: Beating the 2 VM Limit What have been the greatest intellectual achievements? The APL Programming Language Source Code
Tuning LLVM's SLP Vectorizer Cost Model
2026-05-24 · via Hacker News: Front Page

Similar to my last post, this writeup covers how I solved a performance regression on LLVM by analyzing a benchmark from a RISCV target.

TLDR
A recent LLVM patch introduced ordered vector reductions to replace a chain of scalar fadds, but it triggered a performance regression on a benchmark by failing to account for cost of building the initial vector per iteration. This in turned caused unprofitable code to be deemed “profitable.”
PR, Issue

The Regression

Looking at Igalia’s LNT instance for the BPI-F3, I noticed this particular benchmark with a delta of 89%. Specifically, there was an increase in ~26% issued instructions and a ~48% increase in cycles.

I have attached two more pictures right below, with the first one being the assembly of a basic block from the older build and the corresponding assembly from the newer build.

Info

Bn here refers to Billions of cycles. This basic block is basically taking twice as many cycles to execute.

We can see that that newer build of LLVM is performing a sequence of fsd instructions, also known as Float Store Double. It’s essentially storing the floating point values from those registers onto the stack. Specifically, it’s storing the value at the address s1 + 0x80.

From a preceding basic block that I have not included here, I know that value of the register a5 to be equal to s1 + 0x80 from this instruction.

The Vector Load Instruction vle64.v is loading the values from memory at the address at a5 (s1 + 0x80) into the vector register v16.

Finally, it executes the vfredosum.vs instruction (Ordered floating-point sum), which performas the following for a vector register of size VL.

\[ vd[0] = \left( \dots \left( \left( vs1[0] + vs2[0] \right) + vs2[1] \right) + \dots + vs2[VL-1] \right) \]

The new codegen is basically trying to replace the ordered fadd instructions in the first basic block with this vector sum reduction instruction. I hope this diagram may illustrate this better, with what was previously happening versus what is currently occurring. From the images above, it can be observed that the new code is significantly more expensive in terms of cycles.

    graph LR
    %% Left-to-Right top level allows parallel tracks to scale height independently
    V_IN["Original Source Data<br/><i>(Residing in Scalar Registers)</i>"]

    %% Link to Scalar Track
    V_IN -->|Old Execution| S1
    
    %% Link to Vector Track
    V_IN -->|New Execution| FSD1

    %% Left Side Track: Original Scalar Chain
    subgraph ScalarChain ["Original Intent: Ordered fadd Chain"]
        direction TB
        S1["fadd (START_VAL + SCALAR_VAL_0)"] --> S2["fadd (Result + SCALAR_VAL_1)"]
        S2 --> S3["fadd (Result + SCALAR_VAL_2)"]
        S3 --> S4["fadd (Result + SCALAR_VAL_3)<br/><code>FINAL_SCALAR_SUM</code>"]
    end

    %% Right Side Track Part 1: Memory Gather (Now cleanly scales to its actual contents)
    subgraph Gather ["Memory Gather (Stack Spilling Penalty)"]
        direction TB
        FSD1["Store SCALAR_VAL_0 to s1 + 0x80"]
        FSD2["Store SCALAR_VAL_1 to s1 + 0x88"]
        FSD3["Store SCALAR_VAL_2 to s1 + 0x90"]
        FSD4["Store SCALAR_VAL_3 to s1 + 0x98"]
    end

    subgraph VectorOps ["Vector Load & Reduction"]
        direction TB
        VLE["Vector Load<br/><i>(Loads from memory into vector register v16)</i>"] --> VFRED["Vector Ordered Sum Reduction<br/><i>(vfredosum)</i>"]
    end

    %% Connect the two vector halves sequentially
    FSD4 -->VLE

    %% --- STYLING BLOCK ---
    style ScalarChain fill:none,stroke:#888888,stroke-dasharray: 5 5
    
    %% Gather Penalty (Soft Red Glow)
    style Gather fill:#b71c1c18,stroke:#ef4444,stroke-width:2px,rx:8,ry:8
    
    %% Vector Operations (Soft Blue Glow)
    style VectorOps fill:#0d47a118,stroke:#3b82f6,stroke-width:2px,rx:8,ry:8
  

Info

If you visit the link yourself, you may notice that there is also another basic block further down that also has a significant increase in cycles compared to its older counterpart. I chose not to include that as both of them are identical, so fixing one fixes the other.

The Where?

To narrow down where these new fsd and vfredosum.vs instructions are introduced, I ran the command below to get emit the LLVM IR. The output of this commend will give us the intermediate representation produced by the middle-end. If we can observe IR code that would result in those instructions, we can rule out the backend.

$lbd/bin/clang -O3 \
  --target=riscv64-unknown-linux-gnu \
  -march=rva22u64_v \
  --gcc-toolchain=/usr \
  --sysroot=/usr/riscv64-linux-gnu \
  -I. \
  -DFP_ABSTOLERANCE=1e-5 \
  -S -emit-llvm seidel-2d.c \
  -o seidel-2d.ll
for.body8.i:                                      ; preds = %for.body8.i, %for.cond5.preheader.i
  %18 = phi double [ %.pre117.i, %for.cond5.preheader.i ], [ %26, %for.body8.i ]
  %19 = phi double [ %.pre116.i, %for.cond5.preheader.i ], [ %18, %for.body8.i ]
  %20 = phi double [ %.pre115.i, %for.cond5.preheader.i ], [ %25, %for.body8.i ]
  %21 = phi double [ %.pre114.i, %for.cond5.preheader.i ], [ %div.i40, %for.body8.i ]
  %22 = phi double [ %.pre113.i, %for.cond5.preheader.i ], [ %24, %for.body8.i ]
  %23 = phi double [ %.pre.i, %for.cond5.preheader.i ], [ %22, %for.body8.i ]
  %indvars.iv.i38 = phi i64 [ 1, %for.cond5.preheader.i ], [ %indvars.iv.next.i39, %for.body8.i ]
  %add.i = fadd double %22, %23
  %indvars.iv.next.i39 = add nuw nsw i64 %indvars.iv.i38, 1
  %arrayidx23.i = getelementptr inbounds nuw [8 x i8], ptr %arrayidx.i37, i64 %indvars.iv.next.i39
  %24 = load double, ptr %arrayidx23.i, align 8, !tbaa !14
  %arrayidx34.i = getelementptr inbounds nuw [8 x i8], ptr %17, i64 %indvars.iv.i38
  %arrayidx40.i = getelementptr inbounds nuw [8 x i8], ptr %17, i64 %indvars.iv.next.i39
  %25 = load double, ptr %arrayidx40.i, align 8, !tbaa !14
  %arrayidx60.i = getelementptr inbounds nuw [8 x i8], ptr %arrayidx44.i, i64 %indvars.iv.next.i39
  %26 = load double, ptr %arrayidx60.i, align 8, !tbaa !14
  %27 = insertelement <8 x double> poison, double %24, i32 0
  %28 = insertelement <8 x double> %27, double %add.i, i32 1
  %29 = insertelement <8 x double> %28, double %21, i32 2
  %30 = insertelement <8 x double> %29, double %20, i32 3
  %31 = insertelement <8 x double> %30, double %25, i32 4
  %32 = insertelement <8 x double> %31, double %19, i32 5
  %33 = insertelement <8 x double> %32, double %18, i32 6
  %34 = insertelement <8 x double> %33, double %26, i32 7
  %35 = call double @llvm.vector.reduce.fadd.v8f64(double -0.000000e+00, <8 x double> %34)
  %div.i40 = fdiv double %35, 9.000000e+00
  store double %div.i40, ptr %arrayidx34.i, align 8, !tbaa !14
  %exitcond.not.i41 = icmp eq i64 %indvars.iv.next.i39, 1999
  br i1 %exitcond.not.i41, label %for.inc66.i, label %for.body8.i, !llvm.loop !25

We can observe here a chain of insertelement instructions building an <8 x double> vector. The instruction follows this format:

%result = insertelement <vector_type> <source_vector>, <scalar_value>, <index>

Using the snippet above, we can see that %27 is the initial vector. Because there is no prior value, the source is marked as poison, the scalar value it’s inserting is the value %24, and the index is 0. From there, each subsequent instruction feeds the result of the previous line its source vector operand.

Once the vector is completely packed at %34, it serves as an operand to the vector.reduce.fadd intrinsic (maps to vfredosum). The LLVM LangRef explains how this intrinsic sums up the elements in the second operand (34%), and adds the first operand (0.0), and stores the result (%35). The pseudocode provided by the LangRef probably explains this better.

float sequential_fadd(start_value, input_vector)
  result = start_value // This is -0.00000
  for i = 0 to length(input_vector) // Traverses every element in %34
    result = result + input_vector[i]
  return result

Intrinsic

LLVM intrinsics are built-in functions that map directly to low-level architecture instructions, but can be hard to express in normal LLVM IR semantics. They give the compiler a better understanding of a hardware operation, allowing it to generate more efficient assembly.

Since the behaviour is already introduced by the middle-end, we can rule out the RISCV backend. I can confidently say that it was some middle-end pass causing this behaviour.

This gives us a better idea of what to isolate. I ran git log on the dates of when the builds were ran, and grepped for keywords. After hitting a couple of dead-ends, I found a promising lead when I filtered specifically for reduction.

git log --since="2026-05-19 00:00:00 +0000" --until="2026-05-20 00:13:39 +0000" --oneline | grep reduc

e28e7ec30357 Reland [VectorCombine] foldShuffleChainsToReduce - add support for partial vector reductions (#197659)
dfa05b675eee [VectorCombine] Fold reduce.add == 0 into reduce.[or,umax] == 0 (#180001)
230980947083 [SLP] Support ordered fadd reduction via reduction intrinsics
2d9407bb56b7 [X86] Add handling for sub-128bit minmax reductions (#198319)

The third commit immediately stood out, as its description would perfectly explain the new codegen. After building LLVM locally before and with this commit, I was able to determine that the commit was the culprit.

The Why?

For some reason, the SLP Vectorizer thinks it more profitable to performed the fadd reduction here when we know from the benchmark that its not. The key things that author added in that commit were the following:

  • matchOrderedReduction
    • Responsible for finding possible instructions for ordered reduction.
  • tryToReduceOrdered
    • This is the function that actually checks if its profitable, and commits to the ordered reduction if it is.

SLP Vectorizer

I think the LLVM docs does a decent job at a high-level overview of what the SLPVectorizer does. LLVM has two vectorizers, and the SLP Vectorizer is one of them. The pass’ main goal is to combine independent instructions into vector instructions. This is a pretty important in terms of performance. By vectorizing what was previously scalar instructions, you can perform the same operations on multiple values at once. This is especially beneficial when operations within a loop can be vectorized, as these tend to be the hotspots within a program.

In this case though, the SLP Vectorizer was vectorizing a sequence of ordered instructions. I think the commit message itself will elucidate the goal of this commit better.

Previously, the SLP vectorizer could only vectorize ordered reductions by keeping the original scalar chain and emitting extractelement instructions. The new path replaces the scalar chain with a vector ordered reduction intrinsic (where profitable), which allows the backend to lower it more efficiently.

The following test case from the PR further illustrates the goal of the commit.

- ; CHECK-NEXT:    [[TMP28:%.*]] = extractelement <4 x float> [[TMP17]], i32 0
- ; CHECK-NEXT:    [[TMP32:%.*]] = extractelement <4 x float> [[TMP17]], i32 1
- ; CHECK-NEXT:    [[ADD30:%.*]] = fadd float [[TMP28]], [[TMP32]]
- ; CHECK-NEXT:    [[TMP20:%.*]] = extractelement <4 x float> [[TMP17]], i32 2
- ; CHECK-NEXT:    [[ADD31:%.*]] = fadd float [[ADD30]], [[TMP20]]
- ; CHECK-NEXT:    [[TMP30:%.*]] = extractelement <4 x float> [[TMP17]], i32 3
- ; CHECK-NEXT:    [[ADD33:%.*]] = fadd float [[ADD31]], [[TMP30]]
+ ; CHECK-NEXT:    [[ADD33:%.*]] = call float @llvm.vector.reduce.fadd.v4f32(float -0.000000e+00, <4 x float> [[TMP17]])

Before this commit, LLVM had to extract elements out of a vector to perform a chain of scalar fadd. This commit notices that it’s cheaper to keep them in the vector, and use the reduction intrinsic (which maps to vfredosum on RISCV).

    graph TD
    %% Top Level Input
    V_IN["Source Vector Input<br/><code>VECTOR_INPUT</code>"]

    %% Split Path
    V_IN -->|Old Code Generation| OldBehavior
    V_IN -->|Optimized SLP Rewrite| NewBehavior

    %% Left Side Path: Old Behavior (The Costly Extraction Chain)
    subgraph OldBehavior ["Before: Extraction Penalty"]
        direction TB
        
        %% Extraction Row
        subgraph Extractions ["Explicit Extractions"]
            E0["extractelement (EL0)"]
            E1["extractelement (EL1)"]
            E2["extractelement (EL2)"]
            E3["extractelement (EL3)"]
        end
        
        %% Chain
        A0["fadd (EL0 + EL1)"]
        A1["fadd (Result + EL2)"]
        A2["fadd (Result + EL3)<br/><code>FINAL_SCALAR_SUM</code>"]
        
        %% Wire it up
        E0 & E1 --> A0
        A0 & E2 --> A1
        A1 & E3 --> A2
    end

    %% Right Side Path: New Behavior (The Native Intrinsic)
    subgraph NewBehavior ["Direct Reduction"]
        direction TB
        INTRINSIC["<b>@llvm.vector.reduce.fadd</b><br/><i>Executes natively inside the vector unit</i><br/><code>FINAL_SCALAR_SUM</code>"]
    end

    %% --- STYLING BLOCK ---
    style OldBehavior fill:none,stroke:#888888,stroke-dasharray: 5 5
    style NewBehavior fill:none,stroke:#888888,stroke-dasharray: 5 5
    style Extractions fill:none,stroke:none
    
    %% Highlight the bad overhead path in red
    style E0 fill:#b71c1c18,stroke:#ef4444,stroke-width:1px
    style E1 fill:#b71c1c18,stroke:#ef4444,stroke-width:1px
    style E2 fill:#b71c1c18,stroke:#ef4444,stroke-width:1px
    style E3 fill:#b71c1c18,stroke:#ef4444,stroke-width:1px
    
    %% Highlight the clean vector intrinsic win in green
    style INTRINSIC fill:#1b5e2018,stroke:#4caf50,stroke-width:2px,rx:8,ry:8
  

TreeEntry

The TreeEntry is a datastructure used to hold relevant data to determine which scalar operations can be combined into vector operations. The log below was printed using TE.dump() on the benchmark.

==== [SLP Cost Loop] Examining TreeEntry (Idx: 0) ====
0.
Scalars: 
    %45 = load double, ptr %arrayidx23.i75, align 8, !tbaa !16
    %add.i73 = fadd double %43, %44
    %42 = phi double [ %.pre114.i65, %for.cond5.preheader.i58 ], [ %div.i86, %for.body8.i71 ]
    %41 = phi double [ %.pre115.i67, %for.cond5.preheader.i58 ], [ %46, %for.body8.i71 ]
    %46 = load double, ptr %arrayidx40.i80, align 8, !tbaa !16
    %40 = phi double [ %.pre116.i68, %for.cond5.preheader.i58 ], [ %39, %for.body8.i71 ]
    %39 = phi double [ %.pre117.i70, %for.cond5.preheader.i58 ], [ %47, %for.body8.i71 ]
    %47 = load double, ptr %arrayidx60.i84, align 8, !tbaa !16
State: NeedToGather
MainOp: NULL
AltOp: NULL
VectorizedValue: NULL
ReuseShuffleIndices: Empty
ReorderIndices: 
UserTreeIndex: <invalid>

I’m not a 100% sure why there are phi instructions within Scalars, but I’m fairly sure it’s because the function matchOrderedReduction, which is responsible for finding ordered operations, walks back to the root of the instructions. The comments on this function may explain this better.

  /// Analyze whether \p Root forms a linearized ordered reduction chain.
  /// If \p MatchLHS is true, analyzes LHS-associated (left-linearized) chains
  /// where the chain recurses on the LHS and the RHS at each level is a leaf:
  ///   ((((v0 op v1) op v2) op v3) op v4)
  /// If \p MatchLHS is false, analyzes RHS-associated (right-linearized) chains
  /// where the chain recurses on the RHS and the LHS at each level is a leaf:
  ///   (v0 op (v1 op (v2 op (v3 op v4))))
  /// Leaf values are stored in ReducedVals.back() in accumulation order
  /// (innermost pair first, outermost last), e.g. [v0,v1,v2,v3,v4] for
  /// LHS-associated and [v4,v3,v2,v1,v0] for RHS-associated chains.
  /// \returns true if the chain is a valid ordered reduction.

The key thing to notice is that the UserTreeIndex is invalid. tryToReduceOrdered builds an isolated tree by calling V.buildTree(VL, IgnoreList) specifically for the ordered reduction candidates. Since this is the root, there is no parent, so EI (EdgeInfo) would be null.

Tracing the logs to the code

I first raised an issue and pinged the author of the commit with a kind of hacky solution. He suggested an inaccurate TTI (Target Transform Info) for this particular target.

Issue: https://github.com/llvm/llvm-project/issues/199267

Target Transform Info (TTI)

LLVM middle-end tries to be target-agnostic. But it’s easy to imagine scenarios where we would need info on the target during the middle-end. For example, building the vector in this benchmark might be cheaper on other targets. Target Transform Info provides hooks that can be overridden by backends to provide info to the middle-end.

With this in mind, I kept investigating the code alongside the logs. I noticed this piece of code from the diff.

+      LLVM_DEBUG(dbgs() << "SLP: Vectorizing ordered reduction at cost:" << Cost
+                        << ". (HorRdx)\n");

When the SLPVectorizer commits to performing an ordered reduction, it prints that debug statement alongside the cost. Searching through the log for this debug message, I found two instances of “Vectorizing ordered…”, which corresponds to the two basic blocks in this benchmark.

SLP: Adding cost 15 for bundle Idx: 0, n=8 [  %24 = load double, ptr %arrayidx23.i, align 8, !tbaa !16, ..].
SLP: Current total cost = 15
SLP: Adding cost -4 for reduction of n=8 [  %24 = load double, ptr %arrayidx23.i, align 8, !tbaa !16, ..] (It is a splitting reduction)
Scale 1996002000 For entry 0
SLP: Spill Cost = 0.
SLP: Extract Cost = 0.
SLP:  Reduction Cost = -7984008000.
SLP: Total Cost = -7984007985.
SLP: Found cost = -7984007985 for ordered reduction
SLP: Vectorizing ordered reduction at cost:-7984007985. (HorRdx)

From these logs, we can observe a couple of things:

  • The value of Scale, which is the number of iterations in a loop, is 1996002000.
  • The Reduction Cost was -7984008000, which I deduce is from the -4 being multiplied by the Scale value
  • The TreeCost which encompasses the cost of the bundle in this case, which corresponds to the instructions building the vector to reduce it, is 15
  • The total cost was -7894007895, the TreeCost subtracted by the ReductionCost

\[ \begin{aligned} &\text{TreeCost} = 15 \\ &\text{ReductionCost} = -4 \\ &\text{Scale} = 1,996,002,000 \\[1em] &\text{ReductionCost}' = \text{ReductionCost} \times \text{Scale} \\ &\text{Total Cost} = \text{TreeCost} + \text{ReductionCost}' \\ &\text{Total Cost} = 15 + (-7,984,008,000) \\ &\text{Total Cost} = -7,984,007,985 \end{aligned} \]

I hope the problem is clearer now. In this benchmark, the TreeCost - the cost to build the vector - is not scaled alongside the ReductionCost. This results in really big negative value, leading the SLPVectorizer to think this ordered reduction is profitable.

If you’re wondering where exactly the Scale came to be, this snippet from the source code can explain this.

// seidel-2d.h
/* Default to LARGE_DATASET. */
# if !defined(MINI_DATASET) && ...
#  define LARGE_DATASET
# endif

#  ifdef LARGE_DATASET
#   define TSTEPS 500
#   define N 2000
#  endif

// seidel-2d.c
for (t = 0; t <= _PB_TSTEPS - 1; t++)
  for (i = 1; i <= _PB_N - 2; i++)
    for (j = 1; j <= _PB_N - 2; j++)

\[ \begin{aligned} &\text{TSTEPS} = 500 \\ &\text{_PB_N} = 2000 \\[1em] &\text{Total Iterations} = \text{TSTEPS} \times (\text{_PB_N} - 2) \times (\text{_PB_N} - 2) \\ &\text{Total Iterations} = 500 \times 1998 \times 1998 \\ &\text{Total Iterations} = 1,996,002,000 \end{aligned} \]

If we look at the function getTreeCost, we can see how ReductionCost is scaled.

InstructionCost BoUpSLP::getTreeCost(InstructionCost TreeCost,
                                     ArrayRef<Value *> VectorizedVals,
                                     InstructionCost ReductionCost,
                                     Instruction *RdxRoot) {
  
  ...

  InstructionCost Cost = TreeCost;

  SmallDenseMap<std::tuple<const TreeEntry *, Value *, Instruction *>, uint64_t>
      EntryToScale;
  auto ScaleCost = [&](InstructionCost C, const TreeEntry &TE,
                       Value *Scalar = nullptr, Instruction *U = nullptr) {
    if (!C.isValid() || C == 0)
      return C;
    uint64_t &Scale =
        EntryToScale.try_emplace(std::make_tuple(&TE, Scalar, U), 0)
            .first->getSecond();
    if (!Scale)
      Scale = getScaleToLoopIterations(TE, Scalar, U);
    LLVM_DEBUG(dbgs() << "Scale " << Scale << " For entry " << TE.Idx << "\n");
    return C * Scale;
  };
  Instruction *ReductionRoot = RdxRoot;
  if (UserIgnoreList) {
    // Scale reduction cost to the factor of the loop nest trip count.
    ReductionCost = ScaleCost(ReductionCost, *VectorizableTree.front().get(),
                              /*Scalar=*/nullptr, ReductionRoot);
  }

  // Add the cost for reduction.
  Cost += ReductionCost;

  ...

}

ReductionCost is being multiplied by the valued returned by getScaleToLoopIterations. The function getScaleToLoopIterations also has access to *RdxRoot, which gives getScaleToLoopIterations awareness of the parent block, and lets it figure out the number of iterations if the block is a loop.

To figure out why the TreeCost is not being scaled, lets again compare the logs to the code. By searching for “for bundle:”, I was able to find this snippet of the code in a function called calculateTreeCostAndTrimNonProfitable. As the name suggests, this is the function responsible for calculating the cost of TreeCost.

InstructionCost
BoUpSLP::calculateTreeCostAndTrimNonProfitable(ArrayRef<Value *> VectorizedVals,
                                               Instruction *RdxRoot) {

    ...

    InstructionCost C = getEntryCost(&TE, VectorizedVals, CheckedExtracts);
    uint64_t Scale = 0;
    bool CostIsFree = C == 0;

    ...

    if (!CostIsFree && !Scale) {
      Scale = IsGatherLike ? getGatherNodeEffectiveScale(TE)
                           : getScaleToLoopIterations(TE);
      C *= Scale;
      EntryToScale.try_emplace(&TE, Scale);
      if (!TE.isGather() && TE.hasState()) {
        PrevVecParent = TE.getMainOp()->getParent();
        PrevScale = Scale;
      }
    }
    Cost += C;
    NodesCosts.try_emplace(&TE, C);
    LLVM_DEBUG(dbgs() << "SLP: Adding cost " << C << " for bundle "
                      << shortBundleName(TE.Scalars, TE.Idx) << ".\n"
                      << "SLP: Current total cost = " << Cost << "\n");

    ...

}

To get a better idea of what was happening, I added debug logs. From these logs, I determined that getGatherNodeEffectiveScale is called to calculate the Scale value here. This function then calls getScaleToLoopIterations.

uint64_t BoUpSLP::getScaleToLoopIterations(const TreeEntry &TE, Value *Scalar,
                                           Instruction *U) {
  BasicBlock *Parent = nullptr;
  
  ... 

  } else if (TE.isGather() || TE.State == TreeEntry::SplitVectorize) {
    EdgeInfo EI = TE.UserTreeIndex;
    while (EI.UserTE) {
      ...
    }
    if (!Parent)
      return 1;
  } else {

  ...

}

In this case, EI.UserTE is null, so the (!Parent) would return true and the function would just return 1.

Landing a fix

I’ll be upfront in that the author, alexey-bataev, heavily guided me towards this fix. Take a look at the issue if you want to see my initial ideas on trying to solve this.

Here is a link to the final PR as well as the precommit test:

The solution was to pass the RdxRoot instruction to calculateTreeCostAndTrimNonProfitable, and subsequently to getGatherNodeEffectiveScale and getScaleToLoopIterations, so that they have the loop context needed to correctly scale TreeCost.

In simpler terms, Scale was previously being assigned the value of 1 because the pass did not have access to the parent block of the insertelement instructions. This patch addresses that.

    graph TD
    %% Main execution flow container
    subgraph Pipeline ["The Fix: Passing RdxRoot"]
        direction TB
        
        %% Step 1
        A["<b>tryToReduceOrdered</b><br/>Identifies reduction and holds reference to <code>RdxRootInst</code>"]
        
        %% Step 2
        B["<b>calculateTreeCostAndTrimNonProfitable</b><br/>Accepts new parameter: <code>Instruction *RdxRoot</code>"]
        
        %% Step 3 (The Ternary Switch)
        C{"Is the node<br/>Gather-Like?"}
        
        %% Step 4a
        D["<b>getGatherNodeEffectiveScale(TE, RdxRoot)</b><br/><i>(Applies to Root Node at TE.Idx == 0)</i>"]
        
        %% Step 4b
        E["<b>getScaleToLoopIterations(TE)</b>"]
        
        %% Step 5
        F["<b>getScaleToLoopIterations(TE, nullptr, U)</b><br/>Bypasses missing tree parent by anchoring to <code>U</code> (RdxRoot)"]

        %% Define flow connections
        A -->|Passes RdxRootInst| B
        B --> C
        C -->|Yes: Node is isolated| D
        C -->|No: Standard node| E
        D -->|Passes U to find loop context| F
    end

    %% --- STYLING BLOCK ---
    %% Neutral structural wrapper for the pipeline
    style Pipeline fill:none,stroke:#888888,stroke-dasharray: 5 5
    
    %% Standard nodes styling
    style A fill:none,stroke:#888888,stroke-width:1px
    style B fill:none,stroke:#888888,stroke-width:1px
    style C fill:none,stroke:#888888,stroke-width:1px
    style E fill:none,stroke:#888888,stroke-width:1px
    
    %% Highlights for where the fix actively changes behavior (Soft Green Glow)
    style D fill:#1b5e2018,stroke:#4caf50,stroke-width:2px,rx:8,ry:8
    style F fill:#1b5e2018,stroke:#4caf50,stroke-width:2px,rx:8,ry:8
  
diff --git a/llvm/lib/Transforms/Vectorize/SLPVectorizer.cpp b/llvm/lib/Transforms/Vectorize/SLPVectorizer.cpp
index 54a4d6b68b2e..d90eba7118b3 100644
--- a/llvm/lib/Transforms/Vectorize/SLPVectorizer.cpp
+++ b/llvm/lib/Transforms/Vectorize/SLPVectorizer.cpp
@@ -2176,7 +2176,8 @@ public:
   /// Calculates the cost of the subtrees, trims non-profitable ones and returns
   /// final cost.
   InstructionCost
-  calculateTreeCostAndTrimNonProfitable(ArrayRef<Value *> VectorizedVals = {});
+  calculateTreeCostAndTrimNonProfitable(ArrayRef<Value *> VectorizedVals = {},
+                                        Instruction *RdxRoot = nullptr);
 
   /// \returns the vectorization cost of the subtree that starts at \p VL.
   /// A negative number means that this is profitable.
@@ -3996,7 +3997,8 @@ private:
   /// LICM hoisting that optimizeGatherSequence() performs after vectorization
   /// for inserts with loop-invariant operands. Falls back to the whole-entry
   /// scale when per-lane information is unavailable or the feature is off.
-  uint64_t getGatherNodeEffectiveScale(const TreeEntry &TE);
+  uint64_t getGatherNodeEffectiveScale(const TreeEntry &TE,
+                                       Instruction *U = nullptr);
 
   /// Get the loop nest for the given loop \p L.
   ArrayRef<const Loop *> getLoopNest(const Loop *L);
@@ -16455,14 +16457,15 @@ uint64_t BoUpSLP::getLoopNestScale(const Loop *L) {
   return std::max<uint64_t>(1, Scale);
 }
 
-uint64_t BoUpSLP::getGatherNodeEffectiveScale(const TreeEntry &TE) {
+uint64_t BoUpSLP::getGatherNodeEffectiveScale(const TreeEntry &TE,
+                                              Instruction *U) {
   // Only meaningful for gather/buildvector-like entries; the per-lane
   // insertelements that make up such an entry are LICM-hoistable by
   // optimizeGatherSequence() when their operand is loop-invariant.
   assert((TE.isGather() || TE.State == TreeEntry::SplitVectorize) &&
          "Expected gather/split tree entry.");
 
-  uint64_t BaseScale = getScaleToLoopIterations(TE);
+  uint64_t BaseScale = getScaleToLoopIterations(TE, nullptr, U);
   if (!PerLaneGatherScale || LoopAwareTripCount == 0 || BaseScale <= 1)
     return BaseScale;
 
@@ -16487,7 +16490,8 @@ uint64_t BoUpSLP::getGatherNodeEffectiveScale(const TreeEntry &TE) {
     if (isConstant(V))
       continue;
     ++N;
-    uint64_t LaneScale = std::min(getScaleToLoopIterations(TE, V), BaseScale);
+    uint64_t LaneScale =
+        std::min(getScaleToLoopIterations(TE, V, U), BaseScale);
     Sum = SaturatingAdd(Sum, LaneScale, &Overflow);
     if (Overflow)
       return BaseScale;
@@ -18839,8 +18843,9 @@ static T *performExtractsShuffleAction(
   return Prev;
 }
 
-InstructionCost BoUpSLP::calculateTreeCostAndTrimNonProfitable(
-    ArrayRef<Value *> VectorizedVals) {
+InstructionCost
+BoUpSLP::calculateTreeCostAndTrimNonProfitable(ArrayRef<Value *> VectorizedVals,
+                                               Instruction *RdxRoot) {
   // FIXME: support buildvector of the gather nodes with struct types.
   if (any_of(VectorizableTree, [&](const std::unique_ptr<TreeEntry> &TE) {
         return TE->isGather() &&
@@ -18960,8 +18965,10 @@ InstructionCost BoUpSLP::calculateTreeCostAndTrimNonProfitable(
       }
     }
     if (!CostIsFree && !Scale) {
-      Scale = IsGatherLike ? getGatherNodeEffectiveScale(TE)
-                           : getScaleToLoopIterations(TE);
+      Scale =
+          IsGatherLike
+              ? getGatherNodeEffectiveScale(TE, TE.Idx == 0 ? RdxRoot : nullptr)
+              : getScaleToLoopIterations(TE);
       C *= Scale;
       EntryToScale.try_emplace(&TE, Scale);
       if (!TE.isGather() && TE.hasState()) {
@@ -29837,7 +29844,8 @@ public:
 
       V.transformNodes();
       V.computeMinimumValueSizes();
-      InstructionCost TreeCost = V.calculateTreeCostAndTrimNonProfitable(VL);
+      InstructionCost TreeCost =
+          V.calculateTreeCostAndTrimNonProfitable(VL, RdxRootInst);
       V.buildExternalUses(LocalExternallyUsedValues);
 
       InstructionCost ReductionCost =

Performance

The image below captures the post-patch performance improvement. Although, if we compare this to the original baseline I displayed at the beginning of this writeup, we can see an increase in issued instructions, and a slight decrease in vector instructions.

However, the total cycles stays about the same to the baseline! 🥹