Skip to content

PGO instrumentation: counter in num_bigint::div_rem_core incremented by a heap address instead of 1 #164635

Closed as duplicate

Description

@sylvainbx

Summary

With rustc 1.99.1 (LLVM 23.1.3), a -Cprofile-generate build of a small program using num-bigint 0.4.8 writes a raw profile in which one counter of num_bigint::biguint::division::div_rem_core holds a value around 1.5e17. llvm-profdata merge then fails:

warning: train.profraw: excessively large counter value suggests corrupted profile data: 154409276267866944
error: no profile can be merged

The profile file itself is well formed (format version 11). Its 191 records cover the 4277 counters with no gap, and exactly one counter is wrong (115 of the 117 counters of div_rem_core). The largest other counter is 123816. The instrumented code adds the value of a register that holds a heap pointer to that counter, instead of adding 1 (or a 0/1 flag). It does this each time the block runs, and 1636 executions times an address near 0x5585_xxxx_xxxx gives the observed ~1.54e17.

The same source builds a correct profile with rustc 1.97.0 (LLVM 22.1.6).

Environment

  • rustc 1.99.1 (58cf8f9c3 2026-10-13), Arch Linux package rust 1:1.99.1-1, LLVM 23.1.3
  • llvm-profdata 23.1.3 (same LLVM version)
  • host: x86_64-unknown-linux-gnu
  • Not reproduced with rustc 1.97.0 / LLVM 22.1.6 (rustup), same sources and flags

Reproduction

Cargo.toml:

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

[dependencies]
num-bigint = "0.4"   # 0.4.8 in Cargo.lock
num-traits = "0.2"   # 0.2.19

[profile.release]
opt-level = 3
lto = "fat"
codegen-units = 1
panic = "abort"
src/main.rs (111 lines)
// pidigits, CLBG Rust #4 (TeXitoi, Ryohei Machida, 3-Clause BSD), with num-bigint instead of rug
use num_bigint::BigInt;
use num_traits::ToPrimitive;
use std::cmp;
use std::io::Write;

pub struct Context {
    q: BigInt,
    r: BigInt,
    t: BigInt,
    tmp1: BigInt,
    k: u32,
}

impl Context {
    pub fn new() -> Context {
        Context {
            q: BigInt::from(1),
            r: BigInt::from(0),
            t: BigInt::from(1),
            tmp1: BigInt::from(0),
            k: 0,
        }
    }

    pub fn next(&mut self) -> u8 {
        loop {
            self.next_term();
            if self.q > self.r {
                continue;
            }
            let d = self.extract(3);
            if d != self.extract(4) {
                continue;
            }

            self.produce(d);
            return d;
        }
    }

    fn next_term(&mut self) {
        self.k += 1;
        let k2 = self.k * 2 + 1;

        self.tmp1.clone_from(&self.q);
        self.tmp1 <<= 1u32;
        self.r += &self.tmp1;
        self.r *= k2;
        self.t *= k2;
        self.q *= self.k;
    }

    #[inline]
    fn extract(&mut self, nth: u32) -> u8 {
        if nth.is_power_of_two() {
            // use shift operation if possible
            self.tmp1.clone_from(&self.q);
            self.tmp1 <<= nth.trailing_zeros();
        } else {
            self.tmp1.clone_from(&self.q);
            self.tmp1 *= nth;
        };

        self.tmp1 += &self.r;
        (&self.tmp1 / &self.t).to_u8().unwrap()
    }

    fn produce(&mut self, n: u8) {
        self.q *= 10u8;
        self.tmp1.clone_from(&self.t);
        self.tmp1 *= n;
        self.r -= &self.tmp1;
        self.r *= 10u8;
    }
}

fn main() {
    let n: usize = std::env::args_os()
        .nth(1)
        .and_then(|s| s.into_string().ok())
        .and_then(|n| n.parse().ok())
        .unwrap_or(27);

    let mut ctx = Context::new();

    // line buffer
    let mut line_buf = [0u8; 12];
    line_buf[10] = b'\t';
    line_buf[11] = b':';

    // output buffer
    let mut output = Vec::with_capacity(n * 2);

    for d in (0..n).step_by(10) {
        let count = cmp::min(10, n - d);

        for i in 0..count {
            line_buf[i] = b'0' + ctx.next();
        }

        for i in count..10 {
            line_buf[i] = b' ';
        }

        output.extend_from_slice(&line_buf);
        let _ = writeln!(output, "{}", d + count);
    }

    let _ = std::io::stdout().write_all(&*output);
}
RUSTFLAGS="-Cprofile-generate=$PWD/prof" cargo build --release
LLVM_PROFILE_FILE=$PWD/prof/train.profraw target/release/pidigits 200 > /dev/null
llvm-profdata merge -o merged.profdata prof/train.profraw

Expected: the merge succeeds.
Actual: excessively large counter value ... error: no profile can be merged. The bad value changes from run to run (1.537e17 to 1.549e17), because it is a sum of heap addresses.

It also fails with each of the following changes, made one at a time:

  • without -C target-cpu=native;
  • lto = false or lto = "thin";
  • codegen-units = 16;
  • panic = "unwind";
  • opt-level = 2;
  • all of the above at once.

Analysis

In the instrumented binary, counters 114 to 116 of div_rem_core are updated without branches. Counters 114 and 116 get the 0/1 flag rdx == -1 (sete %r8b). Counter 115 is updated in the same 128-bit vpaddq as 116, and it should also get a 0/1 value. Instead, it gets %rax, which is the pointer that the same condition selects for the following store (cmovne %rax,%rsi / mov %rcx,(%rsi,%r10,8)):

xor    %edi,%edi
mov    %rax,%rsi
cmp    $0xffffffffffffffff,%rdx
sete   %r8b
je     2d89b
mov    0x80(%rsp),%rsi
mov    %r8b,%dil
add    %rdi,0x67ea3(%rip)        # 95748  ; counter #114
cmp    %rsi,%r10
jae    2e189
test   $0x1,%al
mov    $0x8,%esi
lea    0x80(%rsp),%r8
cmovne %r8,%rsi
vmovq  %rax,%xmm0
vmovd  %edi,%xmm1
vpunpcklqdq %xmm1,%xmm0,%xmm0
vpaddq 0x67e7a(%rip),%xmm0,%xmm0        # 95750  ; counters #115, #116
vmovdqu %xmm0,0x67e72(%rip)        # 95750  ; counters #115, #116
cmp    $0xffffffffffffffff,%rdx
cmovne %rax,%rsi
mov    %rcx,(%rsi,%r10,8)

Under gdb, %rax at vmovq %rax,%xmm0 is 0x5555555fc7b0, then 0x5555555fc7e0 (heap addresses). The same instrumented binary produces the bad profile on two different machines, so the cause is the generated code, not the environment.

What avoids it (instrumented build only, LLVM 23.1.3)

Extra flags Result
-Cllvm-args=-vectorize-slp=false still corrupt
-Cllvm-args=-do-counter-promotion=false still corrupt
-Cllvm-args=-instrprof-atomic-counter-update-all still corrupt
-Cllvm-args=-two-entry-phi-node-folding-threshold=0 -Cllvm-args=-phi-node-folding-threshold=0 OK
-Cllvm-args=-speculate-one-expensive-inst=false -Cllvm-args=-two-entry-phi-node-folding-threshold=0 OK
opt-level = 1 for num-bigint only OK
opt-level = 1 or 0 for everything OK

This points at SimplifyCFG's two-entry PHI folding (foldTwoEntryPHINode) rather than SLP or counter promotion.

Only the profile counters were seen to be wrong, and the program output is correct. But this is a miscompile, so non-instrumented code using the same pattern may be affected as well.

I can provide the raw profile and the instrumented binary on request.

Activity

  1. added
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Oct 20, 2026
  2. added
    A-LLVMArea: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues.
    A-PGOArea: Profile-guided optimizations (PGO)
    on Oct 20, 2026
  3. added
    I-miscompileIssue: Correct Rust code lowers to incorrect machine code
    and removed
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Oct 20, 2026
  4. added
    I-prioritizeIssue needs a team member to assess the impact. Will be replaced by P-{low,medium,high,critical}
    on Oct 20, 2026
  5. added
    T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.
    on Oct 20, 2026
  6. nikic commented on Oct 20, 2026

    @nikic
    Contributor

    This sounds like a duplicate of #163616, which is fixed on beta. Can you still reproduce the issue on beta?

  7. maxdexh commented on Oct 20, 2026

    @maxdexh
    Member

    Oops, my bad, I thought I initialized rustup in my VM using default nightly while verifying, but I did stable instead.

    This doesn't reproduce on nightly

  8. removed
    I-prioritizeIssue needs a team member to assess the impact. Will be replaced by P-{low,medium,high,critical}
    on Oct 20, 2026
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

    A-LLVMArea: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues.A-PGOArea: Profile-guided optimizations (PGO)C-bugCategory: This is a bug.I-miscompileIssue: Correct Rust code lowers to incorrect machine codeT-compilerRelevant 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