verilog 浮点转定点_定点数优化:性能成倍提升
定點(diǎn)數(shù)這玩意兒并不是什么新東西,早年 CPU 浮點(diǎn)性能不夠,定點(diǎn)數(shù)技巧大量活躍于各類圖形圖像處理的熱點(diǎn)路徑中。今天 CPU 浮點(diǎn)上來了,但很多情況下整數(shù)仍然快于浮點(diǎn),因此比如:libcario (gnome/quartz 后端)及 pixman 之類的很多庫里你仍然找得到定點(diǎn)數(shù)的身影。那么今天我們就來看看使用定點(diǎn)數(shù)到底能快多少。
簡單用一下的話,下面這幾行宏就夠了:
#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 函數(shù),封裝狂可以封裝成一系列 operator xx,如果需要更高的精度,可以將上面用 int32_t 表示的 16.16 定點(diǎn)數(shù)改為用 int64_t 表示的 32.32 定點(diǎn)數(shù)。
那么我們找個(gè)浮點(diǎn)數(shù)的例子優(yōu)化一下吧,比如 libyuv 中的 ARGBAffineRow_C 函數(shù):
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];} }這個(gè)函數(shù)是干什么用的呢?給圖像做 仿射變換(affine transformation) 用的,比如 2D 圖像庫或者 ActionScript 中可以給 Bitmap 設(shè)置一個(gè) 3x3 的矩陣,然后讓 Bitmap 按照該矩陣進(jìn)行變換繪制:
基本上二維圖像所有:縮放,旋轉(zhuǎn),扭曲都是通過仿射變換完成,這個(gè)函數(shù)就是從圖像的起點(diǎn)(u, v)開始按照步長(du, dv)進(jìn)行采樣,放入臨時(shí)緩存中,方便下一步一次性整行寫入 frame buffer。
這個(gè)采樣函數(shù)有幾個(gè)特點(diǎn):
- 運(yùn)算簡單:沒有復(fù)雜的運(yùn)算,計(jì)算無越界,不需要求什么 log/exp 之類的復(fù)雜函數(shù)。
- 范圍可控:大部分圖像長寬尺寸都在 32768 范圍內(nèi),用 16.16 的定點(diǎn)數(shù)即可。
- 轉(zhuǎn)換頻繁:每個(gè)點(diǎn)的坐標(biāo)都需要從浮點(diǎn)轉(zhuǎn)換成整數(shù),這個(gè)操作很費(fèi)事。
適合用定點(diǎn)數(shù)簡單重寫一下:
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); // 浮點(diǎn)數(shù)轉(zhuǎn)定點(diǎn)數(shù)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); // 定點(diǎn)數(shù)坐標(biāo)轉(zhuǎn)整數(shù)坐標(biāo)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; // 定點(diǎn)數(shù)加法v += dv;} }局部用一下定點(diǎn)數(shù)都不需要定義前面那一堆宏,按相關(guān)原理直接寫就是了。
我們用 llvm-mca 分析一下,浮點(diǎn)數(shù)版本 gcc 9.0 的循環(huán)主體部分用 -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))
可以看到,雖然編譯器自動(dòng)生成了 sse 代碼,但性能消耗的大戶,cvttss2si(浮點(diǎn)數(shù)轉(zhuǎn)整數(shù)指令),雖然只有一條命令,但會(huì)生成兩個(gè)微指令(uOP),延遲 6 個(gè)周期,rthroughput 很高 1.0 代表每周期只能同時(shí)運(yùn)行一條該指令,其次是加法指令 addss, 延遲是 4 個(gè)周期,吞吐量 0.5 代表每周期可以并行執(zhí)行 2 條,該代碼塊模擬運(yùn)行 100 次,總消耗 458 個(gè)周期。
再看定點(diǎn)數(shù)版本:
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
指令雖然多了兩條,但是整數(shù)指令 latency 比浮點(diǎn)要低,并且吞吐量(并行性)比浮點(diǎn)更好,大部分指令都是 0.25,代表每周期可以并行執(zhí)行四條,模擬運(yùn)行 100 次,總消耗 337 個(gè)周期。
這里你可能要問,整數(shù)版本,第二列 latency 加起來有 21 個(gè)周期啊,為什么平均運(yùn)行一次才 3.37 個(gè)周期呢?這就是多級(jí)流水線中很多指令可以并行運(yùn)行,只要沒有運(yùn)算結(jié)果依賴,以及沒有執(zhí)行單元的資源沖突,很多運(yùn)算都是可以并行的,所以優(yōu)化里,運(yùn)算解依賴很有用。
我們給 llmv-mca 加一個(gè) -timeline 參數(shù),能得到流水線分析報(bào)表,這里截取兩次循環(huán):
[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每條指令有下面幾個(gè)生存周期:
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從分發(fā)(D),執(zhí)行(e),完成執(zhí)行(E)到退休(R),基本都是同時(shí)進(jìn)行的。后面兩條指令雖然也是和前面四條一起分發(fā)(D),但由于依賴頭兩條指令的運(yùn)算結(jié)果,所以產(chǎn)生了一個(gè)周期的等待(=):
[0,4] D=eER. . . . . . . . . sar eax, 16 [0,5] D=eER. . . . . . . . . sar edx, 16等到頭兩個(gè)指令執(zhí)行成功(e結(jié)束),他們才能開始執(zhí)行,第二次迭代 [1,0] 類似,多次迭代雖然用到了同樣的寄存器,但是在 CPU 里 eax 只是個(gè)名字,CPU 對無關(guān)運(yùn)算的寄存器進(jìn)行重命名后,其實(shí)背后對應(yīng)到了不同的寄存器地址,第二次迭代又有很多地方可以和第一次迭代并行執(zhí)行,所以我們會(huì)發(fā)現(xiàn)兩次迭代的最后一條指令 [0,14] 和 [1,14] 處理的退休時(shí)間 R 都差不多,兩次循環(huán)幾乎是并行執(zhí)行的,如此多次循環(huán)平攤下來每次只要 3.37 個(gè)周期。
可以發(fā)現(xiàn)比浮點(diǎn)數(shù)版本的 4.58 個(gè)周期快了 35% 左右,注意一點(diǎn),實(shí)際 I/O 操作會(huì)占用更多時(shí)間,所以在 mca 的分析里都標(biāo)注了 MayLoad / MayStore,所以算上 I/O,兩邊的周期數(shù)都會(huì)略有增加,但是優(yōu)勢還在那里。
使用 mca 進(jìn)行分析的時(shí)候,你把編譯結(jié)果貼過去時(shí),只能貼循環(huán)的主體部分,因?yàn)楸旧砭鸵M(jìn)行多次運(yùn)行的流水線模擬,所以你貼了循環(huán)外的初始化部分就會(huì)干擾分析結(jié)果。
可能有人會(huì)說,媽呀,靜態(tài)性能分析還要計(jì)算流水線么?其實(shí)大部分時(shí)候不需要,沒有 llvm-mca 的時(shí)候我們做靜態(tài)性能分析一般就是查指令手冊:
大部分時(shí)候,看一下 uops 數(shù)量(越少越好),看一下周期 latency(越少越好),以及 throughput (決定并行效率,越低越好),心中對各類指令的占用消耗有一個(gè)基本概念,再細(xì)致一點(diǎn)的話還可以看看占用哪些硬件資源,p0156 代表可以再 0/1/5/6 幾個(gè)硬件單元里任意一個(gè)執(zhí)行,然后對著紙面代碼進(jìn)行一個(gè)大概評(píng)估。
后面有了 Intel IACA 以及 llvm-mca 后,自動(dòng)化靜態(tài)分析可以更加簡單和準(zhǔn)確。到這里,我們對仿射紋理映射做了一次性能靜態(tài)分析,可以看得出定點(diǎn)數(shù)版本確實(shí)快,于是我們得到了一個(gè)性能更好的仿射紋理映射函數(shù):
那么這樣的靜態(tài)分析準(zhǔn)不準(zhǔn)確呢?我們接著對兩個(gè)函數(shù)進(jìn)行動(dòng)態(tài)性能評(píng)測:
鏈接:http://quick-bench.com/FqOYuExcXoyHe_r6Bl1oSm0wUPE
在 gcc 9.2 下面,定點(diǎn)數(shù)比浮點(diǎn)數(shù)快了 30%,比我們之前靜態(tài)分析的結(jié)論類似(4.58 比 3.37),但差距略稍許偏低沒有純周期計(jì)算出來的 35% 性能差距那么高,因?yàn)閮蛇叾计綌偭?I/O 操作引入的延遲(這部分 mca 沒法計(jì)算進(jìn)去)。
那么我們繼續(xù)切換編譯器,換成 clang 9.0 :
鏈接:http://quick-bench.com/RoUdH66MayHq6exmQ99mhODUN2w
可以看到性能提升了 2.2 倍,看代碼生成,因?yàn)樵?clang 下進(jìn)行了矢量展開,定點(diǎn)數(shù)和浮點(diǎn)數(shù)都進(jìn)行了循環(huán)展開,每輪循環(huán)一次性計(jì)算兩個(gè)點(diǎn),導(dǎo)致了更大的性能提升。
C 版本的定點(diǎn)數(shù)還可以繼續(xù)用 SIMD 一次性算四個(gè)點(diǎn)的定點(diǎn)數(shù)坐標(biāo),性能應(yīng)該還能提升一級(jí)。
定點(diǎn)數(shù)除了能在特定地方讓你的代碼性能提升數(shù)倍外,在很多對結(jié)果嚴(yán)格要求一致的地方,也會(huì)比浮點(diǎn)數(shù)更好,比如幀間同步的游戲,需要在 arm/x86 下面不同客戶端保證同樣的計(jì)算結(jié)果,這樣浮點(diǎn)數(shù)就掛了,不同手機(jī)運(yùn)算結(jié)果不同,只能用定點(diǎn)數(shù)來處理。
(PS:對于一些復(fù)雜運(yùn)算比如 sin/cos 之類的三角函數(shù),定點(diǎn)數(shù)一般用查表+插值進(jìn)行)
最后,定點(diǎn)數(shù)是一個(gè)值得收藏到你編程百寶箱里的好工具,必要的時(shí)候能夠幫到你。
--
總結(jié)
以上是生活随笔為你收集整理的verilog 浮点转定点_定点数优化:性能成倍提升的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python递归汉诺塔详解_汉诺塔在py
- 下一篇: 人耳识别代码_语音识别之——音频特征fb