ElBlo

rdseed behavior under load

The rdrand and rdseed instructions can be used to generate random numbers. These instructions can fail, and they indicate their success by setting the Carry Flag in the EFLAGS register.

The Intel Documentation1 says that rdseed could fail if we ask for values faster than they can be generated.

This simple program helps you test that:

use std::arch::asm;
use std::env;
use std::thread;
use std::time::{Duration, Instant};

fn rdrand_count(its: usize) -> (u64, Duration) {
    let zero: u64 = 0;
    let mut count: u64 = 0;
    let now = Instant::now();
    for _ in 0..its {
        unsafe {
            asm!(
                "rdrand {tmp}",
                "adc {count}, {zero}",
                zero = in(reg) zero,
                count = inout(reg) count,
                tmp = out(reg) _
            );
        }
    }

    (count, now.elapsed())
}

fn rdseed_count(its: usize) -> (u64, Duration) {
    let zero: u64 = 0;
    let mut count: u64 = 0;
    let now = Instant::now();
    for _ in 0..its {
        unsafe {
            asm!(
                "rdseed {tmp}",
                "adc {count}, {zero}",
                zero = in(reg) zero,
                count = inout(reg) count,
                tmp = out(reg) _
            );
        }
    }

    (count, now.elapsed())
}

fn parse_flags(args: &Vec<String>) -> (fn(usize) -> (u64, Duration), usize, usize) {
    println!("{:?}", args);
    // args[1] should have the function name.
    let func = if args.len() > 1 {
        match &args[1][..] {
            "rdseed" => rdseed_count,
            "rdrand" => rdrand_count,
            _ => {
                panic!(
                    "invalid function {}. usage: {} rdrand/rdseed iterations threads",
                    args[1], args[0]
                );
            }
        }
    } else {
        rdseed_count
    };

    let tries = if args.len() > 2 {
        args[2].parse().unwrap()
    } else {
        4000
    };

    let num_threads = if args.len() > 3 {
        args[3].parse().unwrap()
    } else {
        2
    };

    return (func, tries, num_threads);
}

fn main() {
    let args: Vec<String> = env::args().collect();
    let (func, tries, num_threads) = parse_flags(&args);
    let mut threads = vec![];

    for _ in 0..num_threads {
        threads.push(thread::spawn(move || loop {
            func(tries);
        }));
    }

    for _ in 0..100 {
        let (successes, elapsed) = func(tries);
        println!("{}/{}\t{:?}", successes, tries, elapsed);
    }
}

The idea is that you launch background threads that run the rdrand/rdseed instructions in a loop, and then the main thread does the same and measures how long it took and how many of them were successful.

I’ve tested this on an AMD Ryzen Threadripper PRO 3995WX, and I couldn’t get either rdrand or rdseed to fail. Instead, the instructions get way slower as we add more load to the system.

I’ve also tested it on an Intel Xeon Gold 6154, and the behavior there is different: I got a 0.0175 success rate on average using 16/72 threads; after that, the success rate is more or less the same, but the performance starts decreasing.


  1. Intel® Digital Random Number Generator (DRNG) Software Implementation Guide ↩︎

© Marco Vanotti 2024

Powered by Hugo & new.css.