[心得] 使用Structure來存取暫存器

看板C_and_CPP作者 (humanforestQQ)時間5年前 (2019/03/15 21:54), 5年前編輯推噓7(7024)
留言31則, 9人參與, 5年前最新討論串1/1
本版首Po, 查了一下好像沒有相關討論, 請大大們鞭小力一點QQ 網誌好讀版: http://tinyurl.com/y677eu2f 最近從同事那裡聽到這個小技巧, 分享給大家 Tip: 建議盡量使用structure來存取Register,可以獲得以下好處 1. 讓compiler對base address計算做最佳化 (with -O1),讓程式更有效率 2. 易寫、易讀、易懂! 正文開始! 讓compiler對base address計算做最佳化 (with -O1),讓程式更有效率 基本概念: Placing C variables at specific addresses to access memory-mapped peripherals The ARM compiler will normally use a ‘base register’ plus the immediate offset field available in the load/store instruction to compile struct member or specific array element access. In the ARM instruction set, LDR/STR word/byte instructions have a 4KB range, but LDRH/STRH instructions have a smaller immediate offset of 256 bytes. Equivalent 16-bit Thumb instructions are much more restricted - LDR/STR have a range of 32 words, LDRH/STRH have a range of 32 halfwords and LDRB/STRB have a range of 32 bytes. However, 32-bit Thumb instructions offer a significant improvement. Hence, it is important to group related peripheral registers near to each other if possible. The compiler will generally do a good job of minimising the number of instructions required to access the array elements or structure members by using base registers. 以上大意上是說ARM compiler原本就會使用base register加上offset來 對struct member與array element來做存取,所以如果我們將一組連續位置的register用 struct或array來定義,就可以也套用上述的base register存取方式。 直接看例子比較快,如果我們直接用下面這樣的方法去寫A/B/C #define REG_BASE_ADDR (0x10000000FFFFF00) #define REG_A (REG_BASE_ADDR + 0x8) #define REG_B (REG_BASE_ADDR + 0x10) #define REG_C (REG_BASE_ADDR + 0x18) #define READ_REG(reg, val) val = *((volatile unsigned long *) (reg)) #define WRITE_REG(reg, val) *((volatile unsigned long *) (reg)) = val void foo(unsigned long a_val, unsigned long b_val, unsigned long c_val){ WRITE_REG(REG_A, a_val); WRITE_REG(REG_B, b_val); WRITE_REG(REG_C, c_val); } 從Compiler Explorer(ARM64 GCC 8.2 -O2)測試的assembly結果如下 (https://godbolt.org/z/3MRiMJ) 可以看到需要分別計算A/B/C register的base address(Line2~10)才能寫值。 foo: mov x5, 65288 mov x4, 65296 movk x5, 0xfff, lsl 16 movk x4, 0xfff, lsl 16 movk x5, 0x100, lsl 48 mov x3, 65304 movk x4, 0x100, lsl 48 movk x3, 0xfff, lsl 16 movk x3, 0x100, lsl 48 str x0, [x5] str x1, [x4] str x2, [x3] ret 而如果改成下面的寫法,利用structure來存取 (https://godbolt.org/z/g-eJmz) #define REG_BASE_ADDR (0x10000000FFFFF00) typedef struct { unsigned long BASE; unsigned long REG_A; unsigned long REG_B; unsigned long REG_C; } my_register; #define READ_REG(reg, val) do{ \ volatile my_register *base = (my_register *) REG_BASE_ADDR; \ val = base->reg; \ } while(0) #define WRITE_REG(reg, val) do{ \ volatile my_register *base = (my_register *) REG_BASE_ADDR; \ base->reg = val; \ } while(0) void foo(unsigned long a_val, unsigned long b_val, unsigned long c_val){ WRITE_REG(REG_A, a_val); WRITE_REG(REG_B, b_val); WRITE_REG(REG_C, c_val); } 產生的Assembly如下,可以看到只需要去計算Base Address一次,並存在base register x3,接著直接透過offset去讀A/B/C,整整少了一半的指令數!對於斤斤計較MCPS的Hard Real Time Context來說可是有天壤之別! foo: mov x3, 268435200 movk x3, 0x100, lsl 48 str x0, [x3, 8] str x1, [x3, 16] str x2, [x3, 24] ret 易寫、易讀、易懂! 再來看第二個優點,這點對我來說跟甚至比程式效率還重要,而這其實也是Structure本 身最大的用途:用對工程師最友善的方式來描述資料 例如Register A的Spec如下 (little-endian) Bit | 0 1 2 3 4 5 6 7 8------15-------------- 63 | Field | X | Y | Z | W | 假如要對Y寫值(故意挑個中間的),以 bit operation操作的話會寫成如下,我想看到 SET_REG_A_Y_FIELD就知道我的意思了吧! 實在是有夠麻煩,當Regitser / Field一多根 本沒辦法維護。 #define REG_BASE_ADDR (0x10000000FFFFF00) #define REG_A (REG_BASE_ADDR + 0x8) #define REG_B (REG_BASE_ADDR + 0x10) #define REG_C (REG_BASE_ADDR + 0x18) #define READ_REG(reg, val) val = *((volatile unsigned long *) (reg)) #define WRITE_REG(reg, val) *((volatile unsigned long *) (reg)) = val #define SET_REG_A_Y_FIELD(val) do { \ unsigned long cur_val = READ_REG(REG_A, cur_val); \ cur_val = (cur_val & ~0xF0) | ( (val & 0xF) << 0x4); \ WRITE_REG(REG_A, cur_val); \ } while (0) void foo(unsigned char n){ SET_REG_A_Y_FIELD(n); } Compile Result (ARM64 GCC 8.2 -O2) https://godbolt.org/z/kftmDw foo: mov x2, 65288 ubfiz x0, x0, 4, 4 movk x2, 0xfff, lsl 16 movk x2, 0x100, lsl 48 ldr x1, [x2] and x1, x1, -241 orr x0, x0, x1 str x0, [x2] ret 而Structure的寫法如下,有沒有非常簡單!? 甚至,能夠指定任意的Field,而且指令數 還更少!! 因為ARM(MIPS /x86也有)有支援直接對Register某一群bit讀/寫值,下面的bfi 就是,這樣就可以省掉AND與OR操作了。 (Reference: 3.8.1. BFC and BFI) BFI copies a bitfield into one register from another register. It replaces width bits in Rd starting at the low bit position lsb, with width bits from Rn starting at bit[0]. Other bits in Rd are unchanged. #define REG_BASE_ADDR (0x10000000FFFFF00) typedef struct { unsigned long X:4; unsigned long Y:4; unsigned long Z:8; unsigned long W:48; } reg_a_t; typedef struct { unsigned long BASE; reg_a_t REG_A; unsigned long REG_B; unsigned long REG_C; } my_register; #define READ_REG(reg, val) do{ \ volatile my_register *base = (my_register *) REG_BASE_ADDR; \ val = base->reg; \ } while(0) #define WRITE_REG(reg, val) do{ \ volatile my_register *base = (my_register *) REG_BASE_ADDR; \ base->reg = val; \ } while(0) #define SET_REG_A_FIELD(field, val) do { \ WRITE_REG(REG_A.field, val); \ } while (0) void foo(unsigned char n){ SET_REG_A_FIELD(Y,n); } Compile Result (ARM64 GCC 8.2 -O2) https://godbolt.org/z/eIyDNY foo: mov x1, 268435200 movk x1, 0x100, lsl 48 ldr x2, [x1, 8] bfi x2, x0, 4, 4 str x2, [x1, 8] ret 順帶一提,其實近年來compiler的優化已經越做越極致了,在想自幹assembly前先去翻翻 GCC Optimization Option還有用Compiler Explorer玩一玩再決定吧!! -- ※ 發信站: 批踢踢實業坊(ptt.cc), 來自: 36.229.45.83 ※ 文章網址: https://www.ptt.cc/bbs/C_and_CPP/M.1552658080.A.27D.html ※ 編輯: hsnuer1171 (36.229.45.83), 03/15/2019 21:55:57

03/16 00:02, 5年前 , 1F
優文 前陣子翻bios的code也有看到類似的trick
03/16 00:02, 1F

03/16 01:15, 5年前 , 2F
雖然知道這件事 但第一次看到分析 推個
03/16 01:15, 2F

03/16 18:32, 5年前 , 3F
感謝分享
03/16 18:32, 3F

03/16 22:11, 5年前 , 4F
推,我寫 driver 也會用
03/16 22:11, 4F

03/17 22:18, 5年前 , 5F
如果是driver的跨平台層,其實不建議使用bit field
03/17 22:18, 5F

03/17 22:19, 5年前 , 6F
因為bit field 是 implementation-defined
03/17 22:19, 6F

03/17 22:20, 5年前 , 7F
同一份code在Linux, window, Mac compile有可能會得到不同
03/17 22:20, 7F

03/17 22:20, 5年前 , 8F
的結果
03/17 22:20, 8F

03/18 22:40, 5年前 , 9F
不建議bitfield+1,不過其實他會被ABI規範.x86-32沒統一
03/18 22:40, 9F

03/18 22:40, 5年前 , 10F
msvc自成一套. 通常看endian會決定順序,又分能不能跨unit
03/18 22:40, 10F

03/18 22:41, 5年前 , 11F
arm32可以跨,跟大家比較不一樣.
03/18 22:41, 11F

03/18 22:42, 5年前 , 12F
GCC的instruction select很強,不需要bitfield就可以match
03/18 22:42, 12F

03/18 22:42, 5年前 , 13F
出bitfield extrac/insert的行為.
03/18 22:42, 13F

03/18 22:44, 5年前 , 14F
GCC標準pattern就有extract/insert. LLVM比較笨,但寫成
03/18 22:44, 14F

03/18 22:44, 5年前 , 15F
bitfield其實對LLVM match沒幫助 XD
03/18 22:44, 15F

03/18 22:47, 5年前 , 16F
第一部份有點算a64的指令offset範圍的關係, compiler不好
03/18 22:47, 16F

03/18 22:48, 5年前 , 17F
處理. 試用mips或risc-v,high-part可以共用, low-part
03/18 22:48, 17F

03/18 22:48, 5年前 , 18F
剛好可以encode到ld/st的offset, 所以兩種寫法其實會生出
03/18 22:48, 18F

03/18 22:48, 5年前 , 19F
一模一樣的code
03/18 22:48, 19F

03/18 22:49, 5年前 , 20F
另外, 第一個問題其實LLVM會處理
03/18 22:49, 20F

03/18 22:52, 5年前 , 21F
不過有點難講,如果用單一base,會拉長register live range
03/18 22:52, 21F

03/18 22:53, 5年前 , 22F
搞不好會生出比較差的code? GCC會在決定register後才再
03/18 22:53, 22F

03/18 22:54, 5年前 , 23F
嘗試做這樣的optimize,可能是gcc的策略吧..
03/18 22:54, 23F

03/18 22:55, 5年前 , 24F
以你的例子,因為是配到x3,x4,x5,所以gcc會消不掉
03/18 22:55, 24F

03/18 22:55, 5年前 , 25F
配相同的reg會有depdency問題,配不同reg可以平行執行..
03/18 22:55, 25F

03/18 22:55, 5年前 , 26F
有時難講怎樣比較好 orz
03/18 22:55, 26F

03/18 22:57, 5年前 , 27F
我手邊gcc,用Os會被消掉其中一組
03/18 22:57, 27F

03/18 23:28, 5年前 , 28F
太專業了!! 感謝大家的建議 小弟再研讀一下以免誤導大
03/18 23:28, 28F

03/18 23:28, 5年前 , 29F
03/18 23:28, 29F

03/20 12:52, 5年前 , 30F
我也不喜歡用bitfield+1,光移植問題就很麻煩了
03/20 12:52, 30F

03/25 16:31, 5年前 , 31F
03/25 16:31, 31F
文章代碼(AID): #1SYwwW9z (C_and_CPP)