verilog 浮点转定点_定点数优化:性能成倍提升
定點數這玩意兒并不是什么新東西,早年 CPU 浮點性能不夠,定點數技巧大量活躍于各類圖形圖像處理的熱點路徑中。今天 CPU 浮點上來了,但很多情況下整數仍然快于浮點,因此比如:libcario (gnome/quartz 后端)及 pixman 之類的很多庫里你仍然找得到定點數的身影。那么今天我們就來看看使用定點數到底能快多少。
簡單用一下的話,下面這幾行宏就夠了:
#define cfixed_from_int(i) (((cfixed)(i)) << 16) #define cfixed_from_float(x) ((cfixed)((x) * 65536.0f)) #define cfixed_from_double(d) ((cfixed)((d) * 65536.0)) #define cfixed_to_int(f) ((f) >> 16) #define cfixed_to_float(x) ((float)((x) / 65536.0f)) #define cfixed_to_double(f) ((double)((f) / 65536.0)) #define cfixed_const_1 (cfixed_from_int(1)) #define cfixed_const_half (cfixed_const_1 >> 1) #define cfixed_const_e ((cfixed)(1)) #define cfixed_const_1_m_e (cfixed_const_1 - cfixed_const_e) #define cfixed_frac(f) ((f) & cfixed_const_1_m_e) #define cfixed_floor(f) ((f) & (~cfixed_const_1_m_e)) #define cfixed_ceil(f) (cfixed_floor((f) + 0xffff)) #define cfixed_mul(x, y) ((cfixed)((((int64_t)(x)) * (y)) >> 16)) #define cfixed_div(x, y) ((cfixed)((((int64_t)(x)) << 16) / (y))) #define cfixed_const_max ((int64_t)0x7fffffff) #define cfixed_const_min (-((((int64_t)1) << 31))) typedef int32_t cfixed;類型狂可以寫成 inline 函數,封裝狂可以封裝成一系列 operator xx,如果需要更高的精度,可以將上面用 int32_t 表示的 16.16 定點數改為用 int64_t 表示的 32.32 定點數。
那么我們找個浮點數的例子優化一下吧,比如 libyuv 中的 ARGBAffineRow_C 函數:
void ARGBAffineRow_C(const uint8_t* src_argb,int src_argb_stride,uint8_t* dst_argb,const float* uv_dudv,int width) {int i;// Render a row of pixels from source into a buffer.float uv[2];uv[0] = uv_dudv[0];uv[1] = uv_dudv[1];for (i = 0; i < width; ++i) {int x = (int)(uv[0]);int y = (int)(uv[1]);*(uint32_t*)(dst_argb) = *(const uint32_t*)(src_argb + y * src_argb_stride + x * 4);dst_argb += 4;uv[0] += uv_dudv[2];uv[1] += uv_dudv[3];} }這個函數是干什么用的呢?給圖像做 仿射變換(affine transformation) 用的,比如 2D 圖像庫或者 ActionScript 中可以給 Bitmap 設置一個 3x3 的矩陣,然后讓 Bitmap 按照該矩陣進行變換繪制:
基本上二維圖像所有:縮放,旋轉,扭曲都是通過仿射變換完成,這個函數就是從圖像的起點(u, v)開始按照步長(du, dv)進行采樣,放入臨時緩存中,方便下一步一次性整行寫入 frame buffer。
這個采樣函數有幾個特點:
- 運算簡單:沒有復雜的運算,計算無越界,不需要求什么 log/exp 之類的復雜函數。
- 范圍可控:大部分圖像長寬尺寸都在 32768 范圍內,用 16.16 的定點數即可。
- 轉換頻繁:每個點的坐標都需要從浮點轉換成整數,這個操作很費事。
適合用定點數簡單重寫一下:
void ARGBAffineRow_Fixed(const uint8_t* src_argb,int src_argb_stride,uint8_t* dst_argb,const float* uv_dudv,int width) {int32_t u = (int32_t)(uv_dudv[0] * 65536); // 浮點數轉定點數int32_t v = (int32_t)(uv_dudv[1] * 65536);int32_t du = (int32_t)(uv_dudv[2] * 65536);int32_t dv = (int32_t)(uv_dudv[3] * 65536);for (; width > 0; width--) {int x = (int)(u >> 16); // 定點數坐標轉整數坐標int y = (int)(v >> 16);*(uint32_t*)(dst_argb) = *(const uint32_t*)(src_argb + y * src_argb_stride + x * 4);dst_argb += 4;u += du; // 定點數加法v += dv;} }局部用一下定點數都不需要定義前面那一堆宏,按相關原理直接寫就是了。
我們用 llvm-mca 分析一下,浮點數版本 gcc 9.0 的循環主體部分用 -O3 的代碼生成:
Iterations: 100 Instructions: 1300 Total Cycles: 458 Total uOps: 1500Dispatch Width: 6 uOps Per Cycle: 3.28 IPC: 2.84 Block RThroughput: 3.0Instruction Info: [1]: #uOps [2]: Latency [3]: RThroughput [4]: MayLoad [5]: MayStore [6]: HasSideEffects (U)[1] [2] [3] [4] [5] [6] Instructions:2 6 1.00 cvttss2si eax, xmm11 1 0.25 add rsi, 41 4 0.50 addss xmm1, xmm22 6 1.00 cvttss2si edx, xmm01 4 0.50 addss xmm0, xmm31 3 1.00 imul eax, r9d1 1 0.50 shl edx, 21 1 0.25 cdqe1 1 0.25 add rax, rdi1 5 0.50 * mov eax, dword ptr [rax + rdx]1 1 1.00 * mov dword ptr [rsi - 4], eax1 1 0.25 cmp rsi, rcx1 1 0.50 jne .L3鏈接:Compiler Explorer - Analysis (llvm-mca (trunk))
可以看到,雖然編譯器自動生成了 sse 代碼,但性能消耗的大戶,cvttss2si(浮點數轉整數指令),雖然只有一條命令,但會生成兩個微指令(uOP),延遲 6 個周期,rthroughput 很高 1.0 代表每周期只能同時運行一條該指令,其次是加法指令 addss, 延遲是 4 個周期,吞吐量 0.5 代表每周期可以并行執行 2 條,該代碼塊模擬運行 100 次,總消耗 458 個周期。
再看定點數版本:
Iterations: 100 Instructions: 1500 Total Cycles: 337 Total uOps: 1500Dispatch Width: 6 uOps Per Cycle: 4.45 IPC: 4.45 Block RThroughput: 2.5Instruction Info: [1]: #uOps [2]: Latency [3]: RThroughput [4]: MayLoad [5]: MayStore [6]: HasSideEffects (U)[1] [2] [3] [4] [5] [6] Instructions:1 1 0.25 mov eax, edi1 1 0.25 mov edx, ecx1 1 0.25 add rsi, 41 1 0.25 add ecx, r11d1 1 0.50 sar eax, 161 1 0.50 sar edx, 161 1 0.25 add edi, ebx1 3 1.00 imul eax, r10d1 1 0.50 shl edx, 21 1 0.25 cdqe1 1 0.25 add rax, r91 5 0.50 * mov eax, dword ptr [rax + rdx]1 1 1.00 * mov dword ptr [rsi - 4], eax1 1 0.25 cmp rsi, r81 1 0.50 jne .L8鏈接:https://godbolt.org/z/Adj9gQ
指令雖然多了兩條,但是整數指令 latency 比浮點要低,并且吞吐量(并行性)比浮點更好,大部分指令都是 0.25,代表每周期可以并行執行四條,模擬運行 100 次,總消耗 337 個周期。
這里你可能要問,整數版本,第二列 latency 加起來有 21 個周期啊,為什么平均運行一次才 3.37 個周期呢?這就是多級流水線中很多指令可以并行運行,只要沒有運算結果依賴,以及沒有執行單元的資源沖突,很多運算都是可以并行的,所以優化里,運算解依賴很有用。
我們給 llmv-mca 加一個 -timeline 參數,能得到流水線分析報表,這里截取兩次循環:
[0,0] DeER . . . . . . . . . mov eax, edi [0,1] DeER . . . . . . . . . mov edx, ecx [0,2] DeER . . . . . . . . . add rsi, 4 [0,3] DeER . . . . . . . . . add ecx, r11d [0,4] D=eER. . . . . . . . . sar eax, 16 [0,5] D=eER. . . . . . . . . sar edx, 16 [0,6] .DeER. . . . . . . . . add edi, ebx [0,7] .D=eeeER . . . . . . . . imul eax, r10d [0,8] .D=eE--R . . . . . . . . shl edx, 2 [0,9] .D====eER . . . . . . . . cdqe [0,10] .D=====eER. . . . . . . . add rax, r9 [0,11] .D======eeeeeER. . . . . . . mov eax, dword ptr [rax + rdx] [0,12] . D==========eER . . . . . . mov dword ptr [rsi - 4], eax [0,13] . DeE----------R . . . . . . cmp rsi, r8 [0,14] . D=eE---------R . . . . . . jne .L8 [1,0] . DeE----------R . . . . . . mov eax, edi [1,1] . D=eE---------R . . . . . . mov edx, ecx [1,2] . D=eE---------R . . . . . . add rsi, 4 [1,3] . DeE---------R . . . . . . add ecx, r11d [1,4] . D=eE--------R . . . . . . sar eax, 16 [1,5] . D=eE--------R . . . . . . sar edx, 16 [1,6] . D=eE--------R . . . . . . add edi, ebx [1,7] . D==eeeE-----R . . . . . . imul eax, r10d [1,8] . D==eE-------R . . . . . . shl edx, 2 [1,9] . D====eE----R . . . . . . cdqe [1,10] . D=====eE---R . . . . . . add rax, r9 [1,11] . D======eeeeeER . . . . . . mov eax, dword ptr [rax + rdx] [1,12] . D===========eER . . . . . . mov dword ptr [rsi - 4], eax [1,13] . DeE-----------R . . . . . . cmp rsi, r8 [1,14] . D==eE---------R . . . . . . jne .L8每條指令有下面幾個生存周期:
D : Instruction dispatched. e : Instruction executing. E : Instruction executed. R : Instruction retired. = : Instruction already dispatched, waiting to be executed. - : Instruction executed, waiting to be retired.可以看到無依賴的頭四條指令:
[0,0] DeER . . . . . . . . . mov eax, edi [0,1] DeER . . . . . . . . . mov edx, ecx [0,2] DeER . . . . . . . . . add rsi, 4 [0,3] DeER . . . . . . . . . add ecx, r11d從分發(D),執行(e),完成執行(E)到退休(R),基本都是同時進行的。后面兩條指令雖然也是和前面四條一起分發(D),但由于依賴頭兩條指令的運算結果,所以產生了一個周期的等待(=):
[0,4] D=eER. . . . . . . . . sar eax, 16 [0,5] D=eER. . . . . . . . . sar edx, 16等到頭兩個指令執行成功(e結束),他們才能開始執行,第二次迭代 [1,0] 類似,多次迭代雖然用到了同樣的寄存器,但是在 CPU 里 eax 只是個名字,CPU 對無關運算的寄存器進行重命名后,其實背后對應到了不同的寄存器地址,第二次迭代又有很多地方可以和第一次迭代并行執行,所以我們會發現兩次迭代的最后一條指令 [0,14] 和 [1,14] 處理的退休時間 R 都差不多,兩次循環幾乎是并行執行的,如此多次循環平攤下來每次只要 3.37 個周期。
可以發現比浮點數版本的 4.58 個周期快了 35% 左右,注意一點,實際 I/O 操作會占用更多時間,所以在 mca 的分析里都標注了 MayLoad / MayStore,所以算上 I/O,兩邊的周期數都會略有增加,但是優勢還在那里。
使用 mca 進行分析的時候,你把編譯結果貼過去時,只能貼循環的主體部分,因為本身就要進行多次運行的流水線模擬,所以你貼了循環外的初始化部分就會干擾分析結果。
可能有人會說,媽呀,靜態性能分析還要計算流水線么?其實大部分時候不需要,沒有 llvm-mca 的時候我們做靜態性能分析一般就是查指令手冊:
大部分時候,看一下 uops 數量(越少越好),看一下周期 latency(越少越好),以及 throughput (決定并行效率,越低越好),心中對各類指令的占用消耗有一個基本概念,再細致一點的話還可以看看占用哪些硬件資源,p0156 代表可以再 0/1/5/6 幾個硬件單元里任意一個執行,然后對著紙面代碼進行一個大概評估。
后面有了 Intel IACA 以及 llvm-mca 后,自動化靜態分析可以更加簡單和準確。到這里,我們對仿射紋理映射做了一次性能靜態分析,可以看得出定點數版本確實快,于是我們得到了一個性能更好的仿射紋理映射函數:
那么這樣的靜態分析準不準確呢?我們接著對兩個函數進行動態性能評測:
鏈接:http://quick-bench.com/FqOYuExcXoyHe_r6Bl1oSm0wUPE
在 gcc 9.2 下面,定點數比浮點數快了 30%,比我們之前靜態分析的結論類似(4.58 比 3.37),但差距略稍許偏低沒有純周期計算出來的 35% 性能差距那么高,因為兩邊都平攤了 I/O 操作引入的延遲(這部分 mca 沒法計算進去)。
那么我們繼續切換編譯器,換成 clang 9.0 :
鏈接:http://quick-bench.com/RoUdH66MayHq6exmQ99mhODUN2w
可以看到性能提升了 2.2 倍,看代碼生成,因為在 clang 下進行了矢量展開,定點數和浮點數都進行了循環展開,每輪循環一次性計算兩個點,導致了更大的性能提升。
C 版本的定點數還可以繼續用 SIMD 一次性算四個點的定點數坐標,性能應該還能提升一級。
定點數除了能在特定地方讓你的代碼性能提升數倍外,在很多對結果嚴格要求一致的地方,也會比浮點數更好,比如幀間同步的游戲,需要在 arm/x86 下面不同客戶端保證同樣的計算結果,這樣浮點數就掛了,不同手機運算結果不同,只能用定點數來處理。
(PS:對于一些復雜運算比如 sin/cos 之類的三角函數,定點數一般用查表+插值進行)
最后,定點數是一個值得收藏到你編程百寶箱里的好工具,必要的時候能夠幫到你。
--
總結
以上是生活随笔為你收集整理的verilog 浮点转定点_定点数优化:性能成倍提升的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python递归汉诺塔详解_汉诺塔在py
- 下一篇: 人耳识别代码_语音识别之——音频特征fb