Originally published on LinkedIn Pulse. Read the original article.

When Correct C++ Produces the Wrong Answer: How Parasoft C/C++test Can Catch an MSVC Compiler Bug Before Release

Your C++ Code Is Correct—but the Compiler Is Wrong

Compiler optimizations are supposed to change performance, not program meaning. But a newly reported Microsoft Visual C++ regression demonstrates a rare and important exception: valid C++ produces the correct result without optimization and the wrong result when compiled with /O1.

The case is valuable beyond one compiler defect. It shows how static analysis, unit testing, code coverage, compiler-diverse testing, and CI quality gates work together—and why no single technique is enough.

Status on August 7, 2026: Microsoft lists the report as Under Consideration. The public report does not yet identify a fixed compiler release.

Microsoft Developer Community report: https://developercommunity.visualstudio.com/t/MSVC-O1-miscompiles-nested-array-loop/11126250

Live Compiler Explorer reproducer: https://compiler-explorer.com/z/fWc3nz1E5

The symptom: /Od is correct, /O1 is not

The minimized example initializes three 10×10 arrays to 1, then runs this assignment inside nested loops:

arr_2[i][j] = arr_0[j + 1][i] + (arr_1[1][i] < var_1);

For the supplied inputs, the outer loop executes only once with i == 0, while j takes the values 0, 1, and 2. Every loaded value is 1, and the comparison 1 < 2 is true. The expected first five results are therefore:

2 2 2 1 1

That is exactly what MSVC 19.51 produces with /Od. With /O1, however, the result is:

81 161 241 1 1

Why 81, 161, 241 reveal the likely compiler failure

The incorrect numbers initially look like data corruption. They are much more structured than that.

Each row of arr_0 contains ten long long values. On this target, that means an 80-byte row:

10 elements × 8 bytes = 80 bytes

For i == 0, the three requested values reside at these offsets from the start of arr_0:

ExpressionByte offsetWrong output
arr_0[1][0]8081
arr_0[2][0]160161
arr_0[3][0]240241

The pattern is exact:

wrong result = byte offset + 1

The additional 1 is the true comparison result. This strongly supports the reporter's assembly-level observation: during optimization, MSVC appears to feed an address offset into an adc operation instead of feeding it the value loaded from that address.

In conceptual terms, behavior resembling this:

load value, [arr_0 + offset]
adc result, value

has become behavior resembling this:

adc result, offset

This is a classic value-versus-address failure in optimized code generation.

For the reported inputs, the source does not rely on uninitialized memory, out-of-range indexing, signed overflow, aliasing tricks, or another obvious form of undefined behavior. The fact that the issue is reportedly a regression from MSVC 19.50 reinforces the compiler-defect diagnosis.

Where Parasoft C/C++test provides protection—and where it does not

It is tempting to say that static analysis "catches compiler bugs." That would overstate what static analysis can do.

The source expression is valid. The defect is introduced later, while MSVC generates optimized machine code. A source-level static analyzer will not ordinarily predict that one version of one optimizer will substitute an address offset for an array value.

Parasoft C/C++test protects the release through layered verification:

  1. Static analysis helps rule out source defects and undefined behavior that often imitate compiler bugs.
  2. Unit tests with explicit expected results expose the wrong optimized behavior.
  3. Structural coverage demonstrates that the affected statements and decisions were exercised.
  4. Host and target execution checks the code in representative environments.
  5. CI reporting and quality gates prevent a compiler upgrade from silently changing production behavior.

Parasoft describes C/C++test as combining static analysis, unit testing, structural code coverage, requirements traceability, runtime analysis, and CI/CD integration: https://www.parasoft.com/products/parasoft-c-ctest/

Official Parasoft code-coverage overview: https://www.parasoft.com/solutions/code-coverage/


The regression test that blocks the bad binary

A focused test should initialize the arrays, call the real production function, and assert the exact result:

TEST(MsvcRegression, NestedArrayLoopProducesValuesNotOffsets)
{
    long long arr0[10][10];
    unsigned int arr1[10][10];
    int arr2[10][10];

    for (std::size_t i = 0; i < 10; ++i) {
        for (std::size_t j = 0; j < 10; ++j) {
            arr0[i][j] = 1;
            arr1[i][j] = 1;
            arr2[i][j] = 1;
        }
    }

    test(656567182542236LL, 2, 1, arr0, arr1, arr2);

    EXPECT_EQ(arr2[0][0], 2);
    EXPECT_EQ(arr2[0][1], 2);
    EXPECT_EQ(arr2[0][2], 2);
    EXPECT_EQ(arr2[0][3], 1);
    EXPECT_EQ(arr2[0][4], 1);
}

C/C++test CT integrates with frameworks such as GoogleTest, making this type of test suitable for automated CI execution and coverage collection.

The important detail is that the production function must be compiled with the actual release compiler and flags. A test that exercises only an /Od test build cannot detect an /O1 code-generation defect.

I would run the same test in this matrix:

BuildPurpose
MSVC 19.50 /O1Known-good baseline reported by the reproducer
MSVC 19.51 /OdUnoptimized behavioral comparison
MSVC 19.51 /O1Reproduce and block the defect
MSVC 19.51 /O2Exercise a different optimization strategy
Clang-cl or another compilerIndependent compiler comparison
Uninstrumented production binaryEnsure coverage instrumentation did not mask the bug

That last row matters. Instrumentation changes generated code and can perturb the optimizer enough to hide a defect. Coverage should therefore complement—not replace—execution of the uninstrumented release binary.

Perspective 1: What should a compiler user do now?

1. Pin or roll back the compiler

The safest immediate response is to remain on MSVC 19.50—or another validated release—until Microsoft identifies a fixed version. Record the exact compiler version in the build and make toolchain upgrades pass the same CI gates as source changes.

2. Disable optimization for the affected function or translation unit

If upgrading cannot be avoided, compile the affected source file with /Od, or isolate the function with MSVC's optimization pragma:

#if defined(_MSC_VER)
#pragma optimize("", off)
#endif

void test(/* parameters */)
{
    // Existing valid implementation
}

#if defined(_MSC_VER)
#pragma optimize("", on)
#endif

Microsoft optimization-option documentation: https://learn.microsoft.com/en-us/cpp/build/reference/o1-o2-minimize-size-maximize-speed?view=msvc-170

3. Do not assume that /O2 is automatically safe

The public reproducer succeeds under /O2, but one passing example does not prove that the complete product is unaffected. Changing optimization strategy should be followed by full regression, performance, concurrency, and target testing.

4. Keep the reproducer permanently

Once a compiler has generated incorrect executable behavior, the test should become part of the project's permanent toolchain qualification suite. Do not remove it merely because a later compiler release passes.


Perspective 2: What is the permanent compiler fix?

For the compiler team, changing user source is not the solution. The permanent repair belongs in the optimizer or backend.

The engineering path should be:

  1. Minimize the program while preserving the /O1 failure.
  2. Compare optimizer-pass output between MSVC 19.50 and 19.51.
  3. Locate the first intermediate form where the required load disappears or becomes an address calculation.
  4. Inspect loop-strength reduction, address-mode formation, instruction selection, and any peephole that combines a comparison and addition into adc.
  5. Preserve the semantic distinction among an effective address, a byte displacement, an induction variable, and the value loaded from memory.
  6. Add both execution and assembly-level regression tests.

The compiler test matrix should vary array dimensions, element sizes, comparison outcomes, loop increments, LTCG settings, and target architectures. It should also include randomized differential testing against an interpreter, an unoptimized build, or an independent compiler.

The key invariant is simple: calculating a memory address does not make the value at that address available. A load may be removed only when its value is proven unnecessary—not because its offset has already been calculated.


The broader lesson: verify the toolchain, not only the source

Compiler failures are uncommon, but their impact is disproportionate: the source can pass review while the shipped binary violates the source semantics.

The practical defense is not distrust of optimization. It is an evidence-based toolchain process:

Static analysis
+ explicit test oracles
+ release-flag execution
+ structural coverage
+ compiler diversity
+ target testing
+ CI quality gates

Parasoft C/C++test does not repair an MSVC optimizer defect. It provides the verification framework that can expose the wrong binary, preserve the regression, document the evidence, and stop that binary from reaching production.

That distinction is important—and it is exactly why layered software verification works.


References

  1. Microsoft Developer Community, "MSVC /O1 miscompiles nested array loop, regression from 19.50" — https://developercommunity.visualstudio.com/t/MSVC-O1-miscompiles-nested-array-loop/11126250
  2. Compiler Explorer reproducer — https://compiler-explorer.com/z/fWc3nz1E5
  3. Parasoft C/C++test product overview — https://www.parasoft.com/products/parasoft-c-ctest/
  4. Parasoft code-coverage overview and official screenshots — https://www.parasoft.com/solutions/code-coverage/
  5. Microsoft /O1 and /O2 documentation — https://learn.microsoft.com/en-us/cpp/build/reference/o1-o2-minimize-size-maximize-speed?view=msvc-170

Hashtags: #Cpp #CPlusPlus #MSVC #CompilerEngineering #StaticAnalysis #SoftwareTesting #Parasoft #CodeCoverage #FunctionalSafety #DevOps #QualityEngineering