Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Assembly (x86-64, AArch64, RISC-V)  Module 13 – Atomics and memory ordering

Caches, cache lines and false sharing

Time a row walk against a column walk on Apple silicon, read the cache line size, and see why threads slow down when their data share a line.

  • Intermediate
  • 15 minutes
  • Examples run with Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 5 of 15 minutes free
Free: A cache moves memory in whole lines, and the system reports their size · A walk in memory order uses every byte of each line · Practise. With a pass: 3 in-depth parts and 3 interview answers, about 10 minutes.

What you will learn

  • Explain how a cache moves memory in lines, and find the line size a system reports
  • Predict which of two loops over the same data runs faster from how each uses cache lines
  • Detect false sharing between threads and remove it with padding and alignment
  • Compare a prefetched walk in memory order with a pointer-chasing walk of the same nodes
  • Find the cache line size on macOS, Linux, x86-64 and AArch64

Before you start

What you’ll learn · Before you start

What you will learn

  • Explain how a cache moves memory in lines, and find the line size a system reports
  • Predict which of two loops over the same data runs faster from how each uses cache lines
  • Detect false sharing between threads and remove it with padding and alignment
  • Compare a prefetched walk in memory order with a pointer-chasing walk of the same nodes
  • Find the cache line size on macOS, Linux, x86-64 and AArch64

Before you start

On this page

matrix.c adds up the same 16,777,216 numbers twice and gets the same total both times, yet on an Apple silicon Mac under macOS, best of five trials each, the column-by-column walk took many times as long as the row-by-row one. Nothing differs but the order of the loads. A cache: A small, fast memory near a core that keeps copies of recently used memory, so that later loads of the same or nearby addresses need no trip to main memory. keeps recently used memory close to the core, and it brings memory in whole cache line: The fixed-size, aligned block in which a cache stores and moves memory. Sizes differ between processors and cache levels: Intel gives 64 bytes for its Core processors. A miss brings in the whole line.: a load that walks memory in order uses every byte it brought in, and one that jumps uses a sliver of each line.

Threads feel the same unit. Two threads that write different variables in one line slow each other down, which is called false sharing: A slowdown in which threads write different variables that happen to lie in the same cache line, so the line moves between their cores although no variable is shared.: only one core at a time may write a line, so the line moves back and forth between their cores. Every run below is native.

A cache moves memory in whole lines, and the system reports their size

Each cache moves memory in cache lines of its own fixed size. macOS reports one line size and the cache sizes through sysctl, which Apple documents for exactly this:

The line size and the cache sizes the host reports Python · lines_session.py
"""Asks macOS for the cache line size and the cache sizes of the two kinds of core."""

import pathlib
import sys

sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session  # noqa: E402

session.run("""
# The line size, the L1 data cache of each kind of core (perflevel0 is the faster kind, perflevel1 the other),
# and the L2 cache of the perflevel0 cores, all in bytes
/usr/sbin/sysctl hw.cachelinesize hw.perflevel0.l1dcachesize hw.perflevel1.l1dcachesize hw.perflevel0.l2cachesize
""")

Output

# The line size, the L1 data cache of each kind of core (perflevel0 is the faster kind, perflevel1 the other),
# and the L2 cache of the perflevel0 cores, all in bytes
$ /usr/sbin/sysctl hw.cachelinesize hw.perflevel0.l1dcachesize hw.perflevel1.l1dcachesize hw.perflevel0.l2cachesize
hw.cachelinesize: 128
hw.perflevel0.l1dcachesize: 131072
hw.perflevel1.l1dcachesize: 65536
hw.perflevel0.l2cachesize: 16777216

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 lines_session.py

hw.cachelinesize is 128, the size Apple calls the system cache line: on a miss, a cache with lines of that size fetches the aligned 128 bytes around the address, not just the 4 or 8 the load asked for. Apple’s open-source kernel reads that number from the cache beyond the L1, and lists 64-byte lines for the L1 data caches of the cores it names. Apple numbers the host’s two kinds of core from the fastest: a perflevel0 core has a 131,072-byte L1 data cache, a perflevel1 core half that, and the L2 of the perflevel0 cores holds 16,777,216 bytes. Intel’s manual gives 64-byte lines for the L1 and L2 caches of its Core and Atom processors.

A walk in memory order uses every byte of each line

matrix.c fills a 4096 x 4096 matrix of int, 64 MiB, and sums it both ways at -O1, so that neither loop is vectorized and each does the same work per element. C stores a matrix row by row, so m[i][j] and m[i][j + 1] are 4 bytes apart while m[i][j] and m[i + 1][j] are 16,384 bytes apart:

matrix.c: one matrix, two walks, best of 5 trials each C · matrix.c
// matrix.c: sums one 4096 x 4096 matrix of int twice, row by row and column by column, and times each walk.
// Both walks add the same 16,777,216 numbers, so they print the same sum; only the order of the loads differs.
// Built with -O1, so neither loop is vectorized and the two loops do the same work per element.
#include <stdio.h>
#include <time.h>

#define N 4096

static int m[N][N];  // 64 MiB, four times the largest cache the host reports

static double now_ms(void) {
    struct timespec t;
    clock_gettime(CLOCK_MONOTONIC, &t);
    return t.tv_sec * 1e3 + t.tv_nsec / 1e6;
}

// Consecutive elements: m[i][j] and m[i][j + 1] are 4 bytes apart.
static long by_rows(void) {
    long sum = 0;
    for (int i = 0; i < N; i++)
        for (int j = 0; j < N; j++) sum += m[i][j];
    return sum;
}

// One element per row: m[i][j] and m[i + 1][j] are 4096 x 4 = 16,384 bytes apart.
static long by_columns(void) {
    long sum = 0;
    for (int j = 0; j < N; j++)
        for (int i = 0; i < N; i++) sum += m[i][j];
    return sum;
}

int main(void) {
    for (int i = 0; i < N; i++)
        for (int j = 0; j < N; j++) m[i][j] = (i + j) % 10;
    double best_rows = 1e30, best_columns = 1e30;
    long sum_rows = 0, sum_columns = 0;
    for (int trial = 0; trial < 5; trial++) {  // the best of 5 trials, as the machine is shared
        double t0 = now_ms();
        sum_rows = by_rows();
        double t1 = now_ms();
        sum_columns = by_columns();
        double t2 = now_ms();
        if (t1 - t0 < best_rows) best_rows = t1 - t0;
        if (t2 - t1 < best_columns) best_columns = t2 - t1;
    }
    printf("matrix: %d x %d int, %zu bytes\n", N, N, sizeof m);
    printf("row by row:       sum %ld, best of 5: %.1f ms\n", sum_rows, best_rows);
    printf("column by column: sum %ld, best of 5: %.1f ms\n", sum_columns, best_columns);
    printf("column by column took %.1f times as long\n", best_columns / best_rows);
    return 0;
}
The row walk and the column walk, timed Python · matrix_session.py
"""Builds matrix.c without vectorization and times a row-by-row and a column-by-column sum of the same 64 MiB."""

import pathlib
import sys

sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session  # noqa: E402

session.run("""
clang -O1 -o matrix matrix.c
./matrix
""")

Output

$ clang -O1 -o matrix matrix.c
$ ./matrix
matrix: 4096 x 4096 int, 67108864 bytes
row by row:       sum 75497480, best of 5: 4.1 ms
column by column: sum 75497480, best of 5: 48.1 ms
column by column took 11.6 times as long

This output changes from run to run: The times change from run to run and with the load of the machine; the sums and the sizes do not.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 matrix_session.py

The row walk has spatial locality: The tendency of a program to use memory near memory it has just used, such as the next element of a row. It lets one cache line serve many loads.: once a 128-byte line has come in, the next 31 loads find their ints in it. The column walk moves 16,384 bytes per load, so each load needs a new line, and one column touches 4,096 lines, 512 KiB, more than the 128 KiB L1 of a perflevel0 core can hold. Both walks need the same cache lines, but the column walk fetches each into the L1 many times over: by the time it returns for the next column, those lines have left the L1:

Interactive · message by message Row by row and column by column through 128-byte lines Step through the first loads of each walk of matrix.c, and count the 128-byte lines each walk brings in from memory.
Row by row and column by column through 128-byte lines, step 6 of 6. Messages so far: memory to core and caches: line of 32 ints; memory to core and caches: next line; memory to core and caches: line of 32 ints; memory to core and caches: a line per row; memory to core and caches: same line again.core and cachesup to 16.8 Mmemory16 KiB strideline of 32 intsnext lineline of 32 intsa line per rowsame line again

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The first load misses. The row walk loads m[0][0]. Its line is not in the caches, so the whole 128-byte line that holds it comes in from memory: m[0][0] to m[0][31].
  2. The next 31 loads hit. m[0][1] to m[0][31] are in that same line, 4 bytes apart, so they come from the caches without another trip to memory.
  3. One fetch per 32 ints. m[0][32] starts the next line. The row walk brings each line in once and uses all of it: 16,777,216 ints / 32 = 524,288 line fetches for the matrix.
  4. The column walk starts. The column walk also starts with m[0][0] and fetches its line, but uses one int of the 32 before it moves to m[1][0].
  5. Each load is a new line. m[1][0] is 16,384 bytes further on, in another line, and so is every element below it. One column touches 4,096 lines: 512 KiB, more than the 128 KiB L1 of a perflevel0 core can hold.
  6. The lines have left the L1. When the walk returns for m[0][1], its line has long been pushed out of the L1 to make room. In the worst case every load fetches a line again: 16,777,216 fetches, 32 times the row walk.

Key idea

Memory reaches the core a line at a time. A loop is fast when consecutive loads fall in the same line, and slow when every load needs a line of its own.

Three questions go further:

  • False sharing slows threads that write separate variables in one line, and padding each variable to its own line removes it.
  • A hardware prefetcher: A part of the processor that watches the addresses of a program's loads and fetches the lines it predicts will be needed next, before the loads ask for them. fetches lines ahead of a walk in memory order, which pointer chasing: Walking a data structure by loading an address from memory and then loading through that address, as a linked-list walk does at every node. More in “Structs, arrays and linked lists in assembly” through a shuffled list defeats.
  • Each system reports the line size its own way: sysctl on macOS, sysfs on Linux, CPUID or CTR_EL0 on the processor.

Checkpoint

Check: Caches and cache lines · Walking memory in order

  1. Question 1 of 2 A loop reads 4-byte ints one after another from the start of a 128-byte-aligned array. How many of its loads does one 128-byte line serve?

    Type a number.

    Show the answer: Question 1 of 2

    Answer: 32

    128 / 4 = 32. The first load of each line misses and brings the whole line in, and the next 31 find their ints already in the cache, which is why the row walk of matrix.c was fast.

  2. Question 2 of 2 In the column walk of matrix.c, how far apart in memory are two consecutive loads, and what does that mean for the cache?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: 16,384 bytes, a whole row, so each load needs a different line

    One column touches 4,096 lines, 512 KiB, more than the 128 KiB L1 of a perflevel0 core can hold, so the lines have left the L1 before the next column needs them again.

Practise

Three tasks on matrix.c. Build with clang -O1 -o matrix matrix.c and run ./matrix.

  1. Change int to long long in the matrix and in nothing else. Work out how many elements now share a 128-byte line, predict whether the gap between the walks grows or shrinks, then run it.
  2. Swap the two loops of by_columns so that it walks row by row, and check that its time falls to the row walk’s.
  3. Work out how many lines one column of a 4096 x 4096 matrix of char touches, and whether they fit in a perflevel0 core’s L1.

Practice that adapts to you

Practise this lesson

8 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 8 What does a cache bring in when a load misses?

    Choose one answer.

    Show the answer: Question 1 of 8

    Answer: The whole aligned line that holds the address, at that cache's own line size

    hw.cachelinesize reported 128 on the host, and Intel's manual gives 64 for its Core and Atom processors.

  2. Question 2 of 8 The L2 of the host's perflevel0 cores holds 16,777,216 bytes, and macOS reports 128-byte lines. How many lines of that size fit in it?

    Type a number.

    Show the answer: Question 2 of 8

    Answer: 131072 lines

    16,777,216 / 128 = 131,072. A cache holds its size divided by its line size, so a walk that touches more distinct lines than that before it comes back cannot find the first ones still there.

  3. Question 3 of 8 The same program runs on the Apple silicon host of the runs and on an Intel Core processor. How does the line size macOS reports on the host compare with the L1 and L2 lines Intel's manual gives?

    Choose one answer.

    Show the answer: Question 3 of 8

    Answer: 128 bytes reported on the host, 64 bytes in Intel's manual

    A loop over 4-byte ints therefore uses 32 elements of each 128-byte line and 16 of each 64-byte line.

  4. Question 4 of 8 A loop reads one byte from every 128th byte of a large array. With 128-byte lines, the size macOS reports on the host, how much of each line it fetches does it use?

    Choose one answer.

    Show the answer: Question 4 of 8

    Answer: One byte of the 128 in each line, so it fetches 128 times the data it uses

    A stride of a whole line or more wastes most of every fetch, the same effect the column walk had with its stride of a whole row.

  5. Question 5 of 8 Which walk of matrix.c was faster on the Apple silicon host, best of five trials each, and why?

    Choose one answer.

    Show the answer: Question 5 of 8

    Answer: Row by row, because consecutive loads fall in the same line

    This is spatial locality: data used together should sit together in memory.

  6. Question 6 of 8 Practise task 2 swaps the two loops of by_columns. What happens to its time, and why?

    Choose one answer.

    Show the answer: Question 6 of 8

    Answer: It falls to the row walk's, because the inner loop now moves 4 bytes per load

    Loop interchange is the fix for a column walk over a row-major matrix; the slow column walk of the run shows that clang at -O1 kept the loops in the order they were written.

  7. Question 7 of 8 With long long instead of int, how many elements share a 128-byte line, and what happens to the row walk's advantage?

    Choose one answer.

    Show the answer: Question 7 of 8

    Answer: 16 per line, so the row walk gets 15 hits per miss instead of 31 and its advantage shrinks

    The column walk still needs a line per load, so it stays slow; the row walk does more fetches than before.

  8. Question 8 of 8 An image filter stores pixels row by row but processes the image one column at a time. What change keeps its results and speeds it up?

    Choose one answer.

    Show the answer: Question 8 of 8

    Answer: Process the image row by row, or store it column by column for this filter

    Match the order of the loops to the order of the data in memory, and the cache serves most loads from lines it already holds.

False sharing measured, and the padding that removes it Pro: opens with a Pro, Premium or Ultimate pass

Two threads that never touch each other’s variables can still slow each other down when those variables share a cache line. The run below shows the cost on two private counters, and the alignment that removes it.

In this part:

  • Reproducing false sharing with two private counters
  • Padding each counter to its own line

2 runnable examples (C, Python) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Prefetchers and pointer chasing, measured on the same nodes Pro: opens with a Pro, Premium or Ultimate pass

A walk in memory order is fast for a second reason besides whole lines: the processor notices the pattern and fetches lines before the loads ask for them. A linked list in shuffled order defeats that, with the same nodes and the same work per node.

In this part:

  • Timing the same nodes in memory order and in a shuffled order
  • Making a linked structure kinder to the cache

2 runnable examples (C, Python) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Finding the cache line size on each system, and the read that macOS refuses Pro: opens with a Pro, Premium or Ultimate pass

Padding to a cache line needs the line size of the machine the code runs on, and every system reports it somewhere else. Here are the four places to ask, including the processor register that a macOS program is not allowed to read.

In this part:

  • Reading the Cache Type Register from a program, and why macOS answers with SIGILL
  • The supported query on each system

2 runnable examples (Python, ctr.c, ctr.s) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

Why does summing a large C matrix row by row run faster than column by column?

C stores a matrix row by row, so the elements of a row are next to each other in memory, and the elements of a column are a whole row apart. A cache brings memory in whole lines, 64 bytes on many x86-64 processors and 128, as macOS reports it, on the Apple silicon host of the runs above, so a row walk uses every element of each line it fetches: for 4-byte ints, one miss serves 32 loads on a 128-byte line. A column walk jumps a row’s length on every load, so each load needs a line of its own, and in a large matrix those lines have left the cache before the walk comes back for the next column. Both walks do the same additions and give the same sum.

Three harder questions build on this one: counters that refuse to scale, a list against an array of the same records, and padding chosen for two kinds of processor.

  1. Mid to senior

    A server keeps one request counter per worker thread in an array, long counts[16], indexed by thread number, so that no two threads ever write the same counter. Adding threads makes it slower, not faster. Explain why, show how you would confirm it, and fix it.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A linked list and an array hold the same two million records, and a loop sums one field of each. Both loops are O(n), yet the list's loop is many times slower. Explain the gap in terms of cache lines and loads, and say when a list can be made fast.

    Model answer, Pro. First step and follow-ups

  3. Senior to staff

    A library pads per-thread data to avoid false sharing and must run on x86-64 Linux servers and on Apple silicon Macs. How do you choose the padding at compile time, how would you query the line size at run time on each system, and what does each answer cost?

    Model answer, Pro. First step and follow-ups

Flashcards · 5 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. In what unit does a cache move memory, and how big is it?

    In whole aligned lines. Many x86-64 processors use 64 bytes; on the Apple silicon Mac used for the runs, sysctl reported 128. A miss brings in the whole line.

  2. Why does summing a large C matrix row by row run faster than column by column?

    C stores rows next to each other, so a row walk uses every element of each line it fetches: one miss serves 32 loads of 4-byte ints on a 128-byte line. A column walk jumps a whole row per load and needs a new line for every element.

  3. Both walks of the matrix do the same additions and give the same sum. What sets their cost?

    The order of the loads: whether consecutive loads fall in the same cache line or each needs a line of its own.

  4. What is spatial locality, and why does it matter for caches?

    The habit of using memory near memory just used, such as the next element of a row. It lets one fetched cache line serve many loads.

  5. What is false sharing?

    A slowdown in which threads write different variables that happen to sit in the same cache line, so the line keeps moving between their cores although no variable is actually shared.

Key takeaways

  • Caches move memory in whole lines, and macOS reports the system line through sysctl: 128 bytes on the host of the runs.
  • A loop whose consecutive loads fall in the same line uses all of each fetch; a loop that jumps a row per load needs a new line for every element.
  • The order of the loads, not the number of additions, set the cost of the two walks of matrix.c.

Terms in this lesson

cache
A small, fast memory near a core that keeps copies of recently used memory, so that later loads of the same or nearby addresses need no trip to main memory.
cache coherence
The hardware protocol that keeps the cores' cached copies of a line consistent, so that only one core at a time may write a line and others see its writes.
cache line
The fixed-size, aligned block in which a cache stores and moves memory. Sizes differ between processors and cache levels: Intel gives 64 bytes for its Core processors. A miss brings in the whole line.
false sharing
A slowdown in which threads write different variables that happen to lie in the same cache line, so the line moves between their cores although no variable is shared.
hardware prefetcher
A part of the processor that watches the addresses of a program's loads and fetches the lines it predicts will be needed next, before the loads ask for them.
spatial locality
The tendency of a program to use memory near memory it has just used, such as the next element of a row. It lets one cache line serve many loads.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.