51 Working with bits — idioms and traps
What to know first
Looking back
chapter 29 gave the rule “do bit work on unsigned types”. But why is the sign the problem — is a bit not just a bit?
A. A bit is just a bit, but C always sees a bunch of bits as a value of some type. If that type is signed, the topmost bit acquires the special meaning “sign”, and from there shifting and division behave differently (chapter 6) and some combinations fall outside the contract entirely (chapter 50). Choosing unsigned is removing that special meaning so that bits are really bits. This chapter is about what can be built on top of that.
The need for this chapter, and its context
By the end of this chapter
<stdbit.h>). After that seven rules, the accidents that happen around here, and a look at what the compiler already knows — in the machine code it emits.The questions this chapter answers
- Would bit fields (chapter 49) not spare us all this shifting and masking?
51.1 The five basic moves#
Working with bits comes down to five things. Everything else is a combination.
Before the table, the letters. All five rows are about changing v.
| letter | what it is |
|---|---|
v | the value being changed. The starting value, and where the result lands |
n | the position number of the bit in question. The rightmost is number 0 |
m | the mask — a value with 1s marking “which places do we touch” |
s | the position where the field starts (where the 1s of m begin) |
x | the new value for that field, still sitting at position 0, not yet shifted |
Table 51.1 — The letters used in the table below
First, what 1u << n actually is. 1u has only the rightmost place turned on, and << n pushes it n places to the left. So 1u << n is the value with exactly one place on, the n-th counted from the right end — numerically . What matters is that the counting starts at 0, not 1: 1u << 0 is the rightmost bit (value 1) and 1u << 3 is the fourth from the right (value 8).
position 7 6 5 4 3 2 1 0 <- positions count from the right, from 0
1u 0 0 0 0 0 0 0 1 value 1
1u << 3 0 0 0 0 1 0 0 0 value 8 (the 1 moved three places left)
1u << 7 1 0 0 0 0 0 0 0 value 128So “bit n” means the place n steps to the left of the right end. 1u << n turns on exactly that place, and it is the raw material of all five moves below.
| what | how it is written | why it works | remember |
|---|---|---|---|
| set | v |= 1u << n | OR gives 1 if either side is 1 — other places OR with 0 and stay | doing it twice changes nothing |
| clear | v &= ~(1u << n) | AND gives 0 if either side is 0 — the mask is 0 only there | forgetting the ~ is a common slip |
| flip | v ^= 1u << n | XOR inverts exactly where the mask has 1s | twice returns to the start |
| test | v & (1u << n) | only that place survives | ★ the result is the bit’s value, not 1 |
| replace a field | v = (v & ~m) | ((x << s) & m) | clear the places m marks, then lay the shifted x on top | order matters — clear, then insert |
Table 51.2 — The five moves on a single bit
examples/bitwise/bitops.c
// 비트를 다루는 다섯 가지 기본 동작.
// 모두 부호 없는 타입 위에서, 폭을 이름에 적은 타입으로 한다.
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
static void show(const char *label, uint32_t v)
{
printf(" %-28s 0x%08" PRIX32 "\n", label, v);
}
int main(void)
{
uint32_t v = 0x00F0'0000; // C23 의 자릿수 구분자
show("start", v);
// ① 세우기 --- 그 자리에 1 을 넣는다
v |= UINT32_C(1) << 3;
show("set bit 3 (|=)", v);
// ② 지우기 --- 그 자리에만 0 인 마스크와 AND
v &= ~(UINT32_C(1) << 20);
show("clear bit 20 (&= ~)", v);
// ③ 읽기 --- 결과는 0 이거나 「그 비트의 값」이지 1 이 아니다
printf(" is bit 3 on? %s\n", (v & (UINT32_C(1) << 3)) ? "yes" : "no");
printf(" the value of v & (1<<3) is %" PRIu32 ", not 1\n",
v & (UINT32_C(1) << 3));
// ④ 뒤집기 --- XOR 은 마스크가 1 인 자리만 뒤집는다
v ^= UINT32_C(0xF0);
show("flip bits 4..7 (^=)", v);
// ⑤ 필드 바꿔 넣기 --- 지우고, 밀어 넣는다
// 비트 8~15 를 하나의 8비트 필드로 본다.
uint32_t field = 0xAB;
uint32_t mask = UINT32_C(0xFF) << 8;
v = (v & ~mask) | ((field << 8) & mask);
show("put 0xAB into bits 8..15", v);
// 꺼내는 것은 반대 순서다 --- 내리고, 남긴다
printf(" reading it back gives 0x%02" PRIX32 "\n", (v >> 8) & 0xFF);
return 0;
}
Output
start 0x00F00000
set bit 3 (|=) 0x00F00008
clear bit 20 (&= ~) 0x00E00008
is bit 3 on? yes
the value of v & (1<<3) is 8, not 1
flip bits 4..7 (^=) 0x00E000F8
put 0xAB into bits 8..15 0x00E0ABF8
reading it back gives 0xAB
Only the last row has four letters, and the end of the demo is exactly that case. There, bits 8..15 of v are treated as one field and 0xAB is put into it — m is the mask with just those eight places set (0xFF << 8), s is 8, the position the field starts at, and x is the value to insert, 0xAB. Read in two pieces:
v & ~m— empty the place.~mis zero exactly at those eight places, so the AND leaves everything else alone and clears them.(x << s) & m— move the new value into place. Shiftxup bysto line it up, and mask withmin casexis larger than the field.
OR the two together and you have “that field replaced, nothing else touched”. Reading it back reverses the steps — (v >> s) & (m >> s), which in the demo is (v >> 8) & 0xFF.
The fourth line is where beginners are caught most often. The value of v & (1u << 3) is 8, not 1. Put straight into an if it works (only truth matters there), but stored or compared it goes wrong.
Counter-example. Comparing a bit test against 1
if ((flags & FLAG_READY) == 1) { … } /* almost always false */If FLAG_READY is 1u << 5, the test yields 32, not 1. There are two ways out — if only truth is needed, do not compare at all: if (flags & FLAG_READY); and if a 0/1 really is needed, write !!(flags & FLAG_READY) or (flags & FLAG_READY) != 0.
51.1.1 Building masks, and the cliff#
A mask records “which places are we looking at”. Three ways of building one are enough.
| what | how | watch out |
|---|---|---|
| a single bit | 1u << n | n must satisfy 0 <= n < width |
the low n bits | (1u << n) - 1u | ★ outside the contract when n equals the width |
w bits starting at s | ((1u << w) - 1u) << s | s + w must not exceed the width |
Table 51.3 — Three masks
Beware the cliff in the second row. Turning on all 32 bits of a uint32_t by writing (1u << 32) - 1 shifts by the full width, which is outside the contract (chapter 50). When the whole width is wanted, choose an expression with no shift at all — UINT32_MAX, or ~UINT32_C(0). If it must be general, split the case: w == 32 ? ~UINT32_C(0) : ((UINT32_C(1) << w) - 1).
51.2 A set of flags — the commonest use#
Holding several yes/no answers in one integer is the first use of bit work.
enum {
OPT_READ = 1u << 0,
OPT_WRITE = 1u << 1,
OPT_APPEND = 1u << 2,
OPT_BINARY = 1u << 3,
};
unsigned opts = OPT_READ | OPT_BINARY; /* start with two on */
opts |= OPT_WRITE; /* turn one more on */
opts &= ~OPT_BINARY; /* turn one off */
if (opts & OPT_WRITE) { /* writing is on */ }
if ((opts & (OPT_READ | OPT_WRITE)) == (OPT_READ | OPT_WRITE))
{ /* *both* are on */ }
if (opts & (OPT_READ | OPT_WRITE))
{ /* *either* is on */ }Look at the difference between the last two. Either is a plain AND, but both must ask “is it equal to the mask”. Mixing these two up is a very common bug.
★ An enum is a good place to name these values — the debugger shows the names and the values sit together. But an enumeration constant has type int (still the default in C23), so a place like 1u << 31, beyond the signed width, belongs in a macro or an unsigned constant.
51.3 The idioms — and why they work#
What follows has been polished over half a century. Rather than memorising them, follow why each works once and you can rebuild it when you need it.
| what it gives | how it is written | why |
|---|---|---|
| is it even | (x & 1u) == 0 | the lowest place is the ones place |
| keep only the lowest set bit | x & (0u - x) | 0u - x is ~x + 1 — below that bit nothing changes, above it everything flips |
| clear the lowest set bit | x & (x - 1u) | x - 1 turns that bit into 0 and everything below it into 1s |
| is it a power of two | x != 0 && (x & (x - 1u)) == 0 | it means exactly one bit is set |
round up to a multiple of a (a power of two) | (x + a - 1u) & ~(a - 1u) | overshoot first, then cut the low bits off |
round down to a multiple of a | x & ~(a - 1u) | just discard the low bits |
remainder (unsigned, a a power of two) | x & (a - 1u) | keep only the low bits |
| count the 1 bits | while (x) { x &= x - 1; n++; } | “clear the lowest set bit”, repeated — it loops once per set bit |
| rotate left | (x << (n & 31)) | (x >> ((32 - n) & 31)) | the bits pushed off one end come back in the other. The & 31 guards the n == 0 cliff |
| choose without branching | (x & -(uint32_t)c) | (y & ~-(uint32_t)c) | for c of 0 or 1 the mask becomes all-zeros or all-ones |
Table 51.4 — The idioms one meets most often
examples/bitwise/idioms.c
// 자주 쓰는 비트 관용구들 --- 그리고 왜 그렇게 되는지.
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
// x 에서 가장 낮은 1 비트만 남긴다.
// 무부호에서 0u - x 는 (~x + 1) 과 같다. 가장 낮은 1 비트 *아래*는 그대로이고
// 그 위는 전부 뒤집히므로, 둘을 AND 하면 그 한 비트만 살아남는다.
static uint32_t lowest_one(uint32_t x) { return x & (0u - x); }
// 가장 낮은 1 비트를 지운다. x-1 은 그 비트를 0 으로 만들고 아래를 전부 1 로 만든다.
static uint32_t clear_lowest_one(uint32_t x) { return x & (x - 1u); }
// 2 의 거듭제곱인가 --- 1 비트가 정확히 하나인가와 같은 말이다.
static int is_power_of_two(uint32_t x) { return x != 0 && (x & (x - 1u)) == 0; }
// a(2의 거듭제곱)의 배수로 올림한다.
static uint32_t align_up(uint32_t x, uint32_t a) { return (x + a - 1u) & ~(a - 1u); }
// 왼쪽으로 n 만큼 회전. n == 0 일 때도 안전하다 --- 아래를 보라.
static uint32_t rotate_left(uint32_t x, unsigned n)
{
return (x << (n & 31)) | (x >> ((32u - n) & 31));
}
// 1 비트의 개수 --- 켜진 비트 수만큼만 돈다(커니핸 방식).
static unsigned count_ones(uint32_t x)
{
unsigned n = 0;
while (x) { x &= x - 1u; n++; }
return n;
}
int main(void)
{
uint32_t x = 0x0000'0B40; // …1011 0100 0000
printf("x = 0x%08" PRIX32 "\n", x);
printf(" lowest set bit (x & -x) = 0x%08" PRIX32 "\n", lowest_one(x));
printf(" clear lowest (x & (x-1)) = 0x%08" PRIX32 "\n", clear_lowest_one(x));
printf(" number of 1 bits = %u\n", count_ones(x));
puts("powers of two:");
for (uint32_t v = 0; v <= 5; v++)
printf(" %" PRIu32 " -> %s\n", v, is_power_of_two(v) ? "yes" : "no");
printf(" 1024 -> %s, 1000 -> %s\n",
is_power_of_two(1024) ? "yes" : "no", is_power_of_two(1000) ? "yes" : "no");
puts("rounding up to a multiple of 16:");
for (uint32_t v = 0; v <= 33; v += 16)
printf(" align_up(%2" PRIu32 ", 16) = %" PRIu32 "\n", v, align_up(v, 16));
printf(" align_up(17, 16) = %" PRIu32 "\n", align_up(17, 16));
puts("rotation keeps every bit --- nothing falls off the end:");
printf(" rotate_left(0x80000001, 1) = 0x%08" PRIX32 "\n", rotate_left(0x8000'0001u, 1));
printf(" rotate_left(0x80000001, 0) = 0x%08" PRIX32 " (n = 0 is safe here)\n",
rotate_left(0x8000'0001u, 0));
return 0;
}
Output
x = 0x00000B40
lowest set bit (x & -x) = 0x00000040
clear lowest (x & (x-1)) = 0x00000B00
number of 1 bits = 4
powers of two:
0 -> no
1 -> yes
2 -> yes
3 -> no
4 -> yes
5 -> no
1024 -> yes, 1000 -> no
rounding up to a multiple of 16:
align_up( 0, 16) = 0
align_up(16, 16) = 16
align_up(32, 16) = 32
align_up(17, 16) = 32
rotation keeps every bit --- nothing falls off the end:
rotate_left(0x80000001, 1) = 0x00000003
rotate_left(0x80000001, 0) = 0x80000001 (n = 0 is safe here)
Look closely at the rotation. The form usually seen is (x << n) | (x >> (32 - n)), and when n is 0 the right-hand side becomes x >> 32, outside the contract. The pair of & 31 in the demo blocks that cliff, and costs nothing — as we will see, the compiler recognises this shape and folds it into a single rotate instruction.
A common misconception. Swapping with XOR and no temporary is faster
*a ^= *b; *b ^= *a; *a ^= *b; /* do not */Two things are wrong. First, it is slower. Counting the machine code this machine’s gcc emits at -O2: the version with a temporary is four instructions (two loads, two stores) while the XOR version is six, and three of those wait on the one before — a dependency chain that cannot proceed in parallel. A trick from an age of scarce registers is a loss today.
Second, it gives wrong answers. If both arguments point at the same place (swap(&v, &v)), the first line zeroes it and the value is gone. The version with a temporary has no such case.
51.4 C23 — the idioms get names#
Many of the idioms above are in fact one machine instruction. The problem was that for a long time there was no standard way to ask for it, so each compiler kept its own name — __builtin_popcount, _BitScanForward — and portable code wrote the idiom out by hand. C23 tidied this up: <stdbit.h>.
| function | what it gives | written by hand |
|---|---|---|
stdc_count_ones | how many bits are 1 | while (x) { x &= x-1; n++; } |
stdc_count_zeros | how many bits are 0 | width minus the above |
stdc_leading_zeros | zeros running down from the top | — |
stdc_trailing_zeros | zeros running up from the bottom | — |
stdc_first_leading_one | position of the first 1 from the top (1-based, 0 if none) | — |
stdc_first_trailing_one | position of the first 1 from the bottom | — |
stdc_bit_width | bits needed to hold the value | while (x) { x >>= 1; n++; } |
stdc_bit_floor · stdc_bit_ceil | nearest power of two at or below / above | — |
stdc_has_single_bit | is it a power of two | x && !(x & (x-1)) |
Table 51.5 — What <stdbit.h> named (there are uc/us/ui/ul/ull variants too)
examples/bitwise/stdbit.c
// C23 이 관용구에 이름을 붙였다 --- <stdbit.h>.
// 손으로 짠 것과 표준 함수를 나란히 놓고 답이 같은지 확인한다.
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdbit.h>
static unsigned count_by_hand(uint32_t x)
{
unsigned n = 0;
while (x) { x &= x - 1u; n++; }
return n;
}
static unsigned width_by_hand(uint32_t x) // 담는 데 필요한 비트 수
{
unsigned n = 0;
while (x) { x >>= 1; n++; }
return n;
}
int main(void)
{
printf("__STDC_VERSION_STDBIT_H__ = %ld\n", (long)__STDC_VERSION_STDBIT_H__);
printf("byte order is %s\n",
__STDC_ENDIAN_NATIVE__ == __STDC_ENDIAN_LITTLE__ ? "little endian"
: __STDC_ENDIAN_NATIVE__ == __STDC_ENDIAN_BIG__ ? "big endian"
: "neither");
uint32_t samples[] = { 0u, 1u, 0x0000'0B40u, 300u, 0xFFFF'FFFFu };
puts("value ones(hand/std) width(hand/std) bit_ceil leading zeros");
for (size_t i = 0; i < sizeof samples / sizeof samples[0]; i++) {
uint32_t x = samples[i];
printf("0x%08" PRIX32 " %2u / %-2u %2u / %-2u %-10" PRIu32 " %u\n",
x,
count_by_hand(x), (unsigned)stdc_count_ones(x),
width_by_hand(x), (unsigned)stdc_bit_width(x),
(uint32_t)stdc_bit_ceil(x),
(unsigned)stdc_leading_zeros(x));
}
// 자리 번호를 세는 함수는 *1 부터* 세고, 0 은 「없다」는 뜻이다.
printf("first_leading_one(0x00F0) = %u (counted from the top, 1-based)\n",
(unsigned)stdc_first_leading_one(UINT32_C(0x00F0)));
printf("first_trailing_one(0x00F0) = %u (counted from the bottom)\n",
(unsigned)stdc_first_trailing_one(UINT32_C(0x00F0)));
printf("first_trailing_one(0) = %u (zero means 'there is none')\n",
(unsigned)stdc_first_trailing_one(UINT32_C(0)));
// 2 의 거듭제곱 판정도 이름을 얻었다.
printf("has_single_bit(1024) = %d, has_single_bit(1000) = %d\n",
(int)stdc_has_single_bit(UINT32_C(1024)),
(int)stdc_has_single_bit(UINT32_C(1000)));
return 0;
}
Output
__STDC_VERSION_STDBIT_H__ = 202311
byte order is little endian
value ones(hand/std) width(hand/std) bit_ceil leading zeros
0x00000000 0 / 0 0 / 0 1 32
0x00000001 1 / 1 1 / 1 1 31
0x00000B40 4 / 4 12 / 12 4096 20
0x0000012C 4 / 4 9 / 9 512 23
0xFFFFFFFF 32 / 32 32 / 32 0 0
first_leading_one(0x00F0) = 25 (counted from the top, 1-based)
first_trailing_one(0x00F0) = 5 (counted from the bottom)
first_trailing_one(0) = 0 (zero means 'there is none')
has_single_bit(1024) = 1, has_single_bit(1000) = 0
Three things to note.
First, the position functions count from 1. When stdc_first_trailing_one returns 5 it means “the fifth bit from the bottom”, the 1u << 4 place. Zero means “there is no such bit”, so these give a safe answer even for a value of 0 — unlike most hand-written idioms, which fall apart there.
Second, stdc_bit_ceil can overflow. If the result does not fit the type, the behaviour is outside the contract. That the demo printed 0 for 0xFFFFFFFF is what this machine did, not a promise of the standard.
Third, this header also tells you the byte order. Compare __STDC_ENDIAN_NATIVE__ against __STDC_ENDIAN_LITTLE__ and __STDC_ENDIAN_BIG__ (chapter 3). It is the first standard C way of asking about endianness.
Platform note. There are still places where it cannot be used
<stdbit.h> is a C23 header, so the compiler has to provide it. On this book’s verifying machine gcc 14 does, and __STDC_VERSION_STDBIT_H__ reads 202311. Where older tools must also be supported, the practical compromise is to branch on #if __has_include(<stdbit.h>) and keep the idioms of the previous section on the other side.
And a standard function does not always become one machine instruction. On this machine stdc_count_ones compiles to a library call with default settings, and only folds into a single popcnt once the compiler is told the machine has that instruction (-mpopcnt). The standard unified the name, not the speed.
51.5 Seven rules#
| rule | why |
|---|---|
| do it on unsigned | it removes the special meaning of the sign bit. Left-shifting a negative is outside the contract; right-shifting one is implementation-defined (chapters 29 and 50) |
| write the width into the type | the width of unsigned varies by machine; uint32_t says 32 |
| put a suffix on constants | 1 << 31 is an int and outside the contract; 1u << 31 is fine. For 64-bit places, UINT64_C(1) |
| do not forget promotion | even uint8_t operands are widened to int first — flipping and comparing surprise people |
| check the shift count | 0 <= n < width, where the width is sizeof(x) * CHAR_BIT |
| be generous with parentheses | &, | and ^ bind more weakly than the comparisons (chapter 50) |
| give it a name | FIELD_GET(v, KIND) reads better in six months than (v >> 8) & 0xFF |
Table 51.6 — Rules for working with bits
The fourth rule is the one that hides. In the flesh:
examples/bitwise/promote.c
// 비트 연산에서 가장 자주 데이는 자리 --- 정수 승격.
// int 보다 좁은 타입은 연산 전에 int 로 넓혀진다. 그래서 「8비트를 뒤집었다」고
// 생각한 결과가 32비트짜리로 나온다.
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
int main(void)
{
uint8_t c = 0x0F;
// ~c 의 타입은 uint8_t 가 아니라 int 다.
printf("c = 0x%02X\n", c);
printf("~c = 0x%08X <- not eight bits\n", (unsigned)~c);
printf("(uint8_t)~c = 0x%02X <- narrow it back yourself\n",
(unsigned)(uint8_t)~c);
// 여기서 (~c == 0xF0) 이라 적으면 gcc 가 -Wsign-compare 로 막아 준다.
printf("((uint8_t)~c == 0xF0) is %s\n", ((uint8_t)~c == 0xF0) ? "true" : "false");
// char 가 부호 있는 기계에서 0x80 이상인 바이트를 int 로 넓히면 음수가 된다.
// 그래서 바이트를 다룰 때는 unsigned char 로 받는다.
char signed_byte = (char)0x80;
unsigned char plain_byte = 0x80;
printf("a char holding 0x80, widened : %d\n", (int)signed_byte);
printf("an unsigned char holding 0x80 : %d\n", (int)plain_byte);
printf("masking with 0xFF fixes it : %d\n", (int)(signed_byte & 0xFF));
// 마스크의 폭도 타입을 따라간다.
uint64_t wide = 0xFFFF'FFFF'FFFF'FFFFu;
printf("wide & ~0u = 0x%016" PRIX64 " <- ~0u is 32 bits wide\n",
wide & ~0u);
printf("wide & ~UINT64_C(0) = 0x%016" PRIX64 "\n", wide & ~UINT64_C(0));
return 0;
}
Output
c = 0x0F
~c = 0xFFFFFFF0 <- not eight bits
(uint8_t)~c = 0xF0 <- narrow it back yourself
((uint8_t)~c == 0xF0) is true
a char holding 0x80, widened : -128
an unsigned char holding 0x80 : 128
masking with 0xFF fixes it : 128
wide & ~0u = 0x00000000FFFFFFFF <- ~0u is 32 bits wide
wide & ~UINT64_C(0) = 0xFFFFFFFFFFFFFFFF
A ~ applied to a uint8_t and the result is 0xFFFFFFF0, because c is widened to int before ~ is computed (chapter 30). Getting back to eight bits means narrowing again. For the same reason ~c == 0xF0 is false — and happily gcc points at that spot: comparison of promoted bitwise complement of an unsigned value with constant.
The last two lines have the same root. ~0u is not “all ones” but “all ones as wide as unsigned”, so ANDing it with a 64-bit value wipes the upper half. A mask has a width too.
51.6 The accidents that happen here#
| accident | symptom | who catches it | the cure |
|---|---|---|---|
flags & MASK == 0 | always the same answer — == binds first | -Wparentheses | (flags & MASK) == 0 |
1 << 31 on an int | outside the contract — the answer shifts with optimisation | -fsanitize=undefined | 1u << 31 |
x >> 32 (at or past the width) | outside the contract — 0 on one machine, x on another | the compiler warns for constants | check the count, or split the shift |
rotation with n == 0 | becomes x >> 32, outside the contract | nobody | add the & 31 |
~ on a narrow type | thought to be 8 bits, comes out 32 | sometimes -Wsign-compare | narrow it back: (uint8_t)~c |
handling bytes in a char | 0x80 and above turn negative | nobody | use unsigned char, or & 0xFF |
>> 1 instead of division on a signed value | a different answer for negatives | nobody | write division as / |
~0u used as a 64-bit mask | the upper half is wiped | nobody | ~UINT64_C(0) |
Table 51.7 — Accidents around bit work
The seventh row is the trap left behind by the old advice that “shifting is fast division”. It is true on unsigned values and gives a different answer on signed ones.
In practice. Is −7 halved −3 or −4?
C’s division truncates toward zero (chapter 29) while an arithmetic right shift truncates downward. So -7 / 2 is -3 and -7 >> 1 is -4. The remainder goes the same way — -7 % 8 is -7 while -7 & 7 is 1.
The difference is one where the value really does differ, which is how it slips by. Computing an array index, snapping a coordinate to a grid, putting a hash into a bucket — nothing happens at all for as long as no negative arrives.
And the trick has no value left. The compiler already turns x / 2 into a shift, with the correction a negative needs so the answer stays right. If division was meant, write division.
51.7 Before optimising by hand#
Many bit tricks were born from the premise “the compiler cannot do this, so a person must”. It is worth checking whether that premise still holds. Here is what this machine’s gcc emits at -O2.
| written in the source | machine code emitted | meaning |
|---|---|---|
(x << n) | (x >> ((32 - n) & 31)) | a single rol | it recognises the rotation idiom |
x & (0u - x) | neg + and; with BMI1 enabled, one blsi | where the instruction exists, it is used |
while (x) { x &= x-1; n++; } | with -mpopcnt, a single popcnt | ★ it recognises the whole loop and folds it |
stdc_count_ones(x) | a library call by default; one popcnt with -mpopcnt | a standard name does not make an instruction |
| swapping with XOR | six instructions and a dependency chain (four with a temporary) | an old trick that became a loss |
Table 51.8 — What the compiler already knows — checked on this machine
The third row is the heart of the table. Kernighan’s counting loop is code a person wrote “cleverly”, and the compiler sees through to the intent and replaces it with one instruction. If the trick and the plain version compile to the same machine code, the only difference left is readability.
★ So the order is this. (1) Write it so the meaning shows. (2) Measure (chapter 13). (3) Only if that is not enough, reach for the trick — and leave a comment saying why it is written that way.
51.8 In the flesh — where bits are used#
| place | what it does | where in this book |
|---|---|---|
| hardware registers | read and write several fields inside one word | chapters 88 and 104 |
| flag sets | several options in one integer | this chapter |
| character encodings | assembling and taking apart UTF-8 bytes | chapter 8 |
| colours and pixels | R, G, B and A packed into one word | — |
| bitmaps (bit sets) | a million yes/no answers in a few words | this chapter |
| hashes and random numbers | mixing bits with XOR and shifts | chapter 97 |
| compression and coding | reading and writing bit by bit | — |
Table 51.9 — Where bit work actually appears
UTF-8 is a good specimen. Splitting one code point into several bytes is, in the end, shifting and masking (chapter 8).
/* U+0800 to U+FFFF become three bytes --- 1110xxxx 10xxxxxx 10xxxxxx */
out[0] = (unsigned char)(0xE0u | (cp >> 12)); /* top four bits */
out[1] = (unsigned char)(0x80u | ((cp >> 6) & 0x3Fu)); /* middle six */
out[2] = (unsigned char)(0x80u | (cp & 0x3Fu)); /* bottom six */0x3F is the mask meaning “only the low six bits”, while 0x80 and 0xE0 are markers saying what kind of byte this is. The five moves of the first section, used exactly as they were given.
And the shape met most often in practice is the bitmap.
examples/bitwise/bitset.c
// 비트맵 --- 비트 연산이 실무에서 가장 자주 쓰이는 모습.
// 참·거짓 백만 개를 담는 데 바이트 백만 개를 쓸 이유가 없다.
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdbit.h>
#define BITS_PER_WORD 64
#define WORDS(n) (((n) + BITS_PER_WORD - 1) / BITS_PER_WORD)
#define N 1000
static uint64_t set[WORDS(N)];
// 번호 i 는 몇 번째 워드의 몇 번째 비트인가.
// 워드 크기가 2 의 거듭제곱이므로 나눗셈과 나머지를 시프트와 마스크로 적을 수 있다.
static size_t word_of(size_t i) { return i >> 6; } // i / 64
static unsigned bit_of (size_t i) { return i & 63u; } // i % 64
static void bit_set (size_t i) { set[word_of(i)] |= UINT64_C(1) << bit_of(i); }
static void bit_clear (size_t i) { set[word_of(i)] &= ~(UINT64_C(1) << bit_of(i)); }
static int bit_test (size_t i) { return (set[word_of(i)] >> bit_of(i)) & 1u; }
static size_t bit_count(void)
{
size_t total = 0;
for (size_t w = 0; w < WORDS(N); w++)
total += stdc_count_ones(set[w]); // 워드 하나를 한 번에
return total;
}
int main(void)
{
printf("%d flags need %zu bytes as a bitmap, %d bytes as one char each\n",
N, sizeof set, N);
// 에라토스테네스의 체 --- 비트맵의 교과서적 쓰임
memset(set, 0, sizeof set);
for (size_t i = 2; i < N; i++)
bit_set(i); // 일단 전부 「소수일 수 있다」
for (size_t p = 2; p * p < N; p++)
if (bit_test(p))
for (size_t m = p * p; m < N; m += p)
bit_clear(m);
// 워드 끝의 남는 비트는 세지 않도록 지운다 --- 잊기 쉬운 자리다.
for (size_t i = N; i < WORDS(N) * BITS_PER_WORD; i++)
bit_clear(i);
printf("primes below %d: %zu\n", N, bit_count());
printf(" is 997 prime? %s\n", bit_test(997) ? "yes" : "no");
printf(" is 999 prime? %s\n", bit_test(999) ? "yes" : "no");
printf("the first ten:");
size_t shown = 0;
for (size_t i = 0; i < N && shown < 10; i++)
if (bit_test(i)) { printf(" %zu", i); shown++; }
puts("");
return 0;
}
Output
1000 flags need 128 bytes as a bitmap, 1000 bytes as one char each
primes below 1000: 168
is 997 prime? yes
is 999 prime? no
the first ten: 2 3 5 7 11 13 17 19 23 29
Three things carry this demo. First, splitting an index into a word and a bit is a shift and a mask — the word size is a power of two, so i / 64 becomes i >> 6 and i % 64 becomes i & 63. Second, do not forget the spare bits at the end of the last word — counting them means counting elements that do not exist. Third, work a word at a time — calling stdc_count_ones once per word does sixty-four times less looping than walking bit by bit.
Q. Would bit fields (chapter 49) not spare us all this shifting and masking?
A. The syntax gets easier but the promises get thinner. Which end a bit field is filled from, whether it may straddle a word boundary, which types may be used — all implementation-defined (chapter 49), which makes bit fields hard to use for things whose layout is already fixed, such as hardware registers or file formats. The same structure can be laid out differently by another compiler.
So practice splits along that line. Where the layout is nobody’s business but yours — small flags inside an internal data structure — bit fields are convenient; where the layout is a promise made to the outside, hand-written shifts and masks are the safe choice. It is why the Linux kernel and many device drivers choose the latter.
51.9 Recap#
Recap
- There are only five moves — set, clear, flip, test, replace a field. Everything else is a combination.
- A test yields the bit’s value, not 1. Do not compare it against 1.
- A mask has a width.
~0uis not a 64-bit mask, and1u << 32is outside the contract. - Learn why an idiom works rather than the idiom — know why
x & (x-1)clears the lowest set bit and the rest follows. - C23′s
<stdbit.h>gave those idioms standard names. The position functions count from 1, and 0 means “there is none”. - On signed values
>> 1is not/ 2and& 7is not% 8. If division was meant, write division. - The compiler recognises the rotation idiom and Kernighan’s counting loop. Measure before reaching for a trick by hand.
- Where a layout is promised to the outside, prefer hand-written shifts and masks over bit fields.
Bits were the lowest layer of a value. The next chapter is the far side of the same coin — reading the same bunch of bits as a real number. Approximation, error, and what “equal” is supposed to mean.