Skip to content

MSVC ARM64 /O2 miscompiles vaddq_u32(vreinterpretq_u32_f32(vmulq_f32(a, b)), c) into integer MLA #11368

Description

@fbarchard

Summary

When compiling for Windows arm64 with /O2 (or /O1) in the cmake-windows-arm64 GitHub Actions workflow (windows-11-arm), MSVC (cl.exe, reproduced through arm64 msvc v19.latest / VS 2026 18.6 v19.51) miscompiles vaddq_u32(vreinterpretq_u32_f32(vmulq_f32(a, b)), c) (as well as vaddq_s32 and vsubq_u32).

MSVC's ARM64 backend instruction combiner ignores the float-to-integer reinterpret (vreinterpretq_u32_f32) and illegally fuses the IEEE-754 floating-point multiply (vmulq_f32, FMUL) with the 32-bit integer add/subtract (ADD/SUB) into a 32-bit integer multiply-accumulate/subtract instruction (MLA/MLS).


Minimal 5-Line C Reproducer

#include <arm_neon.h>
#include <stdint.h>

uint32x4_t msvc_arm64_fmul_add_bug_minimal(float32x4_t vx, uint32x4_t vbias) {
  float32x4_t vy = vmulq_f32(vx, vx);
  uint32x4_t vi = vreinterpretq_u32_f32(vy);
  return vaddq_u32(vi, vbias);
}

Side-by-Side Assembly Output

Compiler Generated ARM64 Assembly Behavior
Broken: arm64 msvc v19.latest (/O2) mla v1.4s, v0.4s, v0.4s
mov v0.16b, v1.16b
ret
Miscompiled: Drops fmul completely and emits integer MLA (vbias + (uint32)vx * (uint32)vx). In vsqr_bf16_neon_broken, emits mla v16.4s, v18.4s, v18.4s on the shll #16 input v18, which always produces 0.
Good: armv8-a clang 23.1.0 (-O2) fmul v0.4s, v0.4s, v0.4s
add v0.4s, v0.4s, v1.4s
ret
Correct: Performs IEEE-754 float32x4_t multiply (fmul), followed by uint32x4_t integer add (add).
 Compiler                            | Generated ARM64 Assembly                          | Behavior
-------------------------------------+---------------------------------------------------+------------------------------------------------------------------------
 Broken: arm64 msvc v19.latest (/O2) | mla v1.4s, v0.4s, v0.4s                           | Miscompiled: Drops fmul completely and emits integer MLA (vbias +
                                     | mov v0.16b, v1.16b                                | (uint32)vx * (uint32)vx). In vsqr_bf16_neon_broken, emits mla v16.4s,
                                     | ret                                               | v18.4s, v18.4s on the shll #16 input v18, which always produces 0.
 Good: armv8-a clang 23.1.0 (-O2)    | fmul v0.4s, v0.4s, v0.4s                          | Correct: Performs IEEE-754 float32x4_t multiply (fmul), followed by
                                     | add v0.4s, v0.4s, v1.4s                           | uint32x4_t integer add (add).
                                     | ret                                               |

Impact in XNNPACK (xnn_bf16_vsqr_ukernel__neon_u8)

  1. In src/bf16-vunary/gen/bf16-vunary-neon.c (xnn_bf16_vsqr_ukernel__neon_u8), vx is loaded from bf16 via vshlq_n_u32(vmovl_u16(vld1_u16(input)), 16), so the lower 16 bits of every 32-bit lane in vx are 0x0000.
  2. When convert_f32_to_bf16(vmulq_f32(vx, vx)) is inlined, MSVC fuses vaddq_u32(vi, vbias) with vmulq_f32(vx, vx) into mla v16.4s, v18.4s, v18.4s on the original vx (v18.4s).
  3. Because the low 16 bits of v18.4s are zero, the 32-bit integer product (x << 16) * (x << 16) = x^2 << 32 is always 0 (mod 2^32). Thus vrounded >> 16 evaluates to 0 for all non-NaN inputs (causing bf16-vsqr-test to fail on windows-11-arm).
  4. Workaround in XNNPACK: Replacing vshrn_n_u32(vaddq_u32(vi, ...), 16) with vaddhn_u32(vi, vaddq_u32(vlsb, vbias)) (ADDHN) avoids the miscompilation because ARM64 NEON has no integer multiply-accumulate-narrow instruction (note that merely reordering vaddq_u32(vi, vaddq_u32(vbias, vlsb)) still miscompiles to mla).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions