Linux kernel - rtw88-usb : gcc optimization / c coding style analysis

收錄在 AIoT Ameba 2020 - Just for Fun

--

周一有點懶懶的, 剛好上周有段 code, 心中有個疑問, 來做個實驗看看.

上週在 rtw88-usb refine checksum function,
  https://github.com/neojou/rtw88-usb/commit/321ea93df682e45dcffcdb00c2cdae381fdd7a64

看到底下這段 code

https://github.com/ulli-kroll/rtl8822bu/blob/master/hal/halmac/halmac_88xx/halmac_api_88xx.c


checksum 是 2 byte == 16 bit,
這是把 資料, 每 2 byte (16 bit) 依序取出來, 做 xor.
乍看之下沒甚麼問題, 迴圈次數可以少一半,
但這樣寫是否好呢?

==

這問題我們可以簡化成:
   有底下兩個 function
             chksum1() 和 chksum2()
   哪種寫法比較好?

chksum1(data, len/2)  v.s. chksum2(data, len)


https://github.com/neojou/new_ldd/blob/master/gcc-optimization/func.c



==

程式可以從幾個面向來看.

1. 易讀性:
    最近剛好看到一篇: 如何提升你程式可讀性的技巧

    chksum1() 的

                chksum ^= *(data + 2 * i) ^ *(data + 2 * i + 1);

    和 chksum2() 比較: 
 
               checksum ^= *data++
 
           
    應該是 chksum2() 比較容易懂


2. 程式執行效率 和 code size.

     這邊剛好也和 compiler optimization 有關

     例如 gcc 的 optimization
     https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html

     常見的有
               O2:  盡量加強 效能/ 減少 code size
               O3:  更加強效能
               Os:   O2 的幾乎都有, 主要 O2 選項中,
                       若加強效能下會增加 code size 的話則不採用.

    如對 編譯器 的優化 有興趣的話, 底下這篇共筆很值得一看
    https://hackmd.io/@sysprog/c-compiler-optimization?type=view

    我們用 objdump -S 來看看編譯出來的組合語言.
    另外因為 x86 是 CISC 指令集, 每個指令所需要花費執行的 cycle 不同,
    我們也改實驗用 ARM RISC 指令集來編譯看看.

    https://github.com/neojou/new_ldd/blob/master/gcc-optimization/O2.txt

    (1) loop 寫法
         用 for 迴圈方式:

 for (i = 0; i < len; i++)
  14: e3510000  cmp r1, #0
  18: da00000a  ble 48 <chksum1+0x48>
  1c: e1a03000  mov r3, r0
  20: e3a00000  mov r0, #0
  24: e0831101  add r1, r3, r1, lsl #2
....
  2c: e2833004  add r3, r3, #4
   for (i = 0; i < len; i++)
  34: e1510003  cmp r1, r3
....
 for (i = 0; i < len; i++)
  40: 1afffff8  bne 28 <chksum1+0x28>
  44: e89da800  ldm sp, {fp, sp, pc}


這邊 r1 放 len, r3 是 i, 每次移動一個 DWORD (4 byte) 位置計算, 所以是 加 4




         用 while 方式:

        while (len--)
  64: e3510000  cmp r1, #0
  68: e2413001  sub r3, r1, #1
  6c: 0a000005  beq 88 <chksum2+0x38>
  70: e3a01000  mov r1, #0
...
 while (len--)
  78: e2433001  sub r3, r3, #1
  7c: e3730001  cmn r3, #1
...
 while (len--)
  84: 1afffffa  bne 74 <chksum2+0x24>



可以發現 while 只要一個變數 len, 和 0 做比較.
寫法會比 for loop 多一個 i 變數的方式, 來的單純.

所以在 Linux kernel, 能用 while (len--) 這寫法的話,
都會盡量採用這方式, 來取代 for loop 的寫法.




(2) checksum 計算寫法:

    chksum1():

chksum ^= *(data + 2 * i) ^ *(data + 2 * i + 1);
  28: e1d320b0  ldrh r2, [r3]
  2c: e2833004  add r3, r3, #4
  30: e153c0b2  ldrh ip, [r3, #-2]
  38: e022200c  eor r2, r2, ip
  3c: e0200002  eor r0, r0, r2







    

chksum2():

 chksum ^= *data++;
   74: e0d020b2 ldrh r2, [r0], #2
   80: e0211002 eor r1, r1, r2

     這邊可以發現, 在 O2 比較時, 倒還好, 只是單純 ldrh / eor  兩次或一次的差別.
     但如在 for loop 所提, 多的這行
               add r3, r3, #4
     這會降低效能, 增加 code size
.
     反觀 chksum2(): 它的指標移動, 在 74行 和 ldrh 一起做了..

更大的問題會在 我們 用 O3  compile 看的出來.
https://github.com/neojou/new_ldd/blob/master/gcc-optimization/O3.txt

用 O3 編譯, 可以發現 chksum1() 和 O2 時的 code 差不多,
但 chksum2() 可以用到 compiler loop unrolling 的技巧 展開來加速
( loop unrolling 在上方共筆那篇也有提到. )

換言之, chksum1() 的寫法, compiler 其實和人一樣, 被搞混了, 沒能看懂,
但 chksum2() 的寫法, compiler 知道, 這就只是單純把資料每 16 bit 取出來做 xor.
所以有辦法用 loop unrolling 技巧來加速.


結論:

compiler 技術日新月異, gcc 今年也到 10 了, 所以只要多留意寫法, 增加可讀性,
gcc 有 warning 時, 要積極地去除 warning. 不用再自己 "搞剛" 用自己以為的方式去加速.
反造成 compiler 的誤解.


PS: 德國這位仁兄上週 email 聯繫上了, 他也很樂意來開發 rtw88-usb, 加速發展
      https://github.com/ulli-kroll/rtw88-usb

      也歡迎有興趣的朋友, 一起來開發. ^^

留言

熱門文章