An eBPF program counts events by looking a key up in a BPF_MAP_TYPE_HASH and doing (*val)++, and under load the totals come out too low. What is wrong, and what do __sync_fetch_and_add() and BPF_MAP_TYPE_PERCPU_HASH each change about it?
answer
- load, add, store — three steps, not one
- every core runs the same program at once
- atomic add on the shared value
- one value per CPU, nobody shares
- the reader has to sum across CPUs
basics
~20 sIncrementing a value fetched from a shared map is a non-atomic read-modify-write, so CPUs running the hook concurrently overwrite each other's counts. __sync_fetch_and_add() makes the update atomic; a per-CPU map gives each CPU its own value, and user space sums across CPUs.
solid answer
~50 s`bpf_map_lookup_elem()` hands back a pointer into the map, and `(*val)++` compiles to a load, an add and a store. Nothing serialises that against the same program running on another CPU — a hook like a syscall tracepoint fires on every core at once — so two CPUs read the same value and both store the same result, and one increment vanishes. There are two fixes. `__sync_fetch_and_add(val, 1)` compiles to a BPF atomic add, so the update is correct on a shared map at the cost of a contended cache line on the hottest keys. Or use `BPF_MAP_TYPE_PERCPU_HASH` (or `BPF_MAP_TYPE_PERCPU_ARRAY`), where each key has one value per CPU: the BPF program's plain `++` is then race-free because only that CPU touches its copy, and no cache line is shared. The cost moves to the reader — a user-space lookup returns an array with one entry per possible CPU, which you must sum, and value memory is multiplied by the CPU count.
code
c · 25 lines#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 10240);
__type(key, __u32);
__type(value, __u64);
} shared_counts SEC(".maps");
SEC("tracepoint/syscalls/sys_enter_openat")
int count_shared(void *ctx)
{
__u32 pid = bpf_get_current_pid_tgid() >> 32;
__u64 init = 1;
__u64 *val = bpf_map_lookup_elem(&shared_counts, &pid);
if (!val)
return bpf_map_update_elem(&shared_counts, &pid, &init, BPF_ANY);
__sync_fetch_and_add(val, 1); /* NOT (*val)++ : that loses counts */
return 0;
}
char LICENSE[] SEC("license") = "GPL";go deeper
Know that bpf_map_lookup_elem() returns a pointer into the map and that several CPUs can run the same BPF program at the same moment, so a plain increment is not safe.
Explain read-modify-write as three steps, and describe both remedies: an atomic add via __sync_fetch_and_add() on a shared map, or a per-CPU map where each CPU owns its value.
Diagnose the symptom — counts consistently low only under load — and weigh the tradeoffs: contention on hot keys against value memory multiplied by CPU count and a reader that must aggregate.
Decide what accuracy the counter owes its consumers: exact enforcement values need one shared atomic value, observability counters can be per-CPU and eventually summed, and that choice sets the memory budget for the whole fleet.
## What the increment really is ```c __u64 *val = bpf_map_lookup_elem(&counts, &key); if (!val) return 0; (*val)++; ``` The pointer points *into the map's storage*, which is why writing through it updates the stored value with no second helper call. But `(*val)++` is three machine steps: load the value, add one, store it back. The verifier is content — nothing unsafe is happening, no memory is touched out of bounds. It is simply not atomic. ## Why that loses counts BPF programs run concurrently. A tracepoint on `sys_enter_openat` fires on whichever CPU the calling task is on, and on a 64-core box that is sixty-four possible simultaneous executions of your program. If two of them are counting the same key, both load 100, both add one, both store 101. Two events, one increment recorded. The kernel does not lock map values for you; there is no implicit critical section around a value pointer. The symptom is characteristic: totals that are close but consistently low, worse the busier the box and the hotter the key, and impossible to reproduce on an idle test machine. If a counter cross-checks against another source and is quietly 3% short under load, this is the first thing to suspect. ## Fix one: make the update atomic ```c __sync_fetch_and_add(val, 1); ``` Clang compiles this GCC-style builtin into a BPF atomic add instruction, and the kernel executes it as an atomic operation on the map's storage. The count becomes correct. What you pay is cache-line contention: every CPU updating the same key is bouncing that line between cores, which on a very hot key can cost more than the work you are instrumenting. It is still the right answer when you need a single shared value — a lock-free shared histogram bucket, a limit that a network program must enforce consistently rather than approximately. ## Fix two: give every CPU its own copy `BPF_MAP_TYPE_PERCPU_HASH` and `BPF_MAP_TYPE_PERCPU_ARRAY` allocate `value_size` bytes **per possible CPU** for each key. When the BPF program looks a key up, the helper returns the pointer to *this CPU's* copy, so: ```c (*val)++; /* safe: nobody else can touch this CPU's copy */ ``` is correct with no atomic and no contention at all. This is why per-CPU maps are the default for high-frequency counters and histograms — bcc and bpftrace lean on them heavily. The cost is threefold: **Memory.** A 64-bit counter on a 96-CPU machine is 768 bytes per key, before you consider that per-CPU values are padded to 8-byte alignment. A large-keyspace per-CPU map is far more expensive than its `value_size` suggests. **The reader has to aggregate.** A user-space lookup on a per-CPU map does not return one value; it fills a buffer holding one value per possible CPU, and your code sums them. libbpf offers `libbpf_num_possible_cpus()` for the sizing — and note *possible*, not *online*: the kernel allocates for CPUs that could come online, and undersizing that buffer corrupts memory in your own agent. **No consistent snapshot.** Summing walks the per-CPU values one at a time while the program keeps incrementing, so the total is a smear across a short interval rather than a point-in-time reading. For rates and histograms this is fine. For an exact invariant it is not. ## Compound updates Both fixes cover a single arithmetic update. If you must read a value, decide something, and write back — a rate limiter checking a budget, say — neither an atomic add nor a per-CPU copy expresses that. Some map types support `bpf_spin_lock()` and `bpf_spin_unlock()` around a lock embedded in the value struct, but the mechanism is narrow: it is unavailable to tracing program types, values holding a lock have restrictions on how user space may read them, and only one lock may be held at a time. Where you cannot use it, redesign so that each CPU decides locally, or move the decision to user space. ## Choosing High-frequency counting and latency histograms: per-CPU, plain increment, sum in the reader. A value that several CPUs must agree on exactly, or a keyspace so large that multiplying by the CPU count is unaffordable: shared map with `__sync_fetch_and_add()`. What is never right is a plain `++` on a shared map — it is not a performance optimisation, it is a data loss bug that only shows up under the load you built the tool to measure.
- When user space looks up one key in a BPF_MAP_TYPE_PERCPU_HASH, what does it get back?One value per possible CPU, laid out as an array that the reader must size with the possible-CPU count and then sum itself. The kernel does no aggregation. Sizing the buffer by online CPUs instead of possible CPUs is a real bug — the kernel writes the full array and overruns your buffer.
- When is a shared map with an atomic add the better choice than a per-CPU map?When the value must be exactly right at any instant rather than eventually, such as a budget a network program enforces per packet, or when the keyspace is large enough that multiplying every value by the CPU count costs unacceptable memory. You accept cache-line contention on hot keys in exchange for one authoritative value.
- Your per-CPU counters are correct but the totals still look slightly inconsistent between two metrics read a moment apart. Why?Summing a per-CPU map walks the CPUs one at a time while the BPF program keeps incrementing, so each total is smeared over the read rather than taken at an instant. That is harmless for rates and histograms, but it means you should not assert exact equalities between two separately-read counters.
saying these in an interview costs you the question
- Assumes map value updates are atomic by default
- Thinks the verifier would reject an unsafe increment
- Believes the kernel sums per-CPU values before returning them
- Sizes the per-CPU read buffer by online CPUs, not possible CPUs
- Uses per-CPU maps for a huge keyspace without counting the memory