DEV Community

Cover image for Why Can Two Threads Writing to Different Variables Still Slow Each Other Down?
Aditya Sharma
Aditya Sharma

Posted on

Why Can Two Threads Writing to Different Variables Still Slow Each Other Down?

Here's a situation that shouldn't be a problem.

You have two threads. Each one updates its own counter. They never read each other's variable. They never write to the same memory location. By every rule of concurrent programming you've learned, these threads are completely independent.

struct Counters {
    int counterA;
    int counterB;
};
Enter fullscreen mode Exit fullscreen mode

Thread A hammers counterA. Thread B hammers counterB. No locks needed. No shared state. Should be fast.

Sometimes it isn't. And the reason has nothing to do with your code.


The CPU Doesn't Think in Variables

To understand why, you need to know something about how CPUs actually access memory.

Your CPU is significantly faster than RAM. If it had to fetch a value from main memory every time it needed one, it would spend most of its time waiting. To avoid this, CPUs have small, fast caches sitting between the processor and RAM.

CPU Core
↓
L1 Cache   (very fast, very small)
↓
L2 Cache
↓
L3 Cache
↓
RAM        (slow, large)
Enter fullscreen mode Exit fullscreen mode

Frequently accessed data lives in these caches so the CPU doesn't have to reach all the way out to RAM every time.

Here's the important part: CPUs don't move individual variables in and out of cache. They move fixed-size blocks of memory called cache lines. A cache line is typically 64 bytes on modern hardware. When the CPU needs a variable, it loads the entire cache line containing that variable, not just the variable itself.

So if counterA and counterB are sitting next to each other in memory, which they are in that struct, they almost certainly occupy the same cache line.

Cache line (64 bytes)
┌──────────────────────────────────┐
│  counterA  │  counterB  │  ...   │
└──────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

From the CPU's perspective, these two variables aren't separate things. They're two parts of the same block.


Now Put Two Cores in the Picture

Modern CPUs have multiple cores, and each core typically has its own L1 and L2 cache. When two cores are both working with the same data, the CPU has to keep their caches consistent. This is called cache coherence.

Here's where the problem appears.

Core 1 is running Thread A and modifying counterA. Core 2 is running Thread B and modifying counterB. Both variables share a cache line, so both cores have a cached copy of that line.

When Core 1 writes to counterA, it modifies its copy of the cache line. The CPU's coherence mechanism now has to deal with the fact that Core 2 has a copy of the same line that no longer reflects what Core 1 just wrote. Core 2's copy needs to be invalidated or updated before Core 2 can safely use that line again.

Then Core 2 writes to counterB. Now Core 1's copy is stale.

Then Core 1 writes to counterA again.

And so on.

Even though neither thread cares about the other's variable, the two cores end up repeatedly competing for ownership of the same cache line. Each write forces a coherence operation. The cache line bounces back and forth. This coordination overhead is what kills performance.


Why "False" Sharing?

True sharing is when multiple threads actually access the same data. You'd expect synchronization overhead there.

False sharing is different. The threads aren't accessing the same data at all. They just happen to be accessing data that lives in the same cache line. The sharing is invisible at the source code level. There's nothing in the program that suggests these variables are related. But the hardware treats them as part of the same unit, and that's enough.

Programmer sees:

Thread A → counterA     (independent)
Thread B → counterB     (independent)


CPU cache sees:

┌──────────────────────────────────┐
│  counterA  │  counterB  │  ...   │
└──────────────────────────────────┘
                ↑
          same cache line
          same coherence unit
Enter fullscreen mode Exit fullscreen mode

The program has no sharing. The cache does.

That's the false in false sharing.


Does It Actually Matter?

It depends on the access pattern. If both threads are writing to their respective variables frequently and concurrently, the coherence traffic can become a real bottleneck. The more cores involved, the worse it can get. If the threads rarely update their counters, or if they run on the same core, you might not notice anything.

The point isn't that false sharing always destroys performance. It's that it can, and the source code gives you no warning that it's happening.


Fixing It

The fix is conceptually simple: put the variables on different cache lines so they don't interfere.

One way to do this is padding. Add enough unused bytes after each variable to push the next one onto a new cache line.

Before:
┌─────────────────────────────────────┐
│  counterA  │  counterB  │  ...      │
└─────────────────────────────────────┘

After:
┌─────────────────────────────────────┐
│  counterA  │  padding               │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│  counterB  │  padding               │
└─────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Now each counter occupies its own cache line. Core 1 can write to counterA all day without touching anything Core 2 cares about.

Some languages and libraries provide explicit alignment or annotation tools to handle this. The general idea is the same across all of them: keep frequently written data belonging to different threads physically separated in memory.

It's one of those fixes that looks like a hack, adding meaningless padding bytes to a struct, but is actually addressing a real hardware constraint.


The Bigger Point

You write code in terms of variables. The CPU operates in terms of cache lines. Most of the time that gap doesn't matter. But occasionally it produces a situation like this: two threads modifying two completely unrelated variables, slowing each other down because the hardware groups them together.

False sharing is a good example of a broader pattern in systems programming. Performance problems often live at the boundary between the abstraction you're working in and the layer underneath it. The program looks fine. The hardware sees something different.

Your variables are independent. The cache line they live on isn't.

Top comments (0)