Inline asm: missing `vzeroupper` insertion for value-less `out("ymmN") _` / `out("zmmN") _` clobbers

Open
#165,332 1 comment 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
28/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
c, rust
Domain
compilers

Research direction

Start from the repro: compile it with rustc -O --crate-type=lib --emit=asm,llvm-ir repro.rs and compare clobber with typed. The difference is in how the explicit out("ymm0") _ operand is lowered to an LLVM constraint, so the register-to-constraint mapping for value-less outputs is the place to read. Done when the clobber emits {ymm0} or equivalent and LLVM inserts vzeroupper before ext(), matching the typed and clang cases.

Written by the indexing model from the issue text.

Description

A-inline-assembly A-LLVM C-bug T-compiler WG-llvm

I wrote this code:

use std::arch::asm;
use std::arch::x86_64::__m256i;

extern "C" {
    fn ext();
}

// Clobber declared as `out("ymm0") _`: no vzeroupper before the call.
#[no_mangle]
#[target_feature(enable = "avx")]
pub unsafe fn clobber() {
    asm!("vpcmpeqd ymm0, ymm0, ymm0", out("ymm0") _);
    ext();
}

// Same, but the output is bound to a 256-bit value: vzeroupper is inserted.
#[no_mangle]
#[target_feature(enable = "avx")]
pub unsafe fn typed() {
    let _x: __m256i;
    asm!("vpcmpeqd ymm0, ymm0, ymm0", out("ymm0") _x);
    ext();
}

After running:

rustc -O --crate-type=lib --emit=asm,llvm-ir repro.rs

I expected the LLVM IR to be the same, but it is not:

define void @clobber()
  asm sideeffect alignstack inteldialect "vpcmpeqd ymm0, ymm0, ymm0", "=&{xmm0},~{dirflag},~{fpsr},~{flags},~{memory}"()
define void @typed()
  asm sideeffect alignstack inteldialect "vpcmpeqd ymm0, ymm0, ymm0", "=&{ymm0},~{dirflag},~{fpsr},~{flags},~{memory}"()

I expected the generated assembly to be the same, but it is not:

clobber:
	pushq	%rax
	vpcmpeqd	%ymm0, %ymm0, %ymm0
	popq	%rax
	jmpq	*ext@GOTPCREL(%rip)

typed:
	pushq	%rax
	vpcmpeqd	%ymm0, %ymm0, %ymm0
	popq	%rax
	vzeroupper
	jmpq	*ext@GOTPCREL(%rip)
Explanation

On x86_64, an asm! output operand naming a ymm or zmm register with _ as its place (ie. a clobber) is passed to LLVM as an {xmmN} constraint. LLVM then does not know the upper bits of the register were written. Its vzeroupper insertion pass (VZeroUpperInserter) does not treat the block as dirtying the upper state, and emits no vzeroupper before following calls or returns.

If the same operand is bound to a 256-bit value, rustc emits {ymmN} and LLVM inserts vzeroupper as expected. Clang passes "ymm0" clobbers through as ~{ymm0}, and also gets a vzeroupper.

I tried this with:

  • x86_64-unknown-linux-gnu
  • rustc 1.99.1 (LLVM 23.1.1) and 1.101.0-nightly (92b5b8c87 2026-10-29)
  • also every release back to 1.60
Clang, for comparison
void ext(void);
__attribute__((target("avx"))) void clobber(void) {
    __asm__ volatile("vpcmpeqd %%ymm0, %%ymm0, %%ymm0" ::: "ymm0");
    ext();
}

This gives "~{ymm0},~{dirflag},~{fpsr},~{flags}", and the code is:

clobber:
	vpcmpeqd	%ymm0, %ymm0, %ymm0
	vzeroupper
	jmp	ext@PLT
Other forms with the same behaviour
  • out("zmm0") _ with avx512f is lowered to =&{xmm0}; no vzeroupper.
  • out(ymm_reg) _ (a register class, not an explicit register) is lowered to =&x; no vzeroupper.
Expected

When the user names ymmN/zmmN (or uses ymm_reg/zmm_reg), a value-less operand should reach LLVM with that register's full width, such as ~{ymmN} or =&{ymmN}.

In other words: a manually-written clobber for a specific register should make its way to LLVM as-is, allowing LLVM VZeroUpperInserter to do its thing (as with clang).

Impact

This is a minor performance impact on older Intel CPUs who had a AVX transition penalty, and later ones it causes false dependencies. I don't believe AMD CPUs have this problem. It is not a correctness problem, as the ISA ensures that uses of a narrow register class is unaffected if the next class up has set bits.

Dominant language
Rust
Stars
121k
Forks
17.3k
Avg merge
2d 21h
Merged PRs (30d)
531

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from rust-lang/rust

All issues in rust-lang/rust

Similar issues

More Rust issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.