魔術數字
cpu做除法div比起其他指令耗費大量的性能,如下表
Operands µops µops each port Latency Reciprocal throughputDIV r64 36 p0 p1 p5 p6 35-88 21-83MUL r64 2 p1 p6 3 1SHR r,i 1 p06 1 0.5(本表引用自 https://agner.org/optimize/instruction_tables.pdf 的Intel Coffee Lake章節)
用乘法取代除法
Section titled “用乘法取代除法”乘法和其它簡單操作的效率遠高於一個div,而魔術數字(magic number)的功能就是用乘法取代除法,假設除數固定,需要事前算出一個魔術數
其中:
- :除數
- :次方數, 需要 被除數
然後事先計算出常數m,比如 n = 64 要除以10的話 m = 1844674407370955162
接著在程式用被除數乘以m,再右移n位數
其中:
- :商
- :被除數
- :上面算出的常數
- :上面使用的次方數
- :數學上除以 ,但>>右移是上面的shr非常快的指令
實際上程式做的運算就只有mul和shr得到商
但是需要右移64位的表示商剛好在mul後的rdx
所以mul直接取rdx就是除法得到的商
以下是經由rax取得餘數的方式,若用原本方法將商rdx乘以除數再用被除數減掉更快的話請用原本方法
2560 ÷ 10
Section titled “2560 ÷ 10”我們觀察一個範例
- :2560
- :10
- :64
事先計算出常數 m = 1844674407370955162
然後直接將被除數 乘以 m
以十六進制來看,原本要右移8 bytes等於後面16位數都是之前蓋掉的
前面三位數0x100是商256,那後面0x400是1024,也就是商數乘以4
我們將乘數4以 代稱, 的來源是
也就是 這個同樣可以在事前計算出來
2562 ÷ 10
Section titled “2562 ÷ 10”將 2562 乘以 m 得到
我們關注低64位rax的部分,這是 的結果,當沒有餘數的時候只有商數 q 乘以 (4) 的0x400,現在加上了兩份m,餘數 r = 2,將算式移向得到
這邊又到了div問題,我們將除數帶入最上面的常數m的算式會得到m剛好等於原本的除數10
也就是這次 m = 除數 d,將 rax 做完上面分子的運算之後乘以除數 d 再右移 64 位就是餘數 r
這裡的 如果是2的次方數請用最快的shl左移 shl rdx, 2
65535 ÷ 2
Section titled “65535 ÷ 2”- 經過事前運算得到 m = 9223372036854775808 (0x8000000000000000)
(2**64+d-1)//d # python代碼
- 除數 65535 乘以 m 得到
- 得到商 rdx = 0x7fff = 32767
- 事前計算 得到 0
-(2**64) % 2# 0
- 得到餘數 1
global _start
section .text_start: ; 2562 ÷ 10 mov rax, 2562 ; 被除數 mov rbx, 1844674407370955162 ; magic number mul rbx ; rdx = 商 shl rdx, 2 ; 事前計算得到δ是4 sub rax, rdx mov rbx, 10 mul rbx ; rdx = 餘數