The famous bug that still haunts C++ developers
\n\nIn 2013, a question appeared on Stack Overflow that garnered hundreds of thousands of views and spawned a real cult among C++ optimizers. It went something like this: "I replaced int with uint64_t in a loop counter — and performance dropped by 10 times. Why?". The answer lay not in magic, but in the microarchitecture of Intel processors. In 2025, this problem hasn't gone away: modern chips can still "stumble" on what seems like a harmless data type change.
Today we'll break down this paradox using the _mm_popcnt_u64 instruction (counting set bits in a 64-bit word) as an example, show real code that suffers from this anomaly, and give clear recommendations on how to write fast and predictable C++ code in 2025.
The core of the problem: 32-bit vs 64-bit counter
\n\nImagine a simple loop that sums the number of set bits (popcount) in an array of 64-bit integers. We use the _mm_popcnt_u64 intrinsic from the SSE4.2 set. Here are two code variants — the first with a 32-bit counter, the second with a 64-bit one:
#include <cstdint>\n#include <vector>\n#include <nmmintrin.h> // for _mm_popcnt_u64\n\n// Variant 1: 32-bit counter (fast)\nuint64_t popcnt_32bit(const std::vector<uint64_t>& data) {\n uint64_t result = 0;\n for (int i = 0; i < data.size(); ++i) { // int — 32 bits\n result += _mm_popcnt_u64(data[i]);\n }\n return result;\n}\n\n// Variant 2: 64-bit counter (slow)\nuint64_t popcnt_64bit(const std::vector<uint64_t>& data) {\n uint64_t result = 0;\n for (uint64_t i = 0; i < data.size(); ++i) { // uint64_t — 64 bits\n result += _mm_popcnt_u64(data[i]);\n }\n return result;\n}\n\n\nIf you measure the speed on an Intel processor (e.g., Core i7-13700K or even an older Haswell), the difference will be shocking: the second variant can be 3–10 times slower. At the same time, on AMD Ryzen there is no such anomaly — both variants run equally fast.
\n\nWhy does this happen? The root of evil — false dependency
\n\nThe POPCNT instruction on Intel processors (up to the most modern ones) has a false dependency on the destination register. Formally, POPCNT completely overwrites the output register, but microarchitecturally the processor behaves as if it reads the old result before writing. This is a bug (or feature) in Intel's implementation.
Now look at the loop with the 64-bit counter i. The compiler is forced to use the same register (e.g., rax) for storing both the counter and the intermediate results of POPCNT. Each iteration creates a dependency chain: the next POPCNT cannot start until the previous one finishes, because the processor "thinks" it needs the old result. This kills the pipeline.
With a 32-bit counter, the compiler uses different registers (e.g., ecx for the counter and rax for accumulating the result), and the false dependency does not occur — POPCNT instructions execute in parallel.
Detailed analysis in assembly
\n\nTo be sure, let's look at what each variant compiles to (GCC 13, flags -O3 -march=native):
32-bit counter (fast):
\n.L3:\n popcnt rcx, QWORD PTR [rdx] ; popcnt into rcx (independent register)\n add rax, rcx ; add to accumulator\n add rdx, 8 ; advance pointer\n cmp rdi, rdx ; compare\n jne .L3\n\n\n64-bit counter (slow):
\n.L7:\n popcnt rax, QWORD PTR [rdx] ; popcnt into rax — the same as the