Why is a HashMap's order different every run?
Short answer: a HashMap records no order, and the RandomState hasher behind HashMap::new()
is randomly seeded and re-keyed for every new map, so the same inserts come back in a different
sequence in every run. When the output has to be stable, sort the keys or use a BTreeMap.
From a fresh project — cargo new hash-order — replace the contents of src/main.rs with each
block below and run it with cargo run. The project name shows up only in cargo’s own output;
nothing else here depends on it.
The order is not recorded
Section titled “The order is not recorded”A hash map keeps entries in buckets chosen by the hash of each key. Nothing records the order the
keys arrived in, and iteration walks the table, so the sequence you get back is a property of the
hashes and the table, not of the inserts. The standard library’s own docs for iter, keys and
values say the same thing in one phrase: “in arbitrary order”.
use std::collections::HashMap;
fn main() { let mut scores = HashMap::new(); for name in ["Ada", "Grace", "Katherine", "Margaret", "Barbara"] { scores.insert(name, name.len()); }
for (name, score) in &scores { println!("{name}: {score}"); }
let mut sorted: Vec<_> = scores.keys().copied().collect(); sorted.sort(); println!("sorted: {sorted:?}");}Two runs of that same binary, back to back, on this machine (Rust 1.99.0, edition 2024). The first:
Margaret: 8Barbara: 7Grace: 5Katherine: 9Ada: 3sorted: ["Ada", "Barbara", "Grace", "Katherine", "Margaret"]Same binary, same machine, second run:
Katherine: 9Ada: 3Grace: 5Margaret: 8Barbara: 7sorted: ["Ada", "Barbara", "Grace", "Katherine", "Margaret"]The last line never moves; the map’s order does.
Where the randomness comes from
Section titled “Where the randomness comes from”HashMap::new() builds a RandomState, and the standard library’s documentation for HashMap
is direct about why: the default algorithm is “selected to provide resistance against HashDoS
attacks” and is “randomly seeded”, currently SipHash 1-3. If the seed were fixed, anyone could
pre-compute keys that all land in one bucket and turn every lookup into a scan.
The seed is per map, not per process. Each RandomState::new() takes a per-thread pair of random
keys and increments one of them, so the next map on the same thread gets different keys. Two maps
built one line apart disagree:
use std::collections::HashMap;
fn main() { let names = ["Ada", "Grace", "Katherine", "Margaret", "Barbara"];
let mut first = HashMap::new(); let mut second = HashMap::new(); for name in names { first.insert(name, 1); second.insert(name, 2); }
let a: Vec<_> = first.keys().copied().collect(); let b: Vec<_> = second.keys().copied().collect(); println!("{a:?}"); println!("{b:?}"); println!("same order: {}", a == b);}["Margaret", "Ada", "Katherine", "Barbara", "Grace"]["Ada", "Katherine", "Grace", "Margaret", "Barbara"]same order: falseBoth lines differ from run to run, and the two never agreed across the five runs behind this page.
Within one run the map is stable — iterating it twice gives the same sequence — and two maps
sharing one RandomState (HashMap::with_hasher(state.clone())) do agree.
Getting a stable order
Section titled “Getting a stable order”Sort when you print. The last line of the first block is the pattern: collect the keys,
sort(), print. The map stays a HashMap, and the cost is one sort per output.
Or use a BTreeMap. Sorted iteration is the structure, not a step:
use std::collections::BTreeMap;
fn main() { let mut scores = BTreeMap::new(); for name in ["Ada", "Grace", "Katherine", "Margaret", "Barbara"] { scores.insert(name, name.len()); }
for (name, score) in &scores { println!("{name}: {score}"); }}Ada: 3Barbara: 7Grace: 5Katherine: 9Margaret: 8The trade-off is the one from the HashMap versus BTreeMap article:
lookups move from expected O(1) to O(log n), and keys need Ord instead of Hash + Eq.
Or keep the HashMap and fix the hasher. BuildHasherDefault<DefaultHasher> seeds SipHash
with fixed keys, so every run of that binary lays the table out the same way:
use std::collections::HashMap;use std::hash::{BuildHasherDefault, DefaultHasher};
fn main() { let mut scores: HashMap<&str, usize, BuildHasherDefault<DefaultHasher>> = HashMap::default(); for name in ["Ada", "Grace", "Katherine", "Margaret", "Barbara"] { scores.insert(name, name.len()); }
for (name, score) in &scores { println!("{name}: {score}"); }}Grace: 5Katherine: 9Ada: 3Barbara: 7Margaret: 8Two caveats, both from the standard library’s own documentation. The default hasher’s algorithm “is not specified, and so it and its hashes should not be relied upon over releases” — a future compiler may order the table differently. And the random seed is the HashDoS protection; with fixed keys it’s gone, so this is the wrong tool when the keys come from untrusted input.
Related
Section titled “Related”- Which collection should you use? — the decision table for the whole chapter
- What is the difference between HashMap and BTreeMap? — the map whose order comes for free
- How to check whether a collection contains a value — the previous article in this chapter
Next in this chapter: how to count word frequency with a HashMap.