2026-09-09
Every time you write flags |= (1 << n) in C, the compiler emits a shift, an OR, and a store. On x86, there's a single instruction that does it directly against memory, at any bit offset, with an optional LOCK prefix that makes it atomic across cores. The bit-test family — BT (test), BTS (test and set), BTR (test and reset), BTC (test and complement) — has been in x86 since the 386 and remains the tersest way to touch one bit in a large bitmap.
The magic is the bit offset semantics. Unlike a normal memory operand, the offset in BT [mem], reg is not clamped to the operand size. If you write BTS [rdi], rax with rax = 100000, the CPU computes the byte address as rdi + (100000 / 8) and toggles bit 100000 % 8. You address any bit in a bitmap of arbitrary size with one instruction — no manual shifting, no word-index arithmetic. The Carry Flag (CF) receives the previous value of the bit, so you get a test-and-set primitive for free.
Real-world example: The Linux kernel's set_bit(), clear_bit(), and test_and_set_bit() (defined in arch/x86/include/asm/bitops.h) are inline asm wrappers around LOCK BTS, LOCK BTR, and LOCK BTS respectively. Every CPU mask, every dirty-page bitmap, every allocator's free-block tracking uses these. When the buddy allocator marks a page as allocated, it's a single LOCK BTR against a bitmap that may span megabytes. The atomic version costs ~20 cycles uncontended; the non-atomic version is 3–4 cycles.
The catch nobody mentions: non-memory-form BT reg, reg is fast (single µop), but the memory form with a large offset can be slower than the equivalent load-shift-mask sequence, because the CPU's address generation unit doesn't have a fast path for the divide-by-8. Modern compilers know this and only emit BT [mem], reg when the offset is a small immediate — for computed offsets, they generate the manual sequence. Check with gcc -O2 -S if you care.
Rule of thumb: for a bitmap of N bits, memory footprint is ceil(N/8) bytes, and atomic single-bit modification costs one LOCK-prefixed cycle round-trip (~20ns on modern hardware) regardless of N. Compare to a std::vector<bool> operation, which may cost 5–10x more due to abstraction overhead.
The bit-string form is also why ffs()/ffz() (find-first-set/zero) pairs so neatly with these: scan with BSF, claim with LOCK BTS, retry on CF=1.
