Loading pageā¦
Rust walkthroughs
Loading pageā¦
rand::seq::SliceRandom::shuffle differ from manual Fisher-Yates implementation in terms of security and performance?rand::seq::SliceRandom::shuffle implements Fisher-Yates correctly and securely by default, using a cryptographically secure random number generator when available and properly handling the subtle indexing requirements that often introduce bugs in manual implementations. A manual Fisher-Yates implementation can be vulnerable to modulo bias, use weak random number generators, or contain off-by-one errors in the swapping logic. The rand crate's shuffle is optimized, well-tested, and integrates with Rust's RNG ecosystem, while manual implementations require careful attention to security and correctness details that are easy to get wrong.
use rand::seq::SliceRandom;
use rand::thread_rng;
fn main() {
let mut items = vec![1, 2, 3, 4, 5];
// Using rand's shuffle
items.shuffle(&mut thread_rng());
println!("Shuffled: {:?}", items);
}SliceRandom::shuffle provides a trait-based shuffle method for slices.
// The Fisher-Yates (Knuth) shuffle algorithm:
// For each element i from last to first:
// Pick random index j from 0 to i (inclusive)
// Swap elements at i and j
fn manual_fisher_yates<T>(slice: &mut [T], rng: &mut impl rand::Rng) {
for i in (1..slice.len()).rev() {
let j = rng.gen_range(0..=i);
slice.swap(i, j);
}
}
fn main() {
use rand::thread_rng;
let mut items = vec![1, 2, 3, 4, 5];
manual_fisher_yates(&mut items, &mut thread_rng());
println!("Manual shuffle: {:?}", items);
let mut items2 = vec![1, 2, 3, 4, 5];
items2.shuffle(&mut thread_rng());
println!("rand shuffle: {:?}", items2);
}Fisher-Yates produces a uniformly random permutation with O(n) time complexity.
use rand::Rng;
// VULNERABLE: Modulo bias
fn biased_shuffle<T>(slice: &mut [T]) {
let mut rng = rand::thread_rng();
for i in (1..slice.len()).rev() {
// WRONG: Using % introduces bias
// If slice.len() doesn't evenly divide RNG range,
// some indices are more likely
let j = rng.gen::<usize>() % (i + 1);
slice.swap(i, j);
}
}
// SECURE: rand's approach
fn secure_shuffle<T>(slice: &mut [T]) {
use rand::seq::SliceRandom;
use rand::thread_rng;
slice.shuffle(&mut thread_rng());
}
fn main() {
// Demonstrate modulo bias with small example
// If RNG produces 0-255 and we want 0-2:
// 0: 0, 3, 6, ..., 252 (85 values)
// 1: 1, 4, 7, ..., 253 (85 values)
// 2: 2, 5, 8, ..., 254 (85 values)
// But 255 % 3 = 0, so 0 gets 86 values
// Bias: 0 is slightly more likely
println!("Modulo bias creates non-uniform distributions");
println!("rand::shuffle uses gen_range which avoids bias");
}rand::shuffle uses gen_range which handles modulo bias correctly.
use rand::Rng;
fn main() {
let mut rng = rand::thread_rng();
// gen_range handles bias correctly
// It uses rejection sampling when range doesn't divide RNG output evenly
// Manual: biased
let biased = rng.gen::<u32>() % 100; // Bias if 100 doesn't divide u32::MAX
// Correct: unbiased
let unbiased = rng.gen_range(0..100); // Uses rejection sampling
// rand's shuffle internally uses gen_range(0..=i)
// which is unbiased for all ranges
}gen_range uses rejection sampling to eliminate bias.
use rand::seq::SliceRandom;
use rand::{thread_rng, rngs::StdRng, SeedableRng};
fn main() {
let mut items = vec!["a", "b", "c", "d", "e"];
// Cryptographically secure (default)
items.shuffle(&mut thread_rng());
// thread_rng() uses OsRng or ChaCha20Rng
// Suitable for security-sensitive applications
// Deterministic (not cryptographically secure)
let mut items2 = vec!["a", "b", "c", "d", "e"];
let mut deterministic_rng = StdRng::seed_from_u64(42);
items2.shuffle(&mut deterministic_rng);
// Predictable output - NOT for security
println!("Deterministic shuffle: {:?}", items2);
// Manual implementation often uses weak RNG:
// let j = rand::random::<usize>() % (i + 1);
// This is:
// 1. Using thread_rng() (good)
// 2. But modulo bias (bad)
}rand::shuffle works with any Rng implementation, allowing security-conscious choices.
use rand::Rng;
fn main() {
// BUG 1: Wrong range (excluding last element)
fn bug1_shuffle<T>(slice: &mut [T], rng: &mut impl rand::Rng) {
for i in (1..slice.len()).rev() {
let j = rng.gen_range(0..i); // WRONG: should be 0..=i
// Last element never swapped with itself
// This makes some permutations impossible
slice.swap(i, j);
}
}
// BUG 2: Swapping wrong indices
fn bug2_shuffle<T>(slice: &mut [T], rng: &mut impl rand::Rng) {
for i in 0..slice.len() {
let j = rng.gen_range(0..slice.len()); // WRONG: should be 0..=i
// All elements can swap with all elements
// Not uniform distribution
slice.swap(i, j);
}
}
// BUG 3: Using random() for each swap (inefficient)
fn bug3_shuffle<T>(slice: &mut [T>) {
use rand::random;
for i in (1..slice.len()).rev() {
let j = random::<usize>() % (i + 1); // Modulo bias + inefficient
slice.swap(i, j);
}
}
// rand's shuffle: correct by default
fn correct_shuffle<T>(slice: &mut [T], rng: &mut impl rand::Rng) {
slice.shuffle(rng); // Correct, tested, optimized
}
}Manual implementations are prone to subtle bugs that affect correctness.
use rand::seq::SliceRandom;
use rand::{thread_rng, Rng};
use std::time::Instant;
fn manual_fisher_yates<T>(slice: &mut [T], rng: &mut impl rand::Rng) {
for i in (1..slice.len()).rev() {
let j = rng.gen_range(0..=i);
slice.swap(i, j);
}
}
fn main() {
const N: usize = 100_000;
let mut data1: Vec<i32> = (0..N as i32).collect();
let mut data2: Vec<i32> = (0..N as i32).collect();
// Manual implementation
let start = Instant::now();
manual_fisher_yates(&mut data1, &mut thread_rng());
let manual_time = start.elapsed();
// rand's shuffle
let start = Instant::now();
data2.shuffle(&mut thread_rng());
let rand_time = start.elapsed();
println!("Shuffling {} elements:", N);
println!(" Manual: {:?}", manual_time);
println!(" rand::shuffle: {:?}", rand_time);
// Performance is nearly identical
// rand::shuffle may be slightly faster due to optimizations
}Performance is comparable; rand::shuffle may have minor optimizations.
// rand's actual implementation (simplified):
//
// pub fn shuffle<R: Rng>(slice: &mut [T], rng: &mut R) {
// for i in (1..slice.len()).rev() {
// let j = rng.gen_range(0..=i);
// slice.swap(i, j);
// }
// }
// Key implementation choices:
// 1. Uses gen_range (not modulo) - no bias
// 2. Correct range: 0..=i (inclusive)
// 3. Iterates from end to start
// 4. Works with any Rng implementation
// 5. Optimized by compiler (inline, bounds check elimination)
fn main() {
// The implementation is the same Fisher-Yates
// but with all edge cases handled correctly
}rand::shuffle implements Fisher-Yates correctly with all edge cases handled.
use rand::seq::SliceRandom;
use rand::thread_rng;
fn main() {
let mut items = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// partial_shuffle: shuffle only first n elements
let remaining = items.partial_shuffle(&mut thread_rng(), 3);
println!("Shuffled first 3: {:?}", &items[..3]);
println!("Remaining: {:?}", remaining);
// More efficient than full shuffle when you only need
// a few random elements
}partial_shuffle efficiently shuffles only a prefix.
use rand::seq::SliceRandom;
use rand::{thread_rng, rngs::OsRng};
fn main() {
// Cryptographic contexts (passwords, tokens, etc.)
let mut secret_data = vec!["secret1", "secret2", "secret3"];
// SECURE: Use OsRng or thread_rng (which uses OsRng internally)
secret_data.shuffle(&mut OsRng);
// or
secret_data.shuffle(&mut thread_rng());
// Gaming/entertainment (doesn't need crypto security)
let mut deck = vec!["A", "K", "Q", "J", "10", "9", "8"];
deck.shuffle(&mut thread_rng());
// Reproducible testing (NOT for security)
use rand::rngs::StdRng;
use rand::SeedableRng;
let mut test_data = vec![1, 2, 3, 4, 5];
let mut rng = StdRng::seed_from_u64(12345);
test_data.shuffle(&mut rng); // Deterministic for testing
}Choose RNG based on security requirements; rand::shuffle adapts to any.
use rand::seq::SliceRandom;
use rand::thread_rng;
fn main() {
// Fisher-Yates (and rand::shuffle) is NOT constant-time
// Memory access patterns reveal swap operations
// This may matter for security-sensitive applications
let mut sensitive = vec!["password1", "password2", "password3"];
sensitive.shuffle(&mut thread_rng());
// Timing analysis could reveal:
// - Which elements were swapped
// - Potentially information about RNG state
// For constant-time operations, you need specialized code
// Fisher-Yates cannot be constant-time due to memory swaps
// However, this is rarely a practical concern
// The RNG quality matters much more for security
}Fisher-Yates cannot be constant-time, but this is rarely exploitable.
use rand::seq::SliceRandom;
use rand::thread_rng;
use std::collections::HashMap;
fn main() {
// Test: all permutations should be equally likely
const TRIALS: usize = 100_000;
let mut permutation_counts: HashMap<Vec<u8>, usize> = HashMap::new();
for _ in 0..TRIALS {
let mut items = vec![0u8, 1, 2];
items.shuffle(&mut thread_rng());
*permutation_counts.entry(items).or_insert(0) += 1;
}
// For 3 elements, there are 6 permutations
// Each should occur approximately 1/6 of the time
println!("Permutation distribution:");
for (perm, count) in &permutation_counts {
let percentage = (*count as f64 / TRIALS) * 100.0;
println!(" {:?}: {:.2}%", perm, percentage);
}
// Expected: ~16.67% each
// Biased shuffles would show non-uniform distribution
}rand::shuffle produces uniformly distributed permutations.
// Rare cases where manual implementation could be justified:
// 1. No dependencies allowed
fn no_deps_shuffle<T>(slice: &mut [T]) {
// But you still need a RNG...
// Usually not worth it
}
// 2. Custom RNG not implementing rand::Rng
struct CustomRng;
// Implement rand::Rng for it instead of manual shuffle
// 3. Specialized in-place algorithm for embedded
// But even embedded can often use rand_core
// 4. Educational purposes
fn educational_shuffle<T>(slice: &mut [T], rng: &mut impl rand::Rng) {
for i in (1..slice.len()).rev() {
let j = rng.gen_range(0..=i); // Note: correct range
slice.swap(i, j);
}
}
fn main() {
// In practice: use rand::shuffle
}Almost always prefer rand::shuffle over manual implementation.
use rand::seq::SliceRandom;
use rand::thread_rng;
fn main() {
// rand::shuffle advantages:
// 1. Correct by default (no modulo bias)
// 2. Proper range (0..=i, inclusive)
// 3. Well-tested implementation
// 4. Works with any RNG implementation
// 5. Optimized by compiler
// 6. Clear intent in code
// Manual Fisher-Yates concerns:
// 1. Easy to get range wrong (0..i vs 0..=i)
// 2. Modulo bias from % operator
// 3. Wrong iteration direction
// 4. Must handle all edge cases
// 5. No automatic optimization hints
// Always use rand::shuffle unless you have:
// - No dependencies (rare)
// - Custom RNG that can't implement rand::Rng (rare)
// - Security audited custom implementation needed (very rare)
}Core distinction:
rand::shuffle: Correct, secure, optimized Fisher-Yates implementationSecurity advantages of rand::shuffle:
gen_range internally (no modulo bias)thread_rng, OsRng)Common manual implementation bugs:
rng.gen() % n instead of gen_range(0..n))0..i instead of 0..=i)Performance comparison:
rand::shuffle may have minor optimizationsrand::shuffle well (inlining, bounds check elimination)Key insight: Fisher-Yates is a simple algorithm, but the details matter enormously for security and correctness. Modulo bias creates non-uniform distributions that can be exploited in cryptographic contexts. Range errors make some permutations impossible. The rand crate's shuffle handles all these details correctly and works with any RNG, making it the right choice for virtually all use cases. The security of your shuffle depends primarily on RNG choiceāuse OsRng or thread_rng for cryptographic applications, StdRng with a seed for reproducible testing.