MSVC v19.51 · Regression01

The Euclidean Remainder Fallacy

How MSVC's /O1–/O2 optimizer confused C++ truncated modulo with math-class modulo — and how Parasoft C/C++test would have caught it before a single optimized binary was built.

Compiler regression

Bug
MSVC /O1–/O2 miscompiles 6U < (v % 5) when v is negative

Regression
MSVC v19.51

Reporter
Ofek Shilon

The Bug02

The Code That Breaks

#include <cstdio>
int test(int v) {
    return 6U < (v % 5) ? 1111 : 2;
}
int main() {
    int res = test(-1);
    printf("%d\n", res);
    return 0;
}
BUG /O1 · /O2

→ prints 2

Optimizer assumes v % 5 ∈ [0, 4], concludes 6U < [0,4] is always false, constant-folds to 2.

CORRECT /Od · GCC · Clang

→ prints 1111

-1 % 5 == -1 (C++ truncated modulo). -1 promotes to UINT_MAX. 6U < UINT_MAX → true.

Root Cause03

Truncated vs. Euclidean Modulo

The optimizer's range analysis assigned v % 5 the wrong range.

PropertyEuclidean Modulo (what optimizer assumed)C++ Truncated Modulo (what standard requires)
Range of v % 5[0, 4] (wrong)[-4, 4] (right)
-1 % 54 (wrong)-1 (right)
Sign of remainderAlways non-negative (wrong)Follows dividend (right)
Standard referenceMath class (wrong)[expr.mul]/4 (right)
Effect on 6U < (v % 5)“Always false” → fold to 2Must evaluate at runtime
The trap:

When the signed remainder -1 meets the unsigned literal 6U, the usual arithmetic conversions promote it to unsigned int → UINT_MAX (4,294,967,295). The comparison 6U < UINT_MAX is trivially true. But the optimizer never gets there — it already folded the expression using the wrong range.

The Story04
“MSVC's optimizer discovered Euclidean modulo and decided the C++ standard was simply wrong. -1 % 5 is -1 in C++, not 4 — and once that -1 meets 6U, it becomes UINT_MAX, making the comparison trivially true. The optimizer ‘helpfully’ replaced truncated modulo with math-class modulo, concluded 6U < [0,4] is always false, and folded to 2. A textbook example of why ‘obviously always false’ is the most dangerous phrase in compiler optimization.”
The Fix05

Workarounds — Today

Option 1 — Most Surgical

#pragma optimize

#pragma optimize("", off)
int test(int v) {
  return 6U < (v % 5) ? 1111 : 2;
}
#pragma optimize("", on)

Kills the bug without changing semantics or affecting the rest of the translation unit.

Option 2 — Explicit Conversion

Break the fold pattern

int test(int v) {
  int rem = v % 5;
  unsigned urem = (unsigned)rem;
  return 6U < urem ? 1111 : 2;
}

Makes the signed→unsigned conversion visible so the optimizer can't skip it.

Option 3 — Change Semantics

Unsigned domain (if intended)

int test(int v) {
  unsigned uv = (unsigned)v;
  return 6U < (uv % 5u) ? 1111 : 2;
}

⚠️ This changes the result: UINT_MAX % 5 == 0, not -1. Only use if you genuinely want unsigned modular arithmetic.

Option 4 — Brute Force

Compile affected TU with /Od

Guaranteed correct, but disables all optimizations for the file. Use only if the bug is widespread in a single translation unit.

Enter Parasoft C/C++test06

Would Parasoft Have Caught This?

Yes — through two independent mechanisms that form a closed loop.

① Static Analysis

Flags the signed/unsigned comparison pattern before the code ever reaches the optimizer. The exact coding error the optimizer made is literally what CERT-C and MISRA-C rules exist to prevent.

② Unit Testing

A C++test unit test with a negative-input boundary case passes under /Od and fails under /O2 — catching the miscompilation at CI build time, not in production.

Static analysis tells you where to write tests. Unit tests tell you whether the code — or the compiler — is actually correct.
Static Analysis · CERT-C07

CERT-C INT10-C: The Exact Rule

CERT-C · INT10-C

Do not assume a non-negative remainder

This rule exists precisely to catch the assumption that v % d ∈ [0, d-1]. This is literally the exact error the MSVC optimizer made. The static analysis tool designed to catch programmer errors would have caught the compiler's error — because the optimizer fell for the same fallacy the rule exists to prevent.

CERT-C INT02-C

Integer Conversion Rules

Flags implicit signed→unsigned conversions — the -1 → UINT_MAX promotion the optimizer missed.

CWE-681

Incorrect Conversion Between Numeric Types

The broader CWE category covering the signed→unsigned implicit promotion chain.

If Parasoft C/C++test had been run on this code before the /O1 build: INT10-C would have flagged 6U < (v % 5), a developer would have reviewed the finding and either made the conversion explicit, cast v to unsigned first, or added a runtime guard — preventing the vulnerable code from ever reaching the miscompiling optimizer.

Static Analysis · MISRA-C 201208

MISRA-C 2012: Three Rules Fire

RuleCategoryWhat It FlagsRelevance
Rule 10.4RequiredBoth operands of an operator should not be of different essential type categories6U (unsigned) vs v % 5 (signed) — mixed-type comparison
Rule 10.6RequiredValue of unsigned expression should not be implicitly converted to greater widthThe promotion chain that turns -1 into UINT_MAX
Rule 12.4AdvisoryComparison should not be made against a constant outside the operand's range6U constant vs v % 5 signed range

Three MISRA-C findings on a single expression. In any MISRA-compliant project, this code would never have reached code review — let alone the optimizer. The signed/unsigned mismatch is flagged at the essential-type level, before any optimization pass runs.

Parasoft · Unit Testing09

The Unit Test That Catches the Compiler

TEST(ModuloSuite, NegativeInput) {
  // -1 % 5 == -1 (C++ truncated)
  // -1 → UINT_MAX on unsigned promotion
  // 6U < UINT_MAX → true → 1111
  int result = test(-1);
  ASSERT_EQUAL(1111, result);
}
PASSES under /Od

Correct codegen: -1 % 5 = -1 → UINT_MAX → 6U < UINT_MAX = true → 1111

FAILS under /O2

Miscompiled: optimizer folds to 2 instead of 1111

The test doesn't need to know why the optimizer is wrong. It just needs to test the boundary input that static analysis flagged.

Why negative input?

  • Positive v: v % 5 ∈ [0,4] — optimizer is correct, test passes
  • Negative v: v % 5 ∈ [-4,-1] — optimizer's range is wrong, test catches it
  • Static analysis told us to test this
The CI/CD Pipeline10

Defense in Depth

1

Parasoft C/C++test Static Analysis

CERT-C INT10-C + MISRA-C 10.4 fire on 6U < (v % 5). Developer reviews finding, makes conversion explicit or adds guard.

2

Build with /O2

The miscompiling optimizer runs, but the code has already been hardened by step 1.

3

Parasoft C/C++test Unit Tests

test(-1) expects 1111. If the optimizer still miscompiles, the test FAILS and the build is blocked.

4

Runtime Error Detection

(Optional) Instrumented run catches unexpected unsigned wraparound at the comparison site.

✓ Merge blocked if any step fails. The miscompilation never reaches production.
The Irony11

The Tool Caught the Compiler

The static analysis tool designed to catch programmer errors would have caught the compiler's error

because the optimizer made the exact same mistake that CERT-C INT10-C exists to prevent.

The Optimizer's Mistake

Assumed v % 5 ∈ [0, 4] — Euclidean range. Ignored negative remainders. Folded the comparison.

What INT10-C Prevents

Assuming v % 5 ∈ [0, 4] — Euclidean range. Ignoring negative remainders. The exact same fallacy.

Takeaways12

Key Takeaways

The Bug

  • MSVC /O1–/O2 assumes v % d ∈ [0, d-1] (Euclidean)
  • C++ requires v % d ∈ [-(d-1), d-1] (truncated)
  • Negative remainder + unsigned comparison = silent miscompilation
  • Regression since MSVC v19.51

The Fix

  • #pragma optimize("", off) — most surgical
  • Explicit (unsigned) cast — breaks the fold
  • Unsigned domain — if Euclidean is intended
  • Compiler team: fix % range inference for signed dividends

Parasoft: Static Analysis

  • CERT-C INT10-C — “non-negative remainder assumption”
  • MISRA-C 10.4 — signed/unsigned essential type mismatch
  • MISRA-C 10.6, 12.4 — conversion + constant range
  • Flags the pattern before it reaches the optimizer

Parasoft: Unit Testing

  • Boundary test: test(-1) expects 1111
  • Passes under /Od, fails under /O2
  • Static analysis tells you what to test
  • Unit tests tell you if the compiler is correct
Static analysis + unit testing = closed loop. The tool catches what the compiler misses. The test proves it.
Credits & Tools13

Powered By

Parasoft

Automated Software Testing — Static Analysis, Unit Testing & Compliance

in/Parasoft ↗
ESL — Engineering Software Lab

The Israeli Center for Static Code Analysis — Parasoft Distribution & Integration

in/Engineering-Software-Lab-ESL ↗

Analysis & Presentation by

Daniel (Dani) Liezrowice in/liezrowice ↗

CEO & Co-Founder, ESL — Engineering Software Lab

zuwasi.github.io/Public-html-pages/Euclidean_Remainder_Fallacy_Presentation.html
← Swipe to navigate →
1 / 13