Skip to content

Misoptimization around asm mul+div #99830

Open

Description

While trying some inline assembly in my solutions to an online math puzzle site (without spoiling which one), I ran into a case that gets the right answer in debug mode or opt-level = 1, but fails at opt-level = 2 or higher. I've extracted as much as I could to make this stand alone.

[package]
name = "mul_bug"
version = "0.1.0"
edition = "2021"

[dependencies]
primal-sieve = "0.3.2"

[profile.release]
opt-level = 2 # 1 is OK
use primal_sieve::Primes;
use std::ops::{Add, MulAssign};

fn main() {
    assert_eq!(solve(4), 650);
    assert_eq!(solve(100), 202_476_099);
    assert_eq!(solve(100_000), 403_221_585);
}

fn solve(n: u32) -> u32 {
    let mut product = Mod(1);
    // Changing to a regular `for` loop suppresses the bug.
    // for p in Primes::all().take_while(|&p| p < n as usize) {
    Primes::all().take_while(|&p| p < n as usize).for_each(|p| {
        let p = p as u32;

        let mut k = n / p;
        let mut pp = p;
        while let Some(x) = pp.checked_mul(p) {
            pp = x;
            k += n / pp;
        }

        product *= Mod(p).pow(2 * k) + Mod(1);
    });
    // }
    product.0
}

#[derive(Clone, Copy)]
struct Mod(u32);

impl Mod {
    const MOD: u32 = 10u32.pow(9) + 9;
}

impl Add for Mod {
    type Output = Mod;

    fn add(mut self, rhs: Mod) -> Mod {
        if rhs.0 != 0 {
            let neg_rhs = Self::MOD - rhs.0;
            if self.0 < neg_rhs {
                self.0 += rhs.0;
            } else {
                self.0 -= neg_rhs;
            }
        }
        self
    }
}

impl MulAssign for Mod {
    fn mul_assign(&mut self, rhs: Mod) {
        // Adding this up-casting implementation and the `assert_eq` supresses the bug.
        // let x = (u64::from(self.0) * u64::from(rhs.0) % u64::from(Self::MOD)) as u32;
        unsafe {
            core::arch::asm!(
                "mul edx",
                "div {:e}",
                in(reg) Self::MOD,
                inout("eax") rhs.0 => _,
                inout("edx") self.0,
                options(pure, nomem, nostack),
            );
        }
        // assert_eq!(self.0, x);
    }
}

impl Mod {
    fn pow(self, mut exp: u32) -> Self {
        if exp == 0 {
            return Mod(1);
        }
        if self.0 < 2 {
            return self;
        }

        let mut base = self;
        while exp & 1 == 0 {
            base *= base;
            exp >>= 1;
        }
        let mut acc = base;
        exp >>= 1;
        while exp != 0 {
            base *= base;
            if exp & 1 == 1 {
                acc *= base;
            }
            exp >>= 1;
        }
        acc
    }
}

I expected to see this happen: the 3 assertions in main should pass.

Instead, this happened: the first assertion fails.

$ cargo +nightly run --release
[...]
thread 'main' panicked at 'assertion failed: `(left == right)`
  left: `654705331`,
 right: `650`', src/main.rs:5:5

As noted in the code comments, it works if I change the for_each to a regular for loop. It also works if I add an assertion in my MulAssign comparing the asm! result to a u64 computation. Furthermore, n = 4 only needs primes [2, 3], but it works if I hard-code that instead of using primal.

Meta

This failure is reproducible on both i686 and x86_64, from 1.59.0 (which stabilized asm!) through nightly.

The bad left value is consistent from run to run, but different between versions -- 1.59.0 gets 0 instead.

$ rustc +1.59.0 -Vv
rustc 1.59.0 (9d1b2106e 2022-02-23)
binary: rustc
commit-hash: 9d1b2106e23b1abd32fce1f17267604a5102f57a
commit-date: 2022-02-23
host: x86_64-unknown-linux-gnu
release: 1.59.0
LLVM version: 13.0.0

$ rustc +nightly -Vv
rustc 1.64.0-nightly (4d6d601c8 2022-07-26)
binary: rustc
commit-hash: 4d6d601c8a83284d6b23c253a3e2a060fd197316
commit-date: 2022-07-26
host: x86_64-unknown-linux-gnu
release: 1.64.0-nightly
LLVM version: 14.0.6
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Assignees

No one assigned

    Labels

    A-LLVMArea: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues.Area: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues.C-bugCategory: This is a bug.Category: This is a bug.I-miscompileIssue: Correct Rust code lowers to incorrect machine codeIssue: Correct Rust code lowers to incorrect machine codeT-compilerRelevant to the compiler team, which will review and decide on the PR/issue.Relevant to the compiler team, which will review and decide on the PR/issue.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions