Sitemap
Towards Dev

A publication for sharing projects, ideas, codes, and new theories.

The Systems Programmer’s Guide to the TLB for Low-Latency C++ Programming

Understand hardware virtual memory management to optimize address translation and unlock microsecond-level speedups in C++.

12 min readMay 21, 2026

--

Press enter or click to view image in full size

As a C++ systems programmer, writing fast code isn’t just about picking the right algorithms — it’s about understanding the hardware that executes your code. Today, we are going to demystify one of the most important components of your CPU: the TLB (Translation Lookaside Buffer), and see how it works alongside your L1/L2 Caches.

In this article, you will understand exactly what these hardware pieces do, the crucial difference between a Memory Page and a Cache Line, and how to write C++ code that takes full advantage of them.

The Virtual Memory Mapping And Address Translation

When you write a C++ program, the memory addresses you see are fake.

#include <iostream>

int main() {
int x = 42;
int* ptr = &x;

// This prints a "Virtual Address",
// NOT the actual physical location in RAM!
std::cout << "Address: " << ptr << std::endl;
return 0;
}

The operating system gives every program its own private Virtual Memory. However, the hardware (the physical RAM sticks in your computer) only understands Physical Addresses.

To bridge this gap, the OS maintains a massive mapping structure called the Page Table.

  • Think of the Virtual Address as a person’s name.
  • Think of the Physical Address as their GPS coordinates.
  • Think of the Page Table as a massive directory that links the two.

Note:
The Physical Map is divided into chunks of memory called Page Frames.
The Virtual Address space allocated to your program is divided into pages called Virtual Pages (Typically 4 KB in x86 but depends on architecture).

So when you try to access the ptr , the virtual address actually get mapped to the Physical address with the help of MMU(Memory Management Unit) by the CPU. The MMU translates the virtual page into a physical page frame, allowing the CPU to access the actual data stored in RAM.

So What is the problem?
The Page Table itself is stored in standard RAM. Without a hardware shortcut, translating a virtual address into a physical address could require multiple expensive memory accesses (a process called a “page walk”) before the CPU can even begin to fetch your actual data.

Modern x86 systems use multi-level page tables, so a page walk may require several sequential memory accesses before the final physical address can be resolved.

Press enter or click to view image in full size

But this doesnot happen actually. To stop the CPU from constantly doing slow page walks in RAM, hardware engineers added a highly optimized associative cache directly inside the CPU called the Translation Lookaside Buffer (TLB).

Translation Lookaside Buffer (TLB)

A TLB is a peice of hardware sits inside the CPU. This TLB cache stores a set of these mapping of Virtual pages to Physical page frames. So if a virtual page mapping is cached inside the TLB then cpu can directly get the mapped page frame address without going through the expensive page walk.

One important point to remember that confuses people: “The TLB caches page mappings, not raw data.”

When the CPU looks up an address in the Page Table, it stores that translation in the TLB. The next time it needs an address in that same memory region, it checks the TLB first.

  • TLB Hit: Mapping found! Skip the Page Table completely and go straight to fetching data.
  • TLB Miss: Mapping not found. The CPU must walk the Page Table to find the translation, which takes time, and then update the TLB.

Important Note:
For simplicity, I’ve treated the TLB as a single structure here, but in reality it is split into instruction (iTLB) and data (dTLB) paths in most modern CPUs.

Press enter or click to view image in full size

Important Nuance: A TLB miss does NOT necessarily mean a dreadfully slow fetch from physical RAM. The Page Table entries themselves are just data, which means they can be (and often are) cached in the regular L1/L2/L3 data caches! A TLB miss might just cost a few extra cycles to resolve from the L1 cache, whereas a true DRAM access costs hundreds of cycles.

The Missing Bridge: Pages vs. Cache Lines

To truly understand how to write fast C++ code, we have to clear up a common point of confusion: the difference between the TLB and the L1/L2 Caches.

Here is the golden rule to remember:

  • The TLB gives you the address.
  • The L1/L2 Cache gives you the actual data.

Simplifying further:

  • TLB answers: “Where is the memory?”
  • Cache answers: “Do I already have the memory contents?”

Conceptually, they form a pipeline. At a high level, address translation must happen before the CPU can fully resolve the cache lookup. The L1 Data Cache cannot do its job until the TLB gives it the physical address first!

Press enter or click to view image in full size

The OS and the TLB manage memory in large chunks called Pages (typically 4096 bytes/4KB). The L1/L2 hardware caches manage actual data in small chunks called Cache Lines (typically 64 bytes).

How do these fit together? Let’s look at the math:

Cache Lines per Page = 4096 Bytes/ 64 Bytes​
= 64

This means that a single TLB entry (mapping one 4KB Page) covers exactly 64 different L1 Cache lines! (Assuming the page size is 4 KB and Cache like is 64 Bytes, this varies depends on hardware so check the manual).

Press enter or click to view image in full size

Because of this relationship, conceptually, the CPU generally needs to translate the address (via the TLB) before it can fully complete the cache lookup to get your actual data. Modern CPUs use clever tricks (like virtually indexed caches and overlapping lookups) to hide this delay, but the fundamental dependency remains.

Get Sagar’s stories in your inbox

Join Medium for free to get updates from this writer.

Here is the complete memory architecture depicting how these pieces work together:

Press enter or click to view image in full size

How TLB impacts Your C++ program?

To understand how the limited size of the TLB impacts a C++ program, we have to talk about a concept called TLB Reach and the performance killer known as TLB Thrashing.

As a systems programmer, you must remember that hardware is physically constrained. A standard L1 TLB is incredibly fast, but tiny — often holding only about 64 to 128 entries.

Because each entry maps exactly one memory Page (usually 4096 bytes), we can calculate the maximum amount of memory the CPU can access instantly without triggering a page walk.

TLB Reach = Number of TLB Entries × Page Size

If your TLB has 64 entries, your TLB reach is exactly 256 KB.

If your C++ program attempts to actively work with a “Working Set” of memory that is larger than 256 KB spread across many different pages, the TLB runs out of sticky notes. It has to constantly erase old mappings to make room for new ones. If the CPU loops back and needs that old mapping again, it suffers a TLB Miss. This endless cycle of evicting and re-fetching is called TLB Thrashing.

Let’s visualize:

Press enter or click to view image in full size

To demonstrate this in C++, we will write a program that does the exact same amount of work (the same number of array lookups and additions), but we will change how many distinct memory Pages it attempts to juggle at once.

We will read one integer per page, jumping forward by exactly 4096 bytes.

#include <iostream>
#include <vector>
#include <chrono>

const int PAGE_SIZE = 4096; // Standard memory page size in bytes
const int INTS_PER_PAGE = PAGE_SIZE / sizeof(int); // 1024 integers fit in one page

// This function performs 'totalAccesses' memory reads.
// It cycles continuously through a 'workingSetPages' number of distinct memory pages.
void testTLB_Capacity(int workingSetPages, int totalAccesses) {
// Allocate enough memory to hold our working set of pages
std::vector<int> memory(workingSetPages * INTS_PER_PAGE, 0);

// Calculate how many times we need to loop to hit our totalAccesses target
int iterations = totalAccesses / workingSetPages;

auto start = std::chrono::high_resolution_clock::now();

for (int i = 0; i < iterations; ++i) {
// Loop through our working set of pages.
// We touch exactly ONE integer per page.
for (int p = 0; p < workingSetPages; ++p) {
// Indexing by (p * INTS_PER_PAGE) forces the CPU to jump exactly 4096 bytes,
// landing on a completely different memory page every single loop iteration.
memory[p * INTS_PER_PAGE]++;
}
}

auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double, std::milli> elapsed = end - start;

std::cout << "Working Set: " << workingSetPages << " Pages ("
<< (workingSetPages * 4) << " KB)\t | Time: "
<< elapsed.count() << " ms\n";
}

int main() {
// We will do exactly 10 Million memory accesses in both scenarios.
const int TOTAL_WORK = 10000000;

std::cout << "Starting TLB Capacity Benchmark...\n";
std::cout << "------------------------------------------------------\n";

// Scenario 1: Small working set.
// 32 Pages easily fits inside a standard L1 TLB (usually 64+ entries).
// The TLB grabs the mappings once and keeps them. Extremely fast!
testTLB_Capacity(32, TOTAL_WORK);

// Scenario 2: Large working set.
// 2048 Pages greatly exceeds the TLB capacity.
// By the time the inner loop finishes reading page 2047, the TLB has already
// evicted the mapping for page 0 to make room. When the outer loop restarts
// and asks for page 0 again, it suffers a TLB Miss. (TLB Thrashing!)
testTLB_Capacity(2048, TOTAL_WORK);

return 0;
}

If you compile and run this code on your machine, Scenario 2 will execute drastically slower than Scenario 1, even though the CPU did exactly 10,000,000 integer increments in both tests.

This teaches us a massive lesson about how to structure C++ programs: Memory layout matters just as much as Big-O time complexity.

If you are working with gigantic datasets (like processing images, doing scientific matrix math, or building game engines), you cannot just loop across all your data haphazardly. If you touch thousands of different memory pages at the same time, your TLB will thrash, and your performance will tank.

Instead, expert programmers use a technique called Cache Blocking (or Tiling). They break massive algorithms down so that the CPU finishes processing a small chunk of data (that easily fits inside the TLB and L1 Cache) completely, before moving on to the next chunk.

When Cache Blocking/Tilling is not Enough?

When software optimization (like Cache Blocking) isn’t enough because your program simply must access gigabytes of random memory rapidly, we can change the rules of the hardware itself by changing the size of the Memory Page. This technique is famously know as Huge Pages.

The Math Behind Huge Pages

By default, the operating system divides memory into standard pages that are 4 KB (4096bytes) in size. If we assume a standard L1 TLB has 64 entries, we have already described above, If your working set is larger than 256 KB, the TLB will start thrashing.

Huge Pages (sometimes called Large Pages) instruct the OS and CPU to group memory into much larger blocks — typically 2 MB or even 1 GB per page!

Let’s recalculate the TLB reach using 2 MB Huge Pages:

Huge Page TLB Reach = 64 entries × 2 MB per page = 128 MB

By simply telling the OS to use Huge Pages, we increased our TLB reach by a factor of 500! A single TLB entry now covers a massive 128 Megabytes of data, effectively eliminating TLB misses for most workloads.

Press enter or click to view image in full size

How to setup Huge Pages in your C++ program or using kernel paramters, is beyond the scope of this article. Will be covered in a separate post.

Let’s look at how to structure your C++ code to avoid this(Does not include Huge Pages Technique)

When your code jumps around randomly in memory, it defeats spatial locality. You trigger TLB Misses (requesting pages the TLB hasn’t mapped yet) and L1 Cache Misses (requesting data the cache hasn’t loaded yet).

Let’s look at how to structure your C++ code to avoid this.

Contiguous vs Scattered Memory:
Data structures that keep items physically next to each other in memory are much faster.

#include <iostream>
#include <vector>

// Nodes allocated one by one via `new` are often scattered across many
// virtual memory pages and cache lines.
struct Node {
int data;
Node* next;
};

// FAST: TLB Friendly (Contiguous Array)
void processArray(const std::vector<int>& dataArray) {
long long sum = 0;

// std::vector allocates memory sequentially.
// A single TLB hit provides the mapping for an entire 4096-byte Page.
// The CPU can process 1024 integers (4 bytes each) rapidly before
// it ever needs to ask the TLB for a new page mapping!
for (size_t i = 0; i < dataArray.size(); ++i) {
sum += dataArray[i];
}
}

// SLOW: TLB Unfriendly (Scattered Nodes)
void processLinkedList(Node* head) {
long long sum = 0;
Node* current = head;

// Following pointers means each `current->next` could easily reside
// on a completely different 4096-byte Page. This scattered access
// defeats hardware prefetchers and greatly increases TLB misses.
while (current != nullptr) {
sum += current->data;
current = current->next;
}
}

The Stride Problem:
Even if you use a contiguous std::vector, how you iterate over it matters. Skipping over memory creates a worst-case scenario for both the TLB and the L1 Cache.

#include <iostream>
#include <vector>

const size_t SIZE = 10 * 1024 * 1024; // 10 Million integers

void runFast(const std::vector<int>& data) {
long long sum = 0;

// Stride of 1: Sequential access.
// Hardware prefetchers easily recognize this pattern. Both the TLB
// and L1 Cache are extremely effective here.
for (size_t i = 0; i < SIZE; i++) {
sum += data[i];
}
}

void runTerriblySlow(const std::vector<int>& data) {
long long sum = 0;

// Stride of 1024: Skipping 1024 integers (4096 bytes) per loop.
// Since an integer is 4 bytes, 1024 * 4 = 4096 bytes. We are skipping
// forward by exactly one full Memory Page on every single iteration!
for (size_t i = 0; i < SIZE; i += 1024) {
sum += data[i];

// WHAT HAPPENS IN HARDWARE HERE?
// 1. We jump to a new 4KB Page, vastly increasing the chance of a TLB Miss.
// 2. We skip over 63 out of 64 Cache Lines in that page, wasting RAM bandwidth.
// Result: The CPU's prefetchers struggle to keep up, often resulting in
// severe stalls while waiting on the memory subsystem.
}
}

The Golden Rule for High Performance

When performance truly matters, remember how the hardware views your data:

  1. Prefer Flat Arrays: Use std::vector or std::array instead of node-based containers like std::list or complex trees.
  2. Access Sequentially: Process data strictly from start to finish (data[0], data[1], data[2]).
  3. Respect the Hierarchy: A single TLB entry maps an entire 4096-byte region, and a single L1 entry caches a 64-byte chunk. Maximize the work you do within those boundaries before moving on.

By feeding data to the CPU in a predictable, straight line, you let the TLB efficiently map your addresses and the L1 Cache easily fetch your data, unlocking the true speed of your machine!

Closing Thoughts

Once you start thinking in terms of pages, cache lines, and TLB reach, performance in C++ becomes far more predictable. Most “random slowdowns” are really just your working set drifting outside what the hardware can efficiently translate and cache.

The core idea is simple: keep your data layout and access patterns aligned with how the CPU actually moves through memory, and it spends more time computing and less time waiting.

These kind of thinking is What really separates a normal C++ application programmer from a Systems or Low-latency C++ engineer.

At that level, performance is no longer an afterthought or a set of micro-optimizations. It becomes a primary design constraint, where understanding the CPU is just as important as understanding the language.

That shift is what turns “working code” into “fast code.”

I’ve skipped the details of how Huge Pages are implemented in C++ and at the OS level — that will be covered in a separate post.

Found this article helpful? Please Clap 👏 and follow for more C++ and system programming content.

--

--