Loading page…
Rust walkthroughs
Loading page…
rayon::iter::enumerate differ from Iterator::enumerate when processing items in parallel?Iterator::enumerate wraps each item with its index sequentially as items flow through the iterator, yielding (0, item0), (1, item1), and so on in order. rayon::iter::enumerate also wraps items with their indices, but the indices reflect each item's position in the original parallel collection, not the order of processing. Since Rayon processes items in parallel across multiple threads, items may be processed out of order, but each item receives the correct index corresponding to its position in the source data. The key difference is that sequential enumerate guarantees items are yielded in index order, while parallel enumerate guarantees items have correct indices but processes them in arbitrary order.
fn sequential_enumerate() {
let data = vec!["a", "b", "c", "d", "e"];
// Iterator::enumerate processes sequentially
for (index, item) in data.iter().enumerate() {
println!("Index {}: {}", index, item);
}
// Output is always in order: 0, 1, 2, 3, 4
// The index is assigned as items flow through
let enumerated: Vec<(usize, &str)> = data.iter().enumerate().collect();
println!("{:?}", enumerated);
// [(0, "a"), (1, "b"), (2, "c"), (3, "d"), (4, "e")]
}Iterator::enumerate yields items in sequential order with consecutive indices.
use rayon::prelude::*;
fn parallel_enumerate() {
let data = vec!["a", "b", "c", "d", "e"];
// Rayon::enumerate processes in parallel, indices are position-based
data.par_iter().enumerate().for_each(|(index, item)| {
// Order of printing is non-deterministic!
// But each item gets the CORRECT index
println!("Index {}: {}", index, item);
});
// Collecting preserves order
let enumerated: Vec<(usize, &str)> = data.par_iter().enumerate().collect();
println!("{:?}", enumerated);
// [(0, "a"), (1, "b"), (2, "c"), (3, "d"), (4, "e")]
}Rayon's enumerate assigns indices based on position in the original data, not processing order.
use rayon::prelude::*;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{Duration, Instant};
fn execution_order() {
// Sequential: always in order
println!("Sequential enumerate:");
(0..5).enumerate().for_each(|(i, _)| {
println!(" Processing {}", i);
});
// Output: 0, 1, 2, 3, 4 (always)
// Parallel: order depends on thread scheduling
println!("\nParallel enumerate:");
(0..5).into_par_iter().enumerate().for_each(|(i, _)| {
// Sleep to make parallelism visible
std::thread::sleep(Duration::from_millis(10));
println!(" Processing {}", i);
});
// Output: possibly 3, 0, 2, 4, 1 or any order
// But the INDEX is always correct for each item
}Sequential enumerate guarantees order; parallel enumerate guarantees correct indices but not processing order.
use rayon::prelude::*;
fn index_mechanism() {
let data = vec![10, 20, 30, 40, 50];
// Iterator::enumerate: assigns index as items arrive
// Step 1: yield (0, 10)
// Step 2: yield (1, 20)
// ... and so on
// Rayon::enumerate: knows indices based on position
// Data: [10, 20, 30, 40, 50]
// Position 0 -> index 0
// Position 1 -> index 1
// etc.
// Even if processed in parallel, each item knows its index
data.par_iter().enumerate().for_each(|(index, &value)| {
// index matches position in original data
assert_eq!(value, data[index]);
});
}Rayon computes indices based on position in the parallel collection structure.
use rayon::prelude::*;
fn working_with_indices() {
let data = vec![100, 200, 300, 400, 500];
// Sequential: use index to access another collection
let multipliers = vec![1, 2, 3, 4, 5];
let sequential: Vec<i32> = data
.iter()
.enumerate()
.map(|(i, &val)| val * multipliers[i])
.collect();
println!("Sequential: {:?}", sequential); // [100, 400, 900, 1600, 2500]
// Parallel: same pattern works
let parallel: Vec<i32> = data
.par_iter()
.enumerate()
.map(|(i, &val)| val * multipliers[i])
.collect();
println!("Parallel: {:?}", parallel); // [100, 400, 900, 1600, 2500]
// Both produce the same result because indices are correct
}Index-based operations work correctly in both sequential and parallel contexts.
use rayon::prelude::*;
fn collect_order() {
let data = vec!["a", "b", "c", "d", "e"];
// Even though parallel enumerate may process items out of order,
// collect() returns items in original order
let result: Vec<(usize, &str)> = data.par_iter().enumerate().collect();
assert_eq!(result, vec![
(0, "a"),
(1, "b"),
(2, "c"),
(3, "d"),
(4, "e")
]);
// The parallel iterator infrastructure preserves order on collect
}collect on parallel iterators returns items in their original order.
use rayon::prelude::*;
fn map_with_enumerate() {
let names = vec!["alice", "bob", "charlie", "diana"];
// Sequential pattern
let sequential: Vec<String> = names
.iter()
.enumerate()
.map(|(i, name)| format!("{}. {}", i + 1, name))
.collect();
println!("Sequential: {:?}", sequential);
// ["1. alice", "2. bob", "3. charlie", "4. diana"]
// Parallel pattern - same result
let parallel: Vec<String> = names
.par_iter()
.enumerate()
.map(|(i, name)| format!("{}. {}", i + 1, name))
.collect();
println!("Parallel: {:?}", parallel);
// ["1. alice", "2. bob", "3. charlie", "4. diana"]
}Common patterns like numbered formatting work identically.
use rayon::prelude::*;
fn filter_with_enumerate() {
let data = vec![5, 10, 15, 20, 25, 30, 35, 40];
// Sequential: filter with index condition
let sequential: Vec<(usize, i32)> = data
.iter()
.enumerate()
.filter(|(i, _)| i % 2 == 0)
.map(|(i, &v)| (i, v))
.collect();
println!("Sequential: {:?}", sequential);
// [(0, 5), (2, 15), (4, 25), (6, 35)]
// Parallel: same logic, parallel execution
let parallel: Vec<(usize, i32)> = data
.par_iter()
.enumerate()
.filter(|(i, _)| i % 2 == 0)
.map(|(i, &v)| (i, v))
.collect();
println!("Parallel: {:?}", parallel);
// [(0, 5), (2, 15), (4, 25), (6, 35)]
}Filter operations based on index work correctly in both cases.
use rayon::prelude::*;
use std::sync::atomic::{AtomicI32, Ordering};
fn processing_order_effects() {
let data = vec![1, 2, 3, 4, 5];
// Sequential: order is deterministic
let mut sum = 0;
data.iter().enumerate().for_each(|(i, &val)| {
sum += val;
println!("Step {}: sum = {}", i, sum);
});
// Output always: Step 0: sum=1, Step 1: sum=3, Step 2: sum=6, ...
// Parallel: order is non-deterministic
let sum = AtomicI32::new(0);
data.par_iter().enumerate().for_each(|(i, &val)| {
// WARNING: This has race conditions!
// sum.fetch_add(val, Ordering::SeqCst);
// The order of additions is non-deterministic
// (Final sum is correct due to atomic, but intermediate values vary)
});
}Sequential code can rely on processing order; parallel code cannot.
use rayon::prelude::*;
fn split_points() {
// Rayon splits work into chunks based on number of threads
// Each chunk knows its starting offset
let data: Vec<i32> = (0..1000).collect();
// When enumerate is called, Rayon:
// 1. Splits data into chunks (e.g., 4 chunks for 4 threads)
// 2. Each chunk knows it handles indices offset..offset+chunk_size
// 3. Items in each chunk get correct indices relative to start
data.par_iter().enumerate().for_each(|(index, &value)| {
// index = chunk_offset + position_within_chunk
// Each thread can compute index without global coordination
});
}Rayon computes indices efficiently using chunk offsets rather than per-item counters.
use rayon::prelude::*;
fn indexed_trait() {
// enumerate() requires IndexedParallelIterator
// This means the iterator must know its exact length
let vec_data: Vec<i32> = (0..100).collect();
vec_data.par_iter().enumerate(); // Works - Vec has known length
let slice_data: &[i32] = &vec_data;
slice_data.par_iter().enumerate(); // Works - slice has known length
// Some parallel iterators don't support enumerate
// because they don't implement IndexedParallelIterator
// (e.g., parallel adapters that change length unpredictably)
}enumerate requires IndexedParallelIterator, which means the iterator must report its exact length.
use rayon::prelude::*;
use std::time::Instant;
fn performance_comparison() {
let data: Vec<i32> = (0..1_000_000).collect();
// Sequential: single-threaded, O(n) but no parallelism
let start = Instant::now();
let sum_sequential: i64 = data.iter()
.enumerate()
.map(|(i, &v)| (i as i64) * (v as i64))
.sum();
println!("Sequential: {:?} (sum={})", start.elapsed(), sum_sequential);
// Parallel: multi-threaded, O(n) but parallelized
let start = Instant::now();
let sum_parallel: i64 = data.par_iter()
.enumerate()
.map(|(i, &v)| (i as i64) * (v as i64))
.sum();
println!("Parallel: {:?} (sum={})", start.elapsed(), sum_parallel);
// Results are identical, performance differs
assert_eq!(sum_sequential, sum_parallel);
}Both produce identical results; parallel version leverages multiple cores.
use rayon::prelude::*;
fn when_order_matters() {
let data = vec![1, 2, 3, 4, 5];
// Sequential: can build state progressively
let mut running_total = 0;
let running: Vec<i32> = data.iter()
.enumerate()
.map(|(i, &v)| {
running_total += v;
running_total
})
.collect();
println!("Running total: {:?}", running);
// [1, 3, 6, 10, 15]
// Parallel: CANNOT do this pattern - order is non-deterministic
// let running: Vec<i32> = data.par_iter()
// .enumerate()
// .map(|(i, &v)| {
// // Can't use shared mutable state correctly
// // Items may process in any order
// })
// .collect();
// Instead: compute without order dependency
let running: Vec<i32> = (1..=5_i32).collect();
let running: Vec<i32> = running.iter()
.scan(0, |state, &v| {
*state += v;
Some(*state)
})
.collect();
println!("Running total (seq): {:?}", running);
}Order-dependent computations require sequential processing or algorithm redesign.
use rayon::prelude::*;
fn after_adapters() {
let data = vec![10, 20, 30, 40, 50];
// enumerate after map
let mapped: Vec<(usize, i32)> = data
.par_iter()
.map(|&x| x * 2)
.enumerate()
.collect();
println!("After map: {:?}", mapped);
// [(0, 20), (1, 40), (2, 60), (3, 80), (4, 100)]
// enumerate after filter
let filtered: Vec<(usize, i32)> = data
.par_iter()
.filter(|&&x| x > 25)
.enumerate()
.collect();
println!("After filter: {:?}", filtered);
// [(0, 30), (1, 40), (2, 50)]
// Note: indices restart at 0 for filtered results
// enumerate after filter preserves position-based indexing
// within the filtered result
}enumerate after adapters assigns indices based on position in the transformed stream.
use rayon::prelude::*;
fn parallel_array_ops() {
let mut array = vec![0i32; 1000];
let values: Vec<i32> = (1..=1000).collect();
// Sequential: in-place update with index
for (i, &val) in values.iter().enumerate() {
array[i] = val * 2;
}
// Parallel: same pattern using enumerate
values.par_iter().enumerate().for_each(|(i, &val)| {
array[i] = val * 3; // Safe: each thread writes different indices
});
// Verify
assert_eq!(array[0], 3); // 1 * 3
assert_eq!(array[999], 3000); // 1000 * 3
}Index-based writes to arrays are safe in parallel when each index is unique.
use rayon::prelude::*;
fn sparse_processing() {
let data = vec![
Some(1), None, Some(3), None, None,
Some(6), None, Some(8), None, Some(10)
];
// Process only Some values, tracking original positions
let with_indices: Vec<(usize, i32)> = data
.par_iter()
.enumerate()
.filter_map(|(i, opt)| opt.map(|v| (i, v)))
.collect();
println!("Non-empty with indices: {:?}", with_indices);
// [(0, 1), (2, 3), (5, 6), (7, 8), (9, 10)]
// Indices reflect original positions
// This is useful for reconstructing data or reporting positions
}Index tracking enables position-aware processing of sparse data.
| Aspect | Iterator::enumerate | rayon::enumerate |
|--------|---------------------|-------------------|
| Processing order | Sequential, index order | Parallel, arbitrary order |
| Index assignment | As items are yielded | Based on position in source |
| Index correctness | Always correct | Always correct |
| collect order | Same as source | Same as source |
| Performance | Single-threaded | Multi-threaded |
| Trait required | Iterator | IndexedParallelIterator |
| Order-dependent code | Works naturally | Requires redesign |
Iterator::enumerate and rayon::enumerate differ fundamentally in execution semantics while producing identical results:
Iterator::enumerate (sequential):
rayon::enumerate (parallel):
Key insight: Both methods provide correct indices—the index assigned to each item matches its position in the source data. The difference is not in correctness but in execution semantics:
For code that only uses indices for lookup or tracking (without order dependencies), both approaches are interchangeable. For code that relies on sequential side effects or progressive state building, only sequential enumerate works directly—parallel code requires algorithmic changes like using atomic operations or reduction patterns.
The IndexedParallelIterator requirement ensures Rayon can compute indices efficiently—iterators that can't report their exact length can't support enumerate because there's no way to assign indices without knowing the total structure.