Rust HashMap: How to Efficiently Search and Insert Data

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

How to Efficiently Search and Insert Data in a HashMap in Rust: Peeking Under the Hood

\n\n

Have you ever written code in Rust and thought: "How do I simultaneously check if a key exists in a HashMap, and if not, insert a new value?" This question plagues thousands of developers on Stack Overflow. And for good reason: a naive solution can kill your application's performance. Today, we won't just give you the answer — we'll dive inside Rust's HashMap and see how it's built. You'll learn why get and insert separately are a path to pain, and how to properly use the entry API.

\n\n

This article is for those already familiar with the basics of Rust but who want a deeper understanding of how collections work under the hood. Let's go!

\n\n

The Problem: Two Calls — Double the Work

\n\n

Imagine a typical task: you have a HashMap to count word occurrences in a text. A naive approach looks like this:

\n\n
use std::collections::HashMap;\n\nfn count_words_naive(words: &[&str]) -> HashMap<String, u32> {\n    let mut map = HashMap::new();\n    \n    for word in words {\n        if map.contains_key(*word) {\n            let count = map.get(*word).unwrap();\n            map.insert(word.to_string(), count + 1);\n        } else {\n            map.insert(word.to_string(), 1);\n        }\n    }\n    \n    map\n}\n
\n\n

What's wrong here? We access the HashMap twice for each word: first contains_key, then get, and then insert. Each call involves computing a hash, searching in a bucket, and comparing keys. For a short text, this is negligible, but for millions of entries, it's a disaster.

\n\n

Under the Hood: How HashMap Works in Rust

\n\n

To understand why this is bad, let's break down how HashMap works in Rust. It's based on an open-addressing hash table (Robin Hood hashing). Here are the key points:

\n\n
    \n
  • Hashing: The key is transformed into a hash code using a hash function (default is SipHash-2-4).
  • \n
  • Buckets: The hash code determines the index in an array of buckets. In Rust, each bucket is a slot that can be empty, occupied, or deleted.
  • \n
  • Collisions: When two different keys land in the same bucket, Robin Hood hashing tries to "steal" a spot from a "richer" neighbor to balance search distances.
  • \n
  • Resizing: When the table fills up (load factor ~0.9), it doubles in size, and all elements are rehashed.
  • \n
\n\n

Now imagine: each call to get or insert is a full cycle: hashing, index lookup, key comparison (possibly multiple times due to collisions). Two calls = double the work. And if a resize happens between calls? Triple!

\n\n

The Solution: Entry API — Your Lifesaver

\n\n

Rust offers an elegant solution — the Entry API. This mechanism allows you to perform a search and, if needed, an insertion in a single pass. Here's what the correct code for counting words looks like:

\n\n
use std::collections::HashMap;\n\nfn count_words_efficient(words: &[&str]) -> HashMap<String, u32> {\n    let mut map = HashMap::new();\n    \n    for word in words {\n        let entry = map.entry(word.to_string());\n        *entry.or_insert(0) += 1;\n    }\n    \n    map\n}\n
\n\n

Concise, elegant, efficient. But how does this work under the hood? Let's break it down step by step.

\n\n

Step 1: The entry() Method

\n\n

The call map.entry(key) returns an Entry enum, which can be in two states:

\n\n
    \n
  • Entry::Occupied — the key already exists in the table.
  • \n
  • Entry::Vacant — the key does not exist, and we have a ready slot for insertion.
  • \n
\n\n

Important: at this

Blogs

Book Recommendations