[英]Why does changing 0.1f to 0 slow down performance by 10x?
為什么這段代碼,
const float x[16] = { 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8,
1.9, 2.0, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6};
const float z[16] = {1.123, 1.234, 1.345, 156.467, 1.578, 1.689, 1.790, 1.812,
1.923, 2.034, 2.145, 2.256, 2.367, 2.478, 2.589, 2.690};
float y[16];
for (int i = 0; i < 16; i++)
{
y[i] = x[i];
}
for (int j = 0; j < 9000000; j++)
{
for (int i = 0; i < 16; i++)
{
y[i] *= x[i];
y[i] /= z[i];
y[i] = y[i] + 0.1f; // <--
y[i] = y[i] - 0.1f; // <--
}
}
運行速度比以下位快 10 倍以上(除另有說明外相同)?
const float x[16] = { 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8,
1.9, 2.0, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6};
const float z[16] = {1.123, 1.234, 1.345, 156.467, 1.578, 1.689, 1.790, 1.812,
1.923, 2.034, 2.145, 2.256, 2.367, 2.478, 2.589, 2.690};
float y[16];
for (int i = 0; i < 16; i++)
{
y[i] = x[i];
}
for (int j = 0; j < 9000000; j++)
{
for (int i = 0; i < 16; i++)
{
y[i] *= x[i];
y[i] /= z[i];
y[i] = y[i] + 0; // <--
y[i] = y[i] - 0; // <--
}
}
使用 Visual Studio 2010 SP1 編譯時。 啟用sse2
的優化級別為-02
。 我沒有用其他編譯器測試過。
歡迎來到非規范化浮點的世界! 他們可以對性能造成嚴重破壞!!!
非正規(或次正規)數字是一種從浮點表示中獲得一些非常接近零的額外值的技巧。 非規范化浮點運算可能比規范化浮點運算慢數十到數百倍。 這是因為許多處理器無法直接處理它們,必須使用微碼捕獲和解析它們。
如果在 10,000 次迭代后打印出這些數字,您將看到它們已經收斂到不同的值,具體取決於使用的是0
還是0.1
。
這是在 x64 上編譯的測試代碼:
int main() {
double start = omp_get_wtime();
const float x[16]={1.1,1.2,1.3,1.4,1.5,1.6,1.7,1.8,1.9,2.0,2.1,2.2,2.3,2.4,2.5,2.6};
const float z[16]={1.123,1.234,1.345,156.467,1.578,1.689,1.790,1.812,1.923,2.034,2.145,2.256,2.367,2.478,2.589,2.690};
float y[16];
for(int i=0;i<16;i++)
{
y[i]=x[i];
}
for(int j=0;j<9000000;j++)
{
for(int i=0;i<16;i++)
{
y[i]*=x[i];
y[i]/=z[i];
#ifdef FLOATING
y[i]=y[i]+0.1f;
y[i]=y[i]-0.1f;
#else
y[i]=y[i]+0;
y[i]=y[i]-0;
#endif
if (j > 10000)
cout << y[i] << " ";
}
if (j > 10000)
cout << endl;
}
double end = omp_get_wtime();
cout << end - start << endl;
system("pause");
return 0;
}
輸出:
#define FLOATING
1.78814e-007 1.3411e-007 1.04308e-007 0 7.45058e-008 6.70552e-008 6.70552e-008 5.58794e-007 3.05474e-007 2.16067e-007 1.71363e-007 1.49012e-007 1.2666e-007 1.11759e-007 1.04308e-007 1.04308e-007
1.78814e-007 1.3411e-007 1.04308e-007 0 7.45058e-008 6.70552e-008 6.70552e-008 5.58794e-007 3.05474e-007 2.16067e-007 1.71363e-007 1.49012e-007 1.2666e-007 1.11759e-007 1.04308e-007 1.04308e-007
//#define FLOATING
6.30584e-044 3.92364e-044 3.08286e-044 0 1.82169e-044 1.54143e-044 2.10195e-044 2.46842e-029 7.56701e-044 4.06377e-044 3.92364e-044 3.22299e-044 3.08286e-044 2.66247e-044 2.66247e-044 2.24208e-044
6.30584e-044 3.92364e-044 3.08286e-044 0 1.82169e-044 1.54143e-044 2.10195e-044 2.45208e-029 7.56701e-044 4.06377e-044 3.92364e-044 3.22299e-044 3.08286e-044 2.66247e-044 2.66247e-044 2.24208e-044
請注意,在第二次運行中,數字非常接近於零。
非規范化數字通常很少見,因此大多數處理器不會嘗試有效地處理它們。
為了證明這與非規范化數字有關,如果我們通過將其添加到代碼的開頭將非規范化數刷新為零:
_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);
然后帶有0
的版本不再慢 10 倍,實際上變得更快。 (這要求在啟用 SSE 的情況下編譯代碼。)
這意味着我們不是使用這些奇怪的低精度幾乎為零的值,而是將其舍入為零。
時序:Core i7 920 @ 3.5 GHz:
// Don't flush denormals to zero.
0.1f: 0.564067
0 : 26.7669
// Flush denormals to zero.
0.1f: 0.587117
0 : 0.341406
最后,這真的與它是整數還是浮點數無關。 0
或0.1f
被轉換/存儲到兩個循環之外的寄存器中。 所以這對性能沒有影響。
使用gcc
並對生成的程序集應用差異只會產生以下差異:
73c68,69
< movss LCPI1_0(%rip), %xmm1
---
> movabsq $0, %rcx
> cvtsi2ssq %rcx, %xmm1
81d76
< subss %xmm1, %xmm0
cvtsi2ssq
確實慢了 10 倍。
顯然, float
版本使用從內存加載的XMM寄存器,而int
版本使用cvtsi2ssq
指令將真正的int
值 0 轉換為float
,花費了大量時間。 將-O3
傳遞給 gcc 沒有幫助。 (gcc 版本 4.2.1。)
(使用double
而不是float
無關緊要,只是它將cvtsi2ssq
更改為cvtsi2sdq
。)
更新
一些額外的測試表明它不一定是cvtsi2ssq
指令。 一旦消除(使用int ai=0;float a=ai;
並使用a
而不是0
),速度差異仍然存在。 所以@Mysticial 是對的,非規范化的浮點數有所不同。 這可以通過測試0
到0.1f
之間的值來看出。 上述代碼中的轉折點大約在0.00000000000000000000000000000001
,此時循環的時間突然增加了 10 倍。
更新<<1
這個有趣現象的一個小可視化:
當非規范化開始時,您可以清楚地看到指數(最后 9 位)變為最低值。此時,簡單的加法會慢 20 倍。
0.000000000000000000000000000000000100000004670110: 10111100001101110010000011100000 45 ms
0.000000000000000000000000000000000050000002335055: 10111100001101110010000101100000 43 ms
0.000000000000000000000000000000000025000001167528: 10111100001101110010000001100000 43 ms
0.000000000000000000000000000000000012500000583764: 10111100001101110010000110100000 42 ms
0.000000000000000000000000000000000006250000291882: 10111100001101110010000010100000 48 ms
0.000000000000000000000000000000000003125000145941: 10111100001101110010000100100000 43 ms
0.000000000000000000000000000000000001562500072970: 10111100001101110010000000100000 42 ms
0.000000000000000000000000000000000000781250036485: 10111100001101110010000111000000 42 ms
0.000000000000000000000000000000000000390625018243: 10111100001101110010000011000000 42 ms
0.000000000000000000000000000000000000195312509121: 10111100001101110010000101000000 43 ms
0.000000000000000000000000000000000000097656254561: 10111100001101110010000001000000 42 ms
0.000000000000000000000000000000000000048828127280: 10111100001101110010000110000000 44 ms
0.000000000000000000000000000000000000024414063640: 10111100001101110010000010000000 42 ms
0.000000000000000000000000000000000000012207031820: 10111100001101110010000100000000 42 ms
0.000000000000000000000000000000000000006103515209: 01111000011011100100001000000000 789 ms
0.000000000000000000000000000000000000003051757605: 11110000110111001000010000000000 788 ms
0.000000000000000000000000000000000000001525879503: 00010001101110010000100000000000 788 ms
0.000000000000000000000000000000000000000762939751: 00100011011100100001000000000000 795 ms
0.000000000000000000000000000000000000000381469876: 01000110111001000010000000000000 896 ms
0.000000000000000000000000000000000000000190734938: 10001101110010000100000000000000 813 ms
0.000000000000000000000000000000000000000095366768: 00011011100100001000000000000000 798 ms
0.000000000000000000000000000000000000000047683384: 00110111001000010000000000000000 791 ms
0.000000000000000000000000000000000000000023841692: 01101110010000100000000000000000 802 ms
0.000000000000000000000000000000000000000011920846: 11011100100001000000000000000000 809 ms
0.000000000000000000000000000000000000000005961124: 01111001000010000000000000000000 795 ms
0.000000000000000000000000000000000000000002980562: 11110010000100000000000000000000 835 ms
0.000000000000000000000000000000000000000001490982: 00010100001000000000000000000000 864 ms
0.000000000000000000000000000000000000000000745491: 00101000010000000000000000000000 915 ms
0.000000000000000000000000000000000000000000372745: 01010000100000000000000000000000 918 ms
0.000000000000000000000000000000000000000000186373: 10100001000000000000000000000000 881 ms
0.000000000000000000000000000000000000000000092486: 01000010000000000000000000000000 857 ms
0.000000000000000000000000000000000000000000046243: 10000100000000000000000000000000 861 ms
0.000000000000000000000000000000000000000000022421: 00001000000000000000000000000000 855 ms
0.000000000000000000000000000000000000000000011210: 00010000000000000000000000000000 887 ms
0.000000000000000000000000000000000000000000005605: 00100000000000000000000000000000 799 ms
0.000000000000000000000000000000000000000000002803: 01000000000000000000000000000000 828 ms
0.000000000000000000000000000000000000000000001401: 10000000000000000000000000000000 815 ms
0.000000000000000000000000000000000000000000000000: 00000000000000000000000000000000 42 ms
0.000000000000000000000000000000000000000000000000: 00000000000000000000000000000000 42 ms
0.000000000000000000000000000000000000000000000000: 00000000000000000000000000000000 44 ms
關於 ARM 的等效討論可以在 Stack Overflow 問題Denormalized floating point in Objective-C 中找到? .
這是由於非規范化的浮點使用。 如何擺脫它和性能損失? 在互聯網上搜索了殺死非正規數的方法后,似乎還沒有“最佳”方法可以做到這一點。 我發現這三種方法可能在不同的環境中效果最好:
在某些 GCC 環境中可能不起作用:
// Requires #include <fenv.h> fesetenv(FE_DFL_DISABLE_SSE_DENORMS_ENV);
在某些 Visual Studio 環境中可能不起作用: 1
// Requires #include <xmmintrin.h> _mm_setcsr( _mm_getcsr() | (1<<15) | (1<<6) ); // Does both FTZ and DAZ bits. You can also use just hex value 0x8040 to do both. // You might also want to use the underflow mask (1<<11)
似乎在 GCC 和 Visual Studio 中都可以使用:
// Requires #include <xmmintrin.h> // Requires #include <pmmintrin.h> _MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON); _MM_SET_DENORMALS_ZERO_MODE(_MM_DENORMALS_ZERO_ON);
默認情況下,英特爾編譯器具有在現代英特爾 CPU 上禁用非規范化的選項。 更多細節在這里
編譯器開關。 -ffast-math
、 -msse
或-mfpmath=sse
將禁用非正規-mfpmath=sse
並使其他一些事情更快,但不幸的是,也會執行許多其他可能破壞您的代碼的近似值。 仔細測試! Visual Studio 編譯器的 fast-math 的等效項是/fp:fast
但我無法確認這是否也禁用了非規范化。 1
在 gcc 中,您可以通過以下方式啟用 FTZ 和 DAZ:
#include <xmmintrin.h>
#define FTZ 1
#define DAZ 1
void enableFtzDaz()
{
int mxcsr = _mm_getcsr ();
if (FTZ) {
mxcsr |= (1<<15) | (1<<11);
}
if (DAZ) {
mxcsr |= (1<<6);
}
_mm_setcsr (mxcsr);
}
也使用 gcc 開關:-msse -mfpmath=sse
(相應的學分來自 Carl Hetherington [1])
Dan Neely 的評論應該擴展為答案:
不是非規范化或導致減速的零常數0.0f
,而是每次循環迭代接近零的值。 隨着它們越來越接近於零,它們需要更高的精度來表示,並且它們變得非規范化。 這些是y[i]
值。 (它們接近於零,因為x[i]/z[i]
對於所有i
都小於 1.0。)
代碼的慢速版本和快速版本之間的關鍵區別在於語句y[i] = y[i] + 0.1f;
. 只要在循環的每次迭代中執行此行,浮點數中的額外精度就會丟失,並且不再需要表示該精度所需的非規范化。 之后, y[i]
上的浮點運算仍然很快,因為它們沒有被非規范化。
為什么添加0.1f
時會丟失額外的精度? 因為浮點數只有這么多有效數字。 假設您有足夠的存儲空間來存儲三位有效數字,然后0.00001 = 1e-5
和0.00001 + 0.1 = 0.1
,至少對於此示例浮點格式,因為它沒有空間將最低有效位存儲在0.10001
。
簡而言之, y[i]=y[i]+0.1f; y[i]=y[i]-0.1f;
y[i]=y[i]+0.1f; y[i]=y[i]-0.1f;
不是您可能認為的無操作。
Mystical 也這么說:浮點數的內容很重要,而不僅僅是匯編代碼。
編輯:為了更好地說明這一點,即使機器操作碼相同,也不是每個浮點運算都需要相同的時間來運行。 對於某些操作數/輸入,相同的指令將需要更多時間來運行。 對於非正規數尤其如此。
很長一段時間內,CPU 對於非正規數只會稍微慢一點。 我的 Zen2 CPU 需要五個時鍾周期來進行非正規輸入和非正規輸出的計算,以及四個時鍾周期和標准化數字。
這是一個用 Visual C++ 編寫的小型基准測試,用於顯示非正規數對性能的輕微影響:
#include <iostream>
#include <cstdint>
#include <chrono>
using namespace std;
using namespace chrono;
uint64_t denScale( uint64_t rounds, bool den );
int main()
{
auto bench = []( bool den ) -> double
{
constexpr uint64_t ROUNDS = 25'000'000;
auto start = high_resolution_clock::now();
int64_t nScale = denScale( ROUNDS, den );
return (double)duration_cast<nanoseconds>( high_resolution_clock::now() - start ).count() / nScale;
};
double
tDen = bench( true ),
tNorm = bench( false ),
rel = tDen / tNorm - 1;
cout << tDen << endl;
cout << tNorm << endl;
cout << trunc( 100 * 10 * rel + 0.5 ) / 10 << "%" << endl;
}
這是 MASM 組裝部件。
PUBLIC ?denScale@@YA_K_K_N@Z
CONST SEGMENT
DEN DQ 00008000000000000h
ONE DQ 03FF0000000000000h
P5 DQ 03fe0000000000000h
CONST ENDS
_TEXT SEGMENT
?denScale@@YA_K_K_N@Z PROC
xor rax, rax
test rcx, rcx
jz byeBye
mov r8, ONE
mov r9, DEN
test dl, dl
cmovnz r8, r9
movq xmm1, P5
mov rax, rcx
loopThis:
movq xmm0, r8
REPT 52
mulsd xmm0, xmm1
ENDM
sub rcx, 1
jae loopThis
mov rdx, 52
mul rdx
byeBye:
ret
?denScale@@YA_K_K_N@Z ENDP
_TEXT ENDS
END
很高興在評論中看到一些結果。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.