Try   HackMD

2023q1 Homework2 (quiz2)

contributed by < lorian0738 >

你所不知道的 C 語言: bitwise 操作 ( 隨記 )

一個字元 1 byte (位元組)

數值系統

  • 數值系統的設計導致不同程式語言都會出現一些計算上的誤差
    例如:0.1-0.01=0.09000000000000001 (python 2.7)

運用 bit-wise operator

  • 大小寫轉換
    • 將字元轉成小寫: 免除使用分支
      ('a' | ' ') // 得到 'a'
      ('A' | ' ') // 得到 'a'
    • 將字元轉為大寫: 免除使用分支
      ('a' & '_') // 得到 'A'
      ('A' & '_') // 得到 'A'
    • 大小寫互轉: 避免使用分支
      ('a' ^ ' ') // 得到 'A'
      ('A' ^ ' ') // 得到 'a'
    ​​​​'a' 的二進位表示 1100001
    ​​​​'A' 的二進位表示 1000001
    ​​​​' ' 的二進位表示  100000
    ​​​​'_' 的二進位表示 1011111
    

交換兩個記憶體空間內的數值,可完全不用額外的記憶體來實作

在 boot manager 或 boot loader 資源有限
主記憶體可能不能用

void xorSwap(int *x, int *y) {
    *x ^= *y;
    *y ^= *x;
    *x ^= *y;
}

位移運算子

  • 未定義狀況
  1. 左移超過變數長度
    ​​​​int i=0xFFFFFFFF;
    ​​​​i = i << 32; // 此結果未定義
    
    輸出:
    ​​​​main.c:14:11: warning: left shift count >= width of type [-Wshift-count-overflow]
    ​​​​   14 |     i = i << 32; // 此結果未定義
    ​​​​      |           ^~
    ​​​​ffffffff
    
  2. 右移一個負數時,可能是邏輯位移(左側補 0 )或是算術位移(補上號數)

signed 和 unsigned 之間的關係

  • 無窮迴圈
int n = 10; 
for  i = n - 1 ; i - sizeof(char) >= 0; i--)
    printf("i: 0x%x\n",i);

sizeof 不是 function 是 operator (運算子)
=> 回傳型態:unsigned interger
整數型態和 unsigned interger 做運算時,會整個變成 unsigned interger

quiz 2

測驗 1

考慮 next_pow2 可針對給定無號 64 位元數值 x,找出最接近且大於等於 2 的冪的值,例如:

  • next_pow2(7) = 8
  • next_pow2(13) = 16
  • next_pow2(42) = 64
uint64_t next_pow2(uint64_t x)
{
    x |= x >> 1;
    x |= x >> 1;
    x |= x >> 1;
    x |= x >> 1;
    x |= x >> 1;
    x |= x >> 1;
    x |= x >> 1;
    x |= x >> 8;
    x |= x >> 16;
    x |= x >> 32;
    return x+1;
}

上述程式碼將 x 和自己右移的值做 bitwise or 運算,目的是希望將比 x 的 MSB 低位的數字都變成 1,因此總共右移了 63 個位元,如此再 +1 進位後就會是想找的 2 的冪的值

疑惑點:
題目描述要找「最接近且大於等於 2 的冪的值」指的應該是最接近 x 且大於等於 x 的 2 的冪的值,但若是 x 本身就是 2 的冪,例如 1000₂ ,做完運算後會得到 10000₂ ,但依照題目敘述應該求的是 1000₂

1. 以 __builtin_clzl 改寫

int __builtin_clzl (unsigned long) 會回傳前導 0 的位數,例如 input 為 9 (1001₂) 時,回傳 64 - 4 = 60

則可以知道最高有效位在最低位往高位數第 4 位,可知想求的值為 1 左移四個位數,即為 10000₂

uint64_t next_pow2(uint64_t x)
{
    int len = 64 - __builtin_clzl(x);
    return 1 << len;
}

經同學提醒,才想到 return 那邊若直接寫 1 會預設是 int ,因此若輸入超過 32 位元的值會溢位,需要特別標示型別為 uint64_t

uint64_t next_pow2(uint64_t x)
{
    int len = 64 - __builtin_clzl(x);
    return (uint64_t)1 << len;
}

2. 在 Linux 核心原始程式碼找出類似的使用案例並解釋

3. 當上述 clz 內建函式已運用時,編譯器能否產生對應的 x86 指令?

提示: 可執行 cc -O2 -std=c99 -S next_pow2.c 並觀察產生的 next_pow2.s 檔案,確認 bsrq 指令 (表示 “Bit Scan Reverse”)

	.file	"next_pow2.c"
	.text
	.p2align 4
	.globl	next_pow2
	.type	next_pow2, @function
next_pow2:
.LFB13:
	.cfi_startproc
	endbr64
	bsrq	%rdi, %rdi
	movl	$64, %ecx
	movl	$1, %eax
	xorq	$63, %rdi
	subl	%edi, %ecx
	sall	%cl, %eax
	cltq
	ret
	.cfi_endproc
    
    ...
    
    main:
.LFB14:
	.cfi_startproc
	endbr64
	subq	$24, %rsp
	.cfi_def_cfa_offset 32
	leaq	.LC0(%rip), %rdi
	movq	%fs:40, %rax
	movq	%rax, 8(%rsp)
	xorl	%eax, %eax
	movq	%rsp, %rsi
	call	__isoc99_scanf@PLT
	bsrq	(%rsp), %rax
    ...

測驗 2

LeetCode 1680. Concatenation of Consecutive Binary Numbers 給定一整數 n ,回傳將 1 到 n 的二進位表示法依序串接在一起所得到的二進位字串,其所代表的十進位數字

mod 109+7 之值。

int concatenatedBinary(int n)
{
    const int M = 1e9 + 7;
    int len = 0; /* the bit length to be shifted */
    /* use long here as it potentially could overflow for int */
    long ans = 0;
    for (int i = 1; i <= n; i++) {
        /* removing the rightmost set bit
         * e.g. 100100 -> 100000
         *      000001 -> 000000
         *      000000 -> 000000
         * after removal, if it is 0, then it means it is power of 2
         * as all power of 2 only contains 1 set bit
         * if it is power of 2, we increase the bit length
         */
        if (!(i & (i-1)))
            len++;
        ans = (i | (ans << len)) % M;
    }
    return ans;
}

1. 解釋上述程式碼運作原理

此程式碼將 1 到 n 依序串接進 ans,若 i & (i-1) 為 0,表示 i 為 2 的冪,則 i 比 i - 1 的位數多一位,ans 在左移準備串接 i 時,要比串接 i - 1 時多左移一位,因此將記錄左移位數的 len + 1。
再利用左移足夠位數後填補的 0 的部分,與要串接的數字做 bitwise or 達到二進位制相加的效果。

例如當 n 為 4 時:

i = 1 時,1&0 為 0,len + 1,len = 1,ans = 1
i = 2 時,1&0 為 0,len + 1,len = 2,ans = 110
i = 3 時,1&0 非 0,len + 1,len = 2,ans = 11011
i = 4 時,1&0 為 0,len + 1,len = 3,ans = 11011110

2. 嘗試使用 _builtin{clz,ctz,ffs} 改寫,並改進
mod 109+7
的運算

clz:回傳 leading 0-bits
ctz:回傳 trailing 0-bits
ffs:回傳最低有效位索引值 + 1
int concatenatedBinary(int n)
{
    const int M = 1e9 + 7;
    int len = 0;
    long ans = 0;
    for (int i = 1; i <= n; i++) {
        len = 32 - __builtin_clz (i);
        ans = (i | (ans << len)) % M;
    }
    return ans;
}

測驗 3

SWAR 實作如下:

size_t swar_count_utf8(const char *buf, size_t len)
{
    const uint64_t *qword = (const uint64_t *) buf;
    const uint64_t *end = qword + len >> 3;

    size_t count = 0;
    for (; qword != end; qword++) {
        const uint64_t t0 = *qword;
        const uint64_t t1 = ~t0;
        const uint64_t t2 = t1 & 0x04040404040404040llu;
        const uint64_t t3 = t2 + t2;
        const uint64_t t4 = t0 & t3;
        count += __builtin_popcountll(t4);
    }

    count = (1 << 3) * (len / 8)  - count;
    count += (len & 7) ? count_utf8((const char *) end, len & 7) : 0;

    return count;
}

首先把字串轉為 uint64_t 的形式存在 *qword
接著 len >> 3 即為 len / 8,長度除以 8 (無條件捨去到整數位) 可知該字串有幾個 8 bytes(64 bits) 一組,加上 qword 可得最後一組的位址

題目中提到:
若輸入的字串是一個有效的 UTF-8 序列,則計算其中的後續位元組數量,並將此數字從總字串長度中減去,即可確定字元數量。
後續位元組的位原模式如題目所述為 not bit6 and bit7

因此接著跑 for 迴圈一次處理 64 個位元,計算想求的後續位元組數量:(如測驗題目說明)

  1. 輸入的位元組
  2. 反向運算 (not bit6)
  3. 隔離反向的第 6 個位元
  4. 對第 6 個位元,向左位移 1 個位元 (移至第 7 個位元的位置)
  5. 進行 not bit6 and bit7
  6. 以 population count 指令計算,即 popcount(t4)

再來將算出來的數字 count 用總字串長度減去
而須注意的是目前只做了可以 64 bits 一組處理的部分,也就是這裡的總長度不應該用原本字串的總長度,而是要先用 len / 8 計算做了幾組,再乘 8 (1 << 3),算出目前已計算的總長度,再減去 count

最後計算有沒有多出來不能 64 bits 一組一起算的部分,以 len & 7 判斷,也就是 len % 8,若有的話再用 count_utf8 計算並加進 count,其長度為 len & 7,也就是以 8 bytes 為一組剩餘的長度;若沒有的話就加 0。

測驗 4

判定 16 位元無號整數是否符合特定樣式 (pattern):

#include <stdint.h>
#include <stdbool.h>

bool is_pattern(uint16_t x)
{
    if (!x)
        return 0;

    for (; x > 0; x <<= 1) {
        if (!(x & 0x8000))
            return false;
    }

    return true;
}

可以看出符合程式碼的樣式:

pattern: 8000 (32768) => 1000 0000 0000 0000
pattern: c000 (49152) => 1100 0000 0000 0000
pattern: e000 (57344) => 1110 0000 0000 0000
pattern: f000 (61440) => 1111 0000 0000 0000
pattern: f800 (63488) => 1111 1000 0000 0000
pattern: fc00 (64512) => 1111 1100 0000 0000
pattern: fe00 (65024) => 1111 1110 0000 0000
pattern: ff00 (65280) => 1111 1111 0000 0000
pattern: ff80 (65408) => 1111 1111 1000 0000
pattern: ffc0 (65472) => 1111 1111 1100 0000
pattern: ffe0 (65504) => 1111 1111 1110 0000
pattern: fff0 (65520) => 1111 1111 1111 0000
pattern: fff8 (65528) => 1111 1111 1111 1000
pattern: fffc (65532) => 1111 1111 1111 1100
pattern: fffe (65534) => 1111 1111 1111 1110
pattern: ffff (65535) => 1111 1111 1111 1111

可觀察到從最左邊開始為連續的 1 ,接著是連續的 0

更精簡的程式碼:

bool is_pattern(uint16_t x)
{
    const uint16_t n = ~x + 1;
    return (n ^ x) < x;
}

這種樣式下將 x 做 bitwise not 會將原本的 1 變成 0、0 變成 1,例如:1111 0000 0000 0000 變成 0000 1111 1111 1111,加一後變成 0001 0000 0000 0000,與 x 做 bitwise xor 後會將 x 最低位的 1 去掉,得到比 x 小的值。

而若非此樣式的,例如 1101 0000 0000 0000,非連續 1 接上 0,bitwise not 後會變成 0010 1111 1111 1111,加 1 後變成 0011 0000 0000 0000,會有兩個位數是 1 ,再做 bitwise xor 會變成 1110 0000 0000 0000 反而會進位。

而 1 接上非連續 0 的情況,例如 1111 0001 0000 0000,bitwise not 再加一後會變成 0000 1111 0000 0000,也會有不只一個位數是 0,做 bitwise 後會變成 1111 1110 0000 0000,數值也會比較大。

可知只有連續 1 接連續 0 的情況下作這些操作後可去掉最低位的 1 ,得到比原本更小的數值。