mirror of
https://github.com/pezkuwichain/revive.git
synced 2026-10-20 18:56:17 +00:00
Fix Windows bytecode divergence during LLVM LSR (#570)
## Description Our differential bytecode comparison tests detected a bytecode mismatch between the Linux/macOS builds and the Windows build for a [contract](https://github.com/paritytech/resolc-compiler-tests/blob/4647018d64eab6ccff30d22c753f02b90e9fc7e3/fixtures/solidity/simple/internal_function_pointers/sort/bubble.sol) implementing recursive bubble sort, specifically with these opt settings: * Solc optimizer disabled * Newyork enabled * `-Oz` This PR applies an [LLVM patch](https://github.com/llvm/llvm-project/pull/190587) which fixes the divergence for contracts with loops that index array elements at constant offsets from a Solidity `uint8` loop variable, observed with the above opt settings. ## Cause The first divergence occurs in an LLVM Loop Strength Reduction (LSR) decision, on `bubbleSort`'s loop (`for (uint8 i = 0; i < ARRAY_SIZE - start - 1; i++)` with `array[i]`/`array[i+1]` accesses): ```diff - %lsr.iv = phi i32 [ 0, %for_cond.preheader153 ], [ %lsr.iv.next, %if_join64 ] ; Linux/macOS + %lsr.iv = phi i32 [ 1, %for_cond.preheader153 ], [ %lsr.iv.next, %if_join64 ] ; Windows + %3 = add i32 %lsr.iv, -1 ; Windows ... - %3 = add i32 %lsr.iv, 1 ; Linux/macOS ... %lsr.iv.next = add nuw nsw i32 %lsr.iv, 1 ; identical on both platforms: step is +1 either way ``` In LLVM's `{Start,+,Step}` recurrence notation (value starts at `Start`, changes by `Step` per loop iteration), the three constants in the diff represent: recurrence *start*, use-site *offset*, and recurrence *step*. Only the first two differ between the platforms: **Linux/macOS** (the `array[i]` calls take `%lsr.iv` directly; the `array[i+1]` calls take `%3`): | Line | Constant | Role | |---|---|---| | `%lsr.iv = phi i32 [ 0, … ]` | `0` | **Start**: the register's value on loop entry (`i`) (holds 0, 1, 2, …) | | `%3 = add i32 %lsr.iv, 1` | `+1` | **Use-site offset**: derives the *other* index (`i + 1`) from the register each iteration. (No register is dedicated to `%3`) | | `%lsr.iv.next = add … , 1` | `+1` | **Step**: how much the register advances each iteration | **Windows** (mirrored: the `array[i+1]` calls take `%lsr.iv` directly; the `array[i]` calls take `%3`): | Line | Constant | Role | |---|---|---| | `%lsr.iv = phi i32 [ 1, … ]` | `1` | **Start**: the register's value on loop entry (`i + 1`) (holds 1, 2, 3, …) | | `%3 = add i32 %lsr.iv, -1` | `-1` | **Use-site offset**: derives the *other* index (`i`) from the register each iteration. (No register is dedicated to `%3`) | | `%lsr.iv.next = add … , 1` | `+1` | **Step**: identical | The two solutions are mirror images with exactly equal cost, because `getSetupCost()` (the patched function) rated *any* constant induction variable (IV) start equally (a 0-start and a 1-start IV cost the same). LSR keeps the first solution it enumerates, and the enumeration order derives from walking SSA use-lists, whose in-memory order differed between our Linux/macOS and Windows builds of LLVM. Thus both platforms compute the indices `i` and `i + 1` each iteration (functionally identical), but the LSR decision of which of the two recurrences becomes the loop-carried value (the phi) differs, which affects register allocation. > [!NOTE] > The equal cost tie can only arise if LSR processes the loop at all, and LSR only forms uses for induction variables of a 32-bit or 64-bit integer in our case. ## Partial solution The [upstream fix](https://github.com/llvm/llvm-project/pull/190587) updates `getSetupCost()` to return `0` for constant IV starts whose materialization is `TCC_Free` (which immediate `0` on RISC-V is), which is now cheaper than non-zero starts, resulting in the same LSR decision (no tie to decide on) on Windows as well. As zero-start IVs are most common, this fix addresses the most common scenarios, but it is still only a partial fix as it specifically solves only the zero-start case. (I'm currently writing up another issue regarding this.) ## Verification I created a draft LLVM release that was built with the patch applied, then reran all workflows using the draft release, including temporarily testing more opt settings for the bytecode comparisons which ran successfully (no mismatches) with the following `retester` modes (see [this run](https://github.com/paritytech/revive/actions/runs/28970814016/job/85982029937)): * `NY M0 S+` * `NY M0 S-` * `NY M3 S+` * `NY M3 S-` * `NY Mz S+` * `NY Mz S-` * `Y M0 S+` * `Y M0 S-` * `Y M3 S+` * `Y M3 S-` * `Y Mz S+` * `Y Mz S-` All E2E tests also ran as it does today (allowing all Yul and newyork IR `retester` modes), completing successfully.
This commit is contained in:
4 files changed
+82
-4
No files matched your search
@@ -0,0 +1,52 @@
|
||||
diff --git a/llvm/lib/Transforms/Scalar/LoopStrengthReduce.cpp b/llvm/lib/Transforms/Scalar/LoopStrengthReduce.cpp
|
||||
index a4c1e08430..2876353ee5 100644
|
||||
--- a/llvm/lib/Transforms/Scalar/LoopStrengthReduce.cpp
|
||||
+++ b/llvm/lib/Transforms/Scalar/LoopStrengthReduce.cpp
|
||||
@@ -1373,23 +1373,31 @@ static bool isAMCompletelyFolded(const TargetTransformInfo &TTI,
|
||||
bool HasBaseReg, int64_t Scale,
|
||||
Instruction *Fixup = nullptr);
|
||||
|
||||
-static unsigned getSetupCost(const SCEV *Reg, unsigned Depth) {
|
||||
- if (isa<SCEVUnknown>(Reg) || isa<SCEVConstant>(Reg))
|
||||
+static unsigned getSetupCost(const SCEV *Reg, unsigned Depth,
|
||||
+ const TargetTransformInfo &TTI) {
|
||||
+ if (isa<SCEVUnknown>(Reg))
|
||||
return 1;
|
||||
+ if (const auto *C = dyn_cast<SCEVConstant>(Reg)) {
|
||||
+ if (TTI.getIntImmCost(C->getAPInt(), C->getType(),
|
||||
+ TargetTransformInfo::TCK_RecipThroughput) ==
|
||||
+ TargetTransformInfo::TCC_Free)
|
||||
+ return 0;
|
||||
+ return 1;
|
||||
+ }
|
||||
if (Depth == 0)
|
||||
return 0;
|
||||
if (const auto *S = dyn_cast<SCEVAddRecExpr>(Reg))
|
||||
- return getSetupCost(S->getStart(), Depth - 1);
|
||||
+ return getSetupCost(S->getStart(), Depth - 1, TTI);
|
||||
if (auto S = dyn_cast<SCEVIntegralCastExpr>(Reg))
|
||||
- return getSetupCost(S->getOperand(), Depth - 1);
|
||||
+ return getSetupCost(S->getOperand(), Depth - 1, TTI);
|
||||
if (auto S = dyn_cast<SCEVNAryExpr>(Reg))
|
||||
return std::accumulate(S->operands().begin(), S->operands().end(), 0,
|
||||
[&](unsigned i, const SCEV *Reg) {
|
||||
- return i + getSetupCost(Reg, Depth - 1);
|
||||
+ return i + getSetupCost(Reg, Depth - 1, TTI);
|
||||
});
|
||||
if (auto S = dyn_cast<SCEVUDivExpr>(Reg))
|
||||
- return getSetupCost(S->getLHS(), Depth - 1) +
|
||||
- getSetupCost(S->getRHS(), Depth - 1);
|
||||
+ return getSetupCost(S->getLHS(), Depth - 1, TTI) +
|
||||
+ getSetupCost(S->getRHS(), Depth - 1, TTI);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1459,7 +1467,7 @@ void Cost::RateRegister(const Formula &F, const SCEV *Reg,
|
||||
|
||||
// Rough heuristic; favor registers which don't require extra setup
|
||||
// instructions in the preheader.
|
||||
- C.SetupCost += getSetupCost(Reg, SetupCostDepthLimit);
|
||||
+ C.SetupCost += getSetupCost(Reg, SetupCostDepthLimit, *TTI);
|
||||
// Ensure we don't, even with the recusion limit, produce invalid costs.
|
||||
C.SetupCost = std::min<unsigned>(C.SetupCost, 1 << 16);
|
||||
|
||||
Reference in new issue
Block a user