このコードはなぜですか。
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でコンパイルするとき。 (私は他のコンパイラでテストしていません。)
非正規化浮動小数点 !の世界へようこそ、彼らはパフォーマンスを破壊することができます!!!
非正規(または非正規)数は、浮動小数点表現からゼロに非常に近い追加の値を取得するための一種のハックです。非正規化浮動小数点の操作は、正規化浮動小数点よりも数十倍から数百倍遅い 。これは、多くのプロセッサがそれらを直接処理できず、マイクロコードを使用してそれらをトラップおよび解決する必要があるためです。
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
2回目の実行では、数値がゼロに非常に近いことに注意してください。
非正規化された数値は一般的にまれであるため、ほとんどのプロセッサはそれらを効率的に処理しようとしません。
非正規化をゼロにフラッシュする場合、コードの先頭にこれを追加することで、これが非正規化された数値と関係があることを実証します:
_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
を使用して生成されたアセンブリにdiffを適用すると、この違いだけが発生します。
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
に変換するため、時間がかかります。 gccに-O3
を渡しても役に立ちません。 (gccバージョン4.2.1)
(double
の代わりにfloat
を使用しても問題ありませんが、cvtsi2ssq
をcvtsi2sdq
に変更します。)
更新
いくつかの追加テストは、それが必ずしもcvtsi2ssq
命令ではないことを示しています。いったん削除されると(int ai=0;float a=ai;
を使用し、0
の代わりにa
を使用)、速度の違いは残ります。だから@ Mysticialは正しい、非正規化フロートは違いを生む。これは、0
と0.1f
の間の値をテストすることで確認できます。上記のコードの転換点は、ループが突然10倍の時間がかかるときの0.00000000000000000000000000000001
です。
更新<< 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 question Objective-Cでは非正規化浮動小数点?にあります。
これは非正規化浮動小数点の使用によるものです。それとパフォーマンスのペナルティの両方を取り除く方法は?非正規数を殺す方法についてインターネットを精査したが、これを行うための「最善の」方法はまだないようです。私は、異なる環境で最もうまくいく可能性があるこれら3つの方法を見つけました。
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);
Intelコンパイラには、最近のIntel CPUでデフォルトで非正規化を無効にするオプションがあります。 詳細はこちら
コンパイラスイッチ-ffast-math
、-msse
または-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;
です。この行がループの繰り返しごとに実行されるとすぐに、floatの余分な精度が失われ、その精度を表すのに必要な非正規化は不要になります。その後、y[i]
の浮動小数点演算は、非正規化されていないため高速のままです。
0.1f
を追加すると余分な精度が失われるのはなぜですか?なぜなら、浮動小数点数は有効桁数が非常に多いからです。最下位ビットを0.00001 = 1e-5
に格納するスペースがないため、少なくともこの例のfloat形式では、有効数字3桁、次に0.00001 + 0.1 = 0.1
、および0.10001
に十分なストレージがあるとします。
要するに、y[i]=y[i]+0.1f; y[i]=y[i]-0.1f;
はあなたがそれを思うかもしれないノーオペレーションではありません。
Mysticalはこれも言った :フロートの中身は重要だ、アセンブリコードだけではない。