[MIT OpenCourseWare] Bit Hacks
https://youtu.be/ZusiKXcz_ac?si=2uHDSpjQ7hHJohdj
Set the k-th Bit
Set k-th bit in a word x to 1.
Shift and OR.


Clear the k-th Bit
Clear the k-th bit in a word x.
Shift, completment, and AND.


Toggle the k-th Bit
Flip the k-th bit in a word x.
Shift and XOR.


Extract a Bit Field
Extract a bit field from a word x.
Mask and shift.


압축(Compressed)되어 있거나 인코딩(Encoded)된 데이터를 다룰 때 유용함.
Set a Bit Field
Set a bit field in a word x to a value y.
Invert mask to clear, and OR the shifted value.

For safety's sake : (y << shift) → ((y << shift) & mask)
- y가 높은 비트에 쓰레기 값을 갖고 있으면, 본래의 x 값이 오염될 수 있기 때문에 안전장치를 추가.

Ordinary Swap
Swap two integers x and y.

No-Temp Swap
Swap x and y without using a temporary.


두 번째 y = x ^ y; 가 완료되었을 때, y는 이미 이전의 x 값을 가지고 있음.
마지막으로 x =x ^ y;를 함으로써 Temp variable없이 Swap 성공.
Why it works
XOR is its own inverse :
(x ^ y) ^ y => x

Performance
그다지 좋지 못함. 명령어 수준의 병렬성(Intsruction-Level Parallelism)을 활용하지 못하기 때문.
즉, CPU 레벨에서 슈퍼스칼라 등을 활용하지 못한다는 것인데, 3개의 연산이 전부 x, y를 사용하기 때문에 각각의 instruction이 전부 이전 x, y 결과 값에 의존적이기 때문인듯하다.
→ Temp를 쓴다면, 파이프라인 레벨에서 2개의 instruction을 병렬로 수행가능함.
Minimum of Two Integers
정수 x, y 둘 중 더 작은 값을 r에 저장해보자.
1) if - else statement
2) ternary operator

Performance
둘 다 if branch가 존재한다.
Caveat
현대 컴파일러는 일반적으로 예측불가능한 분기를 제거할만큼 충분히 똑똑하지만, 그렇지 않을 수도 있다.
No-Branch Minimum
분기없이 x, y 중 더 작은 값을 r에 저장하는 방법.

Why it works
우선, True / False가 각각 1과 0에 대응된다는 사실을 알고있다.
그리고 2가지 케이스를 생각해보자.
1) x < y인 경우
-(x < y)는 -1이 되는데, 2의 보수 표현법에서 -1은 모든 비트가 1이라는 것을 알고있다.
따라서 ((x ^ y) & -1)은 (x ^ y)와 동일하므로,
r = y ^ (x ^ y);
앞선 No-Branch Minimum에서, 2번의 xor을 하면 자기자신이라는 것 또한 알고 있으므로,
r = x
2) x >= y인 경우
-(x < y)는 0이 되므로, r = y ^ 0;
r = y
Merging Two Sorted Arrays
[1] Branch
아래 Merge Sort에 사용되는 서브루틴인 merge를 보자.
총 4개의 branch가 존재하는데, 각각 예측가능한지 예상해보자.
FYI) __restrict는 C, A, B가 서로 같은 포인터를 가리킬 경우, 파이프라인상으로 최적화하기 어렵기 때문에 서로 같은 주소가 아니라고 힌트를 주게 된다. 이 경우, 3개의 포인터들은 서로 같은 메모리 주소를 가리키지 않는다고 알려서 추가적인 최적화가 가능하다.

1) while (nb > 0)
- YES
nb가 0인 경우를 제외하고, true이므로 예측가능하다는 것이 밝혀져 있음.
2) while (na > 0)
- YES
1과 동일한 이유로, 예측가능
3) if (*A <= *B)
- NO
*A, *B의 값이 어떤 값인지 알지 못하기 때문에 예측 불가능 (50:50)
4) while (na > 0 && nb > 0)
- YES
na, nb 둘 중 하나가 0이 되기전 까지 true이므로, 예측 가능
[2] Branchless
[1]에서, 3) if (*A <= B*)는 예측이 불가능하므로 하드웨어가 예측되는 명령어를 Prefetch하기 힘들어진다. 따라서, 이전에 배웠던 No-Branch Minimum을 사용하여 추가적인 최적화가 가능해진다.

cmp 값이 (*A <= *B) 결과에 따라 1 또는 0이 되므로,
A == cmp; na -= cmp;
B += !cmp; nb -= !cmp;
위 두 줄의 코드 중 한 줄만이 cmp 값에 따라 유효하게 된다.
Q) 효과가 있을까요?
A) 위 최적화는 일부 머신에서만 효과가 있다.
현대 머신의 clang -O3 옵션을 사용할 경우, branchless 버전이 branching 버전보다 오히려 느리다. 현대 컴파일러는 대부분의 경우에 사용자 프로그래머가 할 수 있는 최적화보다 더 최적화가능하다.
→ 이를 cmov, conditional move라고 하는데, 다음 주에 더 자세히 알아볼 예정
Q) Why Learn Bit Hacks?
Q) 실제로 효과도 없는 이런 야매들을 왜 배우고 있는거죠?
A)
1) 컴파일러가 이러한 비트 트릭을 사용하고 있기 때문인데, 비트 트릭을 이해하고 있다면 어셈블리 코드를 볼 때 컴파일러가 어떤 동작을 하는지 알 수 있다.
2) 컴파일러가 이러한 최적화를 자동으로 수행해주지 않는 경우가 있는데, 그럴 때는 사용자가 직접 최적화해야 함
3) Word에 대한 많은 bit hacks는 고성능 코드에서 널리 사용되는 벡터에 대한 bit/word hacks로 자연스럽게 확장될 수 있음.
4) 이러한 트릭들은 다른 도메인에서도 발생할 수 있기 때문에, 배우는 것 자체로 가치가 있다.
Modular Addition
효과가 있는 한 가지 예제
(0 <= x < n), (0 <= y < n)의 범위를 가정했을 때,
(x + y) mod n 연산을 한다고 해보자.
mod 연산을 할 때 컴파일러는 나눗셈을 수행하게 되는데, 나눗셈은 2의 거듭제곱이 아닌이상 비싼 연산에 해당된다.
컴파일러는 컴파일 타임에 (x + y)가 2의 거듭제곱인지 알 수 없으므로 단순히 나눗셈을 통해 mod 연산을 하게된다.
Case) 나눗셈 대신 Branch를 사용하면?

위 Branch 사용시, z < n의 결과를 예측할 수 없기 때문에 예상되는 결과 instruction을 prefetch하는 것이 불가능해진다.
Approach) 이전에 배웠던 No-Branch Minimum을 사용해보자.

(z >= n)의 값이 1 / 0에 따라 각각 z-n / z 의 결과를 가진다.
Q) z 값에 따라 여전히 분기가 존재하므로 위 최적화가 더 빠른 이유를 모르겠습니다
→ (z >= n)의 값에 따라 true/false로 나뉘는 것이 분기가 아니냐고 묻는듯
A) 서로 다른 2개의 코드 경로로 나뉠 때만 분기 예측이 발생하며, (z >= n)는 bool 값을 생성하므로 분기는 맞지만, 동일한 코드를 생성하므로 분기 예측 오류는 문제가되지 않음.
Round up to a Power of 2

--n;을 하는 이유 :
→ n이 이미 2의 거듭제곱인 경우, 의도한 결과의 2배가 될 수 있기 때문.
n : 8이라고 가정해보자.
--n;을 하지 않을 경우, 0b1000 → 0b10000이 되므로 8이아닌 16이 된다.
따라서 미리 --n;을 통해 0b0111로 만들어 둔다면, 의도한 결과를 넘어가는 것을 막을 수 있다.
Least-Significant 1
Compute the mask of the least-significant 1 in word x.


2의 보수를 취한 -x를 알면, Least-Significant 1를 뽑아낼 수 있다.
Least-Significant 1을 제외하고는 반대 비트이기 때문에 bitwise and로 추출가능.
Why it works
The binary representation of -x is (~x)+1. (2's complement)
Q) Least-Significant 1 의 index를 어떻게 알 수 있을까? 다시 말해서, Log2 (r)
→ 위 예제에서 4를 의미
Log Base 2 of a Power of 2
Compute lg x, where x is a power of 2.

Why it works
먼저, DeBruijin Sequence(드 브루인 수열)이 무엇인지 알아야 한다.
-
N-Queens Problem
'컴퓨터공학 > Performance Engineering' 카테고리의 다른 글
| The Cilk Runtime System (0) | 2026.04.06 |
|---|---|
| Parallel Storage Allocation (0) | 2026.03.30 |
| Why should I learn Software Performance Engineering ? (1) | 2026.03.28 |
| Storage Allocation (0) | 2026.03.27 |
| Synchronization Without Locks (0) | 2025.03.16 |