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)
- 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.
- 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).
- 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).
- 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).
Summary
When compiling for Windows
arm64with/O2(or/O1) in thecmake-windows-arm64GitHub Actions workflow (windows-11-arm), MSVC (cl.exe, reproduced througharm64 msvc v19.latest/ VS 2026 18.6 v19.51) miscompilesvaddq_u32(vreinterpretq_u32_f32(vmulq_f32(a, b)), c)(as well asvaddq_s32andvsubq_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).cmake-windows-arm64in ci: add cmake-windows-arm64-native job using windows-11-arm runner #10528, in case you can help route this to the MSVC ARM64 backend compiler team at Microsoft)Minimal 5-Line C Reproducer
Side-by-Side Assembly Output
mla v1.4s, v0.4s, v0.4smov v0.16b, v1.16bretfmulcompletely and emits integerMLA(vbias + (uint32)vx * (uint32)vx). Invsqr_bf16_neon_broken, emitsmla v16.4s, v18.4s, v18.4son theshll #16inputv18, which always produces0.fmul v0.4s, v0.4s, v0.4sadd v0.4s, v0.4s, v1.4sretfloat32x4_tmultiply (fmul), followed byuint32x4_tinteger add (add).Impact in XNNPACK (
xnn_bf16_vsqr_ukernel__neon_u8)src/bf16-vunary/gen/bf16-vunary-neon.c(xnn_bf16_vsqr_ukernel__neon_u8),vxis loaded frombf16viavshlq_n_u32(vmovl_u16(vld1_u16(input)), 16), so the lower 16 bits of every 32-bit lane invxare0x0000.convert_f32_to_bf16(vmulq_f32(vx, vx))is inlined, MSVC fusesvaddq_u32(vi, vbias)withvmulq_f32(vx, vx)intomla v16.4s, v18.4s, v18.4son the originalvx(v18.4s).v18.4sare zero, the 32-bit integer product(x << 16) * (x << 16) = x^2 << 32is always 0 (mod 2^32). Thusvrounded >> 16evaluates to0for all non-NaN inputs (causingbf16-vsqr-testto fail onwindows-11-arm).vshrn_n_u32(vaddq_u32(vi, ...), 16)withvaddhn_u32(vi, vaddq_u32(vlsb, vbias))(ADDHN) avoids the miscompilation because ARM64 NEON has no integer multiply-accumulate-narrow instruction (note that merely reorderingvaddq_u32(vi, vaddq_u32(vbias, vlsb))still miscompiles tomla).