考慮到這個功能,
float mulHalf(float x) {
return x * 0.5f;
}
以下函式產生與正常輸入/輸出相同的結果。
float mulHalf_opt(float x) {
__m128i e = _mm_set1_epi32(-1 << 23);
__asm__ ("paddd\t%0, %1" : " x"(x) : "xm"(e));
return x;
}
這是帶有 的程式集輸出-O3 -ffast-math。
mulHalf:
mulss xmm0, DWORD PTR .LC0[rip]
ret
mulHalf_opt:
paddd xmm0, XMMWORD PTR .LC1[rip]
ret
-ffast-math啟用-ffinite-math-only“假設引數和結果不是 NaN 或 -Infs” [1]。
因此,mulHalf如果在.paddd-ffast-math-ffast-math
我從Intel Intrinsics Guide獲得了下表。
(MULSS)
Architecture Latency Throughput (CPI)
Skylake 4 0.5
Broadwell 3 0.5
Haswell 5 0.5
Ivy Bridge 5 1
(PADDD)
Architecture Latency Throughput (CPI)
Skylake 1 0.33
Broadwell 1 0.5
Haswell 1 0.5
Ivy Bridge 1 0.5
顯然,paddd是一個更快的指令。然后我想可能是因為整數和浮點單元之間的旁路延遲。
這個答案顯示了來自 Agner Fog 的表格。
Processor Bypass delay, clock cycles
Intel Core 2 and earlier 1
Intel Nehalem 2
Intel Sandy Bridge and later 0-1
Intel Atom 0
AMD 2
VIA Nano 2-3
看到這一點,paddd似乎仍然是贏家,尤其是在 Sandy Bridge 之后的 CPU 上,但是-march為最近的 CPU 指定只是更改mulss為vmulss,它具有相似的延遲/吞吐量。
為什么 GCC 和 Clang 不使用浮點數優化乘法 2^n 到paddd偶數-ffast-math?
uj5u.com熱心網友回復:
對于 的輸入0.0f,這將失敗,這-ffast-math并不排除。(盡管從技術上講,這是一個次正規的特殊情況,恰好也有一個零尾數。)。
整數減法將包裝到一個全1的指數欄位,并翻轉符號位,所以你會得到0.0f * 0.5f,-Inf這根本是不可接受的。
@chtz 指出 0.0f可以通過 using 修復案例psubusw,但對于-0.0f->仍然失敗 Inf。-ffast-math所以不幸的是,即使允許零的“錯誤”符號也不能使用。但是,即使使用快速數學,對于無窮大和 NaN 也是完全錯誤的。
除此之外,是的,我認為這會起作用,并且即使在其他 FP 指令之間使用,也可以在 Nehalem 以外的 CPU 上的旁路延遲與 ALU 延遲方面為自己付出代價。
0.0 行為是一個引人注目的行為。除此之外,與其他輸入的 FP 乘法相比,下溢行為要少得多,例如,即使設定了 FTZ(輸出重繪 為零),也會產生次常態。使用 DAZ 集(非規范化為零)讀取它的代碼仍然可以正確處理它,但是對于具有最小規范化指數(編碼為1)和非零尾數的數字,FP 位模式也可能是錯誤的。例如0x00000001,將歸一化數字乘以0.5f.
即使不是為了0.0f炫耀,這種怪異也可能比 GCC 愿意強加給人們的要多。因此,即使對于 GCC 可以證明非零的情況,我也不指望它,除非它也可以證明與 FLT_MIN 相差甚遠。這可能很少見,不值得尋找。
當您知道它是安全的時,您當然可以手動執行它,盡管使用 SIMD 內在函式更方便。 我希望標量型別雙關語的 asm 相當糟糕,可能是movdinteger的 2 倍,而不是在您只需要低標量 FP 元素時sub將其保留在 XMM 中。paddd
Godbolt 進行了幾次嘗試,包括paddd像我們希望的那樣編譯成一個記憶體源的直接內在函式。Clang 的 shuffle 優化器看到上面的元素是“死的”(_mm_cvtss_f32只讀取底部的),并且能夠將它們視為“不關心”。
// clang compiles this fully efficiently
// others waste an instruction or more on _mm_set_ss to zero the upper XMM elements
float mulHalf_opt_intrinsics(float x) {
__m128i e = _mm_set1_epi32(-1u << 23);
__m128 vx = _mm_set_ss(x);
vx = _mm_castsi128_ps( _mm_add_epi32(_mm_castps_si128(vx), e) );
return _mm_cvtss_f32(vx);
}
和一個普通的標量版本。我還沒有測驗它是否可以自動矢量化,但可以想象它可以這樣做。沒有它,GCC 和 clang 會同時執行movd// add(movd 或sub)來將值反彈到 GP 整數暫存器。
float mulHalf_opt_memcpy_scalar(float x) {
uint32_t xi;
memcpy(&xi, &x, sizeof(x));
xi = -1u << 23;
memcpy(&x, &xi, sizeof(x));
return x;
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/481973.html
上一篇:地址空間,可尋址性,字可尋址,尋址模式之間有什么區別?
下一篇:通過匯編指令了解暫存器的地址分配
