We use hash tables to track the counts of the elements in O(1) time. Thats beautiful when you are trying to count the word frequencies in a large text file or dealing with unicode characters.


In these examples you can't know the boundaries of the data that you are dealing with. But if we know the data boundary, for example if you are trying to count the letter frequency in a string, using an unordered_map<char,int> works in O(1) time complexity, simply using an array consisting of 26 elements would work better. When using unordered_map, the CPU runs a hash function, computes modulo arithmetic against bucket count, and resolves collisions across linked lists. Plus, every unique key triggers a separate heap allocation (new Node), increasing heap fragmentation. On the other hand, std::array<int, 26> lives entirely on the stack, indexing costs few CPU cycles.



this code snippet simply increments the character c's count stored in a fixed array consisting 26 elements. The c - 'A' part is to calculate the offset from the ASCII value. (count of the letter A is in the count[0]...)

void increment_array(std::array<int, 26>& count, char c) {
  count[c - 'A']++;
}


and this is the x86_x64 assembly output of the snippet:

increment_array(std::array<int, 26>&, char): 
movsx rsi, sil ; // Extend the c into 64-bit int. 
add DWORD PTR [rdi-260+rsi*4], 1 ; // adding +1 to the value in memory (++ operation)
ret

0 function call, 0 branching , everything done in just a couple of CPU cycles.



and this is the unordered_map version:

void increment_map(std::unordered_map<char, int>& count, char c) {
  count[c]++;
}


compiled into x86 assembly output:


increment_map(std::unordered_map<char, int>&, char):
        // --- [1. SETUP & REGISTER BACKUPS] ---
        sub     rsp, 56                         // Allocate 56 bytes of stack space
        mov     r8, QWORD PTR [rdi+8]           // r8 = map._M_bucket_count (number of buckets)
        xor     edx, edx                        // edx = 0 (prepare high 32 bits for division)
        mov     QWORD PTR [rsp+24], rbp         // Save callee-saved registers to stack
        movsx   rbp, sil                        // rbp = sign-extended char 'c'
        mov     rax, rbp                        // rax = hash (for char, hash is the char value itself)
        mov     QWORD PTR [rsp+16], rbx         // Save rbx
        mov     rbx, rdi                        // rbx = this pointer (&map)
        mov     QWORD PTR [rsp+40], r13         // Save r13
        mov     QWORD PTR [rsp+32], r12         // Save r12

        // --- [2. COMPUTE BUCKET INDEX] ---
        div     r8                              // rax = rax / r8, rdx = rax % r8 (rdx = bucket_index)
        mov     rax, QWORD PTR [rdi]            // rax = map._M_buckets (pointer to bucket array)
        mov     r13, rbp                        // r13 = target char 'c'
        mov     r11, QWORD PTR [rax+rdx*8]      // r11 = buckets[rdx] (head node of this bucket)
        lea     r10, [0+rdx*8]                  // r10 = bucket byte offset (rdx * 8)
        test    r11, r11                        // Check if bucket is empty (null pointer)
        je      .L41                            // If empty -> jump to allocate new node

        // --- [3. TRAVERSE LINKED LIST (SEARCH KEY)] ---
        mov     rsi, QWORD PTR [r11]            // rsi = node pointer
        mov     r12, rdx                        // r12 = current bucket index
        movzx   edi, BYTE PTR [rsi+8]           // edi = node->key (char at offset +8)
        cmp     dil, r13b                       // Compare node->key == 'c'
        je      .L3                             // If match found -> jump to value increment (.L3)

.L43:
        mov     rcx, QWORD PTR [rsi]            // rcx = node->_M_nxt (next node pointer)
        test    rcx, rcx                        // End of the entire linked list?
        je      .L41                            // If null -> key not found, go allocate
        movsx   rax, BYTE PTR [rcx+8]           // rax = next_node->key
        xor     edx, edx                        // edx = 0
        mov     r11, rsi                        // r11 = current node
        mov     rdi, rax                        // rdi = next_node->key
        div     r8                              // Re-hash: next_node->key % bucket_count
        cmp     r12, rdx                        // Is the next node still in the same bucket?
        jne     .L41                            // If bucket index changed -> key not found in bucket
        mov     rsi, rcx                        // Advance to next node
        cmp     dil, r13b                       // Compare next_node->key == 'c'
        jne     .L43                            // If still not equal -> continue loop

.L3:
        // --- [4. FAST PATH: KEY FOUND] ---
        mov     rax, QWORD PTR [r11]            // rax = node pointer
        lea     r12, [rax+12]                   // r12 = &node->value (int at offset +12)
        test    rax, rax                        // Safety check
        je      .L41                            // Branch if null

.L5:
        // --- [5. INCREMENT VALUE & RETURN] ---
        add     DWORD PTR [r12], 1              // ++(node->value) -> PERFORM INCREMENT
        mov     rbx, QWORD PTR [rsp+16]         // Restore callee-saved registers
        mov     rbp, QWORD PTR [rsp+24]
        mov     r12, QWORD PTR [rsp+32]
        mov     r13, QWORD PTR [rsp+40]
        add     rsp, 56                         // Deallocate stack frame
        ret                                     // Return from function

.L41:
        // --- [6. SLOW PATH: ALLOCATE NEW NODE] ---
        mov     edi, 16                         // Argument: sizeof(Node) = 16 bytes
        mov     QWORD PTR [rsp+48], r14         // Save r14
        mov     QWORD PTR [rsp+8], r10          // Save bucket byte offset
        mov     QWORD PTR [rsp], r8             // Save bucket count
        call    operator new(unsigned long)     // rax = heap address of new node

        // Initialize new node fields:
        mov     BYTE PTR [rax+8], r13b          // node->key = 'c'
        mov     QWORD PTR [rax], 0              // node->_M_nxt = nullptr
        mov     DWORD PTR [rax+12], 0           // node->value = 0 (default initialized)
        mov     r12, rax                        // r12 = new node address

        // Check if table needs resizing (Load Factor check):
        mov     rdx, QWORD PTR [rbx+24]         // rdx = map._M_element_count
        mov     rsi, QWORD PTR [rsp]            // rsi = bucket_count
        lea     rdi, [rbx+32]                   // rdi = &map._M_rehash_policy
        mov     ecx, 1                          // Adding 1 element
        mov     r14, QWORD PTR [rbx+40]
        call    std::__detail::_Prime_rehash_policy::_M_need_rehash
        test    al, al                          // Does it need rehash?
        jne     .L6                             // If yes -> jump to resize table (.L6)

        // --- [7. INSERT NODE INTO BUCKET (NO REHASH)] ---
        mov     r8, QWORD PTR [rbx]             // r8 = map._M_buckets
        mov     r10, QWORD PTR [rsp+8]          // r10 = bucket offset
        add     r10, r8                         // r10 = &buckets[bucket_index]
        mov     rax, QWORD PTR [r10]            // rax = existing bucket head
        test    rax, rax
        je      .L18                            // If empty -> insert as first element in bucket

.L46:
        // Splice new node into bucket's existing list
        mov     rax, QWORD PTR [rax]
        mov     QWORD PTR [r12], rax            // new_node->next = old_head->next
        mov     rax, QWORD PTR [r10]
        mov     QWORD PTR [rax], r12            // old_head->next = new_node

.L19:
        add     QWORD PTR [rbx+24], 1           // map._M_element_count++
        mov     r14, QWORD PTR [rsp+48]         // Restore r14
        add     r12, 12                         // r12 = &new_node->value (+12 offset)
        jmp     .L5                             // Jump to increment value and return

.L6:
        // --- [8. REHASH TABLE (RESIZE & RE-BUCKET)] ---
        mov     r13, rdx                        // r13 = new bucket count
        cmp     rdx, 1
        je      .L44
        mov     rax, rdx
        shr     rax, 60
        jne     .L45                            // Overflow check -> throw exception
        lea     rdx, [0+rdx*8]                  // rdx = new bucket array size in bytes
        mov     rdi, rdx                        // rdi = size
        mov     QWORD PTR [rsp], rdx
        call    operator new(unsigned long)     // Allocate new bucket array
        mov     rdx, QWORD PTR [rsp]            // rdx = size
        xor     esi, esi                        // esi = 0
        mov     rdi, rax                        // rdi = new bucket array address
        call    memset                          // Zero-initialize new bucket array
        lea     r10, [rbx+48]
        mov     r8, rax                         // r8 = new bucket array pointer

.L9:
        // Traverse old nodes and redistribute them into new buckets
        mov     rsi, QWORD PTR [rbx+16]         // rsi = map._M_before_begin._M_nxt
        mov     QWORD PTR [rbx+16], 0
        test    rsi, rsi
        je      .L12                            // If empty -> jump to cleanup
        xor     edi, edi
        lea     r9, [rbx+16]
        jmp     .L16

.L13:
        mov     rdx, QWORD PTR [r11]
        mov     QWORD PTR [rcx], rdx
        mov     rax, QWORD PTR [rax]
        mov     QWORD PTR [rax], rcx
        test    rsi, rsi
        je      .L12

.L16:
        // Loop: recompute hash % new_bucket_count for each existing node
        mov     rcx, rsi
        xor     edx, edx
        mov     rsi, QWORD PTR [rsi]
        movsx   rax, BYTE PTR [rcx+8]           // rax = node->key
        div     r13                             // node->key % new_bucket_count
        lea     rax, [r8+rdx*8]                 // rax = &new_buckets[new_idx]
        mov     r11, QWORD PTR [rax]
        test    r11, r11
        jne     .L13
        mov     r11, QWORD PTR [rbx+16]
        mov     QWORD PTR [rcx], r11
        mov     QWORD PTR [rbx+16], rcx
        mov     QWORD PTR [rax], r9
        cmp     QWORD PTR [rcx], 0
        je      .L14
        mov     QWORD PTR [r8+rdi*8], rcx

.L14:
        mov     rdi, rdx
        test    rsi, rsi
        jne     .L16                            // Repeat for all remaining nodes

.L12:
        // Free the old bucket array
        mov     rdi, QWORD PTR [rbx]            // rdi = old bucket array pointer
        cmp     r10, rdi
        je      .L17                            // Don't delete if it was single stack node
        mov     rax, QWORD PTR [rbx+8]          // rax = old bucket count
        mov     QWORD PTR [rsp], r8
        lea     rsi, [0+rax*8]                  // rsi = old array size in bytes
        call    operator delete(void*, unsigned long) // Free old bucket memory
        mov     r8, QWORD PTR [rsp]

.L17:
        // Update map metadata with new buckets
        mov     rax, rbp                        // rax = current key 'c'
        xor     edx, edx
        mov     QWORD PTR [rbx+8], r13          // map._M_bucket_count = new_bucket_count
        div     r13                             // Compute bucket for our new node
        mov     QWORD PTR [rbx], r8             // map._M_buckets = new_buckets
        lea     r10, [0+rdx*8]
        add     r10, r8                         // r10 = &new_buckets[index]
        mov     rax, QWORD PTR [r10]
        test    rax, rax
        jne     .L46

.L18:
        // Link new node as the head of bucket
        mov     rax, QWORD PTR [rbx+16]
        mov     QWORD PTR [rbx+16], r12
        mov     QWORD PTR [r12], rax
        test    rax, rax
        je      .L20
        movsx   rax, BYTE PTR [rax+8]
        xor     edx, edx
        div     QWORD PTR [rbx+8]
        mov     QWORD PTR [r8+rdx*8], r12

.L20:
        lea     rax, [rbx+16]
        mov     QWORD PTR [r10], rax
        jmp     .L19                            // Return back to increment value

.L45:
        // Exception handling: array size overflow
        shr     r13, 61
        je      .L11
        call    std::__throw_bad_array_new_length()

.L44:
        lea     r8, [rbx+48]
        mov     QWORD PTR [rbx+48], 0
        mov     r10, r8
        jmp     .L9

.L11:
        // Exception handling: out of memory
        call    std::__throw_bad_alloc()


compile here with -O2 opt


briefly, it computes the bucket index and traverses the linked list to find the key, if found, increments the value of the key c, if not found, the code must branch into heap allocation via operator new to create and initialize a 16-byte node, followed by splicing it into the bucket's list. Finally, it must evaluate the container's load factor. if the table has exceeded its capacity threshold, the assembly executes a full rehash routine, which entails allocating a larger bucket array, re-distributing every existing node via modulo operations, and deallocating the old bucket buffer. (We will dive into the low-level details of hashing algorithms and collision resolution in a follow-up post.)


TLDR: If you know the bounds of your key set in advance, use a fixed-size array instead of a hash map. You get O(1) space complexity, eliminate dynamic heap allocations entirely.