私たちのプロジェクトでは、浮動小数点から整数への変換を大量に行っています。基本的に、このようなもの
for(int i = 0; i < HUGE_NUMBER; i++)
int_array[i] = float_array[i];
変換を実行するデフォルトの C 関数は、かなり時間がかかります。
プロセスを少しスピードアップできる回避策 (おそらく手動で調整された関数) はありますか? 精度はあまり気にしません。
私たちのプロジェクトでは、浮動小数点から整数への変換を大量に行っています。基本的に、このようなもの
for(int i = 0; i < HUGE_NUMBER; i++)
int_array[i] = float_array[i];
変換を実行するデフォルトの C 関数は、かなり時間がかかります。
プロセスを少しスピードアップできる回避策 (おそらく手動で調整された関数) はありますか? 精度はあまり気にしません。
ここでの他の回答のほとんどは、ループのオーバーヘッドを排除しようとするだけです。
deft_code の回答だけが、実際の問題である可能性が高いものの核心に到達します。つまり、浮動小数点を整数に変換すると、x86 プロセッサでは驚くほどコストがかかるということです。deft_code の解決策は正しいですが、彼は引用も説明もしていません。
以下はトリックのソースであり、いくつかの説明と、切り上げ、切り下げ、またはゼロに向かって切り上げるかどうかに固有のバージョンも含まれています。 FPU を知る
リンクを提供して申し訳ありませんが、実際にここに書かれていることは、その優れた記事を再現することを除けば、物事を明確にすることにはなりません.
inline int float2int( double d )
{
union Cast
{
double d;
long l;
};
volatile Cast c;
c.d = d + 6755399441055744.0;
return c.l;
}
// this is the same thing but it's
// not always optimizer safe
inline int float2int( double d )
{
d += 6755399441055744.0;
return reinterpret_cast<int&>(d);
}
for(int i = 0; i < HUGE_NUMBER; i++)
int_array[i] = float2int(float_array[i]);
double パラメータは間違いではありません。フロートを直接使用してこのトリックを実行する方法はありますが、すべてのコーナー ケースをカバーしようとすると見苦しくなります。現在の形式では、切り捨てが必要な場合、この関数は float を最も近い整数に丸めます。代わりに 6755399441055743.5 (0.5 少ない) を使用します。
floatからintへの変換を行うさまざまな方法でいくつかのテストを実行しました。簡単な答えは、顧客がSSE2対応のCPUを持っていると想定し、/ arch:SSE2コンパイラフラグを設定することです。これにより、コンパイラーは、マジックナンバー手法の2倍の速度のSSEスカラー命令を使用できるようになります。
それ以外の場合、グラインドするフロートの長いストリングがある場合は、SSE2パックのopsを使用してください。
SSE3命令セットにはFISTTP命令があり、必要な処理を実行しますが、それを利用してlibcよりも高速な結果を生成できるかどうかについては、私にはわかりません。
時間は、いくつかのスレッドを開始するコストを上回るのに十分な長さですか?
ボックスにマルチコアプロセッサまたは複数のプロセッサがあり、それらを利用できると仮定すると、これは複数のスレッド間で並列化するための簡単な作業になります。
重要なのは、不必要に遅い _ftol() 関数を避けることです。このようなデータの長いリストに対する最善の策は、SSE2 命令 cvtps2dq を使用して、2 つのパックされた float を 2 つのパックされた int64 に変換することです。これを 2 回実行すると (2 つの SSE レジスターで 4 つの int64 を取得)、それらをシャッフルして 4 つの int32 を取得できます (各変換結果の上位 32 ビットが失われます)。これを行うためにアセンブリは必要ありません。MSVC は、コンパイラの組み込み関数を関連する命令(メモリが正しく機能する場合は_mm_cvtpd_epi32())に公開します。
これを行う場合、float 配列と int 配列を 16 バイト アラインして、SSE2 のロード/ストア組み込み関数が最大限の効率で動作できるようにすることが非常に重要です。また、ソフトウェア パイプラインを少し実行し、各ループで一度に16 個のフロートを処理することをお勧めします (ここでの「関数」が実際にはコンパイラ組み込み関数の呼び出しであると仮定します)。
for(int i = 0; i < HUGE_NUMBER; i+=16)
{
//int_array[i] = float_array[i];
__m128 a = sse_load4(float_array+i+0);
__m128 b = sse_load4(float_array+i+4);
__m128 c = sse_load4(float_array+i+8);
__m128 d = sse_load4(float_array+i+12);
a = sse_convert4(a);
b = sse_convert4(b);
c = sse_convert4(c);
d = sse_convert4(d);
sse_write4(int_array+i+0, a);
sse_write4(int_array+i+4, b);
sse_write4(int_array+i+8, c);
sse_write4(int_array+i+12, d);
}
これは、SSE 命令のレイテンシが長いためです。そのため、xmm0 に依存する操作で xmm0 へのロードをすぐに実行すると、ストールが発生します。一度に複数のレジスタを「実行中」にすると、レイテンシが少し隠れます。(理論的には、すべてを知っている魔法のコンパイラは、この問題を回避する方法をエイリアス化できますが、実際にはそうではありません。)
この SSE ジュジュに失敗すると、MSVC に /QIfist オプションを指定できます。これにより、_ftol への呼び出しの代わりに単一のオペコードフィストが発行されます。これは、ANSI C 固有の切り捨て操作であることを確認せずに、たまたま CPU に設定されている丸めモードを単純に使用することを意味します。Microsoft のドキュメントによると、/QIfist の浮動小数点コードは現在高速であるため、非推奨になっていますが、逆アセンブラーを使用すると、これが不当に楽観的であることがわかります。/fp:fast でさえ、単に _ftol_sse2 の呼び出しになります。これは、悪質な _ftol よりも高速ですが、関数呼び出しの後に潜在的な SSE op が続くため、不必要に遅くなります。
ちなみに、あなたが x86 arch を使用していると仮定しています。PPC を使用している場合は、同等の VMX 操作があります。または、上記のマジック ナンバー乗算トリックを使用してから、vsel を使用することもできます (非仮数ビット) とアラインされたストア。
いくつかの魔法のアセンブリコードを使用して、プロセッサのSSEモジュールにすべての整数をロードし、同等のコードを実行して値をintに設定し、それらをfloatとして読み取ることができる場合があります。しかし、これがもっと速くなるかどうかはわかりません。私はSSEの第一人者ではないので、これを行う方法がわかりません。多分誰か他の人がチャイムを鳴らすことができます。
整数変換の高速化については、次のインテルの記事を参照してください。
http://software.intel.com/en-us/articles/latency-of-floating-point-to-integer-conversions/
Microsoft によると、VS 2005 では整数変換が高速化されたため、/QIfist コンパイラ オプションは廃止されました。彼らはそれがどのように高速化されたかについては言及していませんが、分解リストを見ると手がかりが得られるかもしれません.
http://msdn.microsoft.com/en-us/library/z8dh4h17(vs.80).aspx
ほとんどの c コンパイラは、float から int への変換ごとに _ftol などへの呼び出しを生成します。縮小浮動小数点適合スイッチ (fp:fast など) を配置すると役立つ場合があります-このスイッチの他の効果を理解し、受け入れる場合。それ以外は、タイトなアセンブリまたは sse 固有のループに入れます。問題がなく、異なる丸め動作を理解している場合。あなたの例のような大規模なループの場合、浮動小数点制御ワードを一度設定してから、fistp 命令のみで一括丸めを実行してから制御ワードをリセットする関数を作成する必要があります-x86 のみのコードパスで問題がない場合は、少なくとも丸めを変更しません。fld および fistp fpu 命令と fpu 制御ワードを読んでください。
Visual C++ 2008 では、最大化された最適化オプションを使用してリリース ビルドを実行し、逆アセンブリを確認すると、コンパイラは SSE2 呼び出しを自動的に生成します (いくつかの条件を満たす必要がありますが、コードを試してみてください)。
Intelでは、最善の策はインラインSSE2呼び出しです。
優れたトリックのみを丸め、丸めを行うために6755399441055743.5(0.5以下)を使用するだけでは機能しません。
6755399441055744 = 2 ^ 52 + 2 ^ 51仮数の終わりから小数がオーバーフローし、fpuレジスタのビット51-0に必要な整数が残ります。
IEEE754では
6755399441055744.0=
指数仮数
0100001100111000000000000000000000000000000000000000000000000000に署名します
ただし、6755399441055743.5は、0100001100111000000000000000000000000000000000000000000000000000にコンパイルされます。
0.5は端からオーバーフローします(切り上げ)。これが、そもそもこれが機能する理由です。
切り捨てを行うには、doubleに0.5を追加する必要があります。これを行うと、保護桁がこの方法で行われた正しい結果への丸めを処理する必要があります。また、64ビットgcc linuxにも注意してください。ここで、長いとかなり厄介なのは64ビット整数を意味します。
丸めセマンティクスをあまり気にしない場合は、lrint()関数を使用できます。これにより、丸めの自由度が高まり、はるかに高速になります。
技術的には、これは C99 関数ですが、コンパイラはおそらく C++ で公開しています。優れたコンパイラは、それを 1 つの命令にインライン化します (最新の G++ はそうします)。
どのコンパイラを使用していますか? Microsoft の最近の C/C++ コンパイラでは、C/C++ -> コード生成 -> 浮動小数点モデルの下にオプションがあり、高速、正確、厳密というオプションがあります。正確はデフォルトで、FP操作をある程度エミュレートすることで機能すると思います。MS コンパイラを使用している場合、このオプションはどのように設定されていますか? 「高速」に設定すると役立ちますか?いずれにせよ、分解はどのように見えますか?
上でthirtysevenが言ったように、CPUはfloat<->int基本的に1つの命令で変換でき、それよりも速くなることはありません(SIMD操作を除く)。
また、最新の CPU は単一 (32 ビット) と倍精度 (64 ビット) の両方の FP 数値に同じ FP 単位を使用することに注意してください。そのため、多くの浮動小数点数を格納してメモリを節約しようとしている場合を除き、float倍精度を優先する理由は実際にはありません。
あなたの結果には驚いています。どのコンパイラを使用していますか? 最適化を最大にしてコンパイルしていますか? これがボトルネックであることをvalgrindと Kcachegrind を使用して確認しましたか? どのプロセッサを使用していますか? アセンブリ コードはどのように見えますか?
変換自体は、単一の命令にコンパイルする必要があります。優れた最適化コンパイラは、テストと分岐ごとに複数の変換が行われるように、ループをアンロールする必要があります。それが起こらない場合は、手でループを展開できます:
for(int i = 0; i < HUGE_NUMBER-3; i += 4) {
int_array[i] = float_array[i];
int_array[i+1] = float_array[i+1];
int_array[i+2] = float_array[i+2];
int_array[i+3] = float_array[i+3];
}
for(; i < HUGE_NUMBER; i++)
int_array[i] = float_array[i];
コンパイラが本当に哀れな場合は、一般的な部分式でコンパイラを助ける必要があるかもしれません。
int *ip = int_array+i;
float *fp = float_array+i;
ip[0] = fp[0];
ip[1] = fp[1];
ip[2] = fp[2];
ip[3] = fp[3];
詳細を報告してください!
非常に大きな配列 (CPU キャッシュのサイズである数 MB よりも大きい) がある場合は、コードの時間を計り、スループットを確認します。FP ユニットではなく、メモリ バスを飽和させている可能性があります。CPU の理論上の最大帯域幅を調べて、それにどれだけ近いかを確認します。
メモリバスによって制限されている場合、余分なスレッドはそれを悪化させるだけです. より良いハードウェアが必要です (より高速なメモリ、別の CPU、別のマザーボードなど)。
おっしゃる通りです。FPU が主要なボトルネックです (xs_CRoundToInt トリックを使用すると、メモリ バスを飽和状態に近づけることができます)。
Core 2 (Q6600) プロセッサのテスト結果を次に示します。このマシンの理論上のメイン メモリ帯域幅は 3.2 GB/秒です (L1 と L2 の帯域幅はそれよりはるかに高くなっています)。コードは Visual Studio 2008 でコンパイルされました。32 ビットと 64 ビット、および /O2 または /Ox の最適化で同様の結果が得られました。
書き込みのみ... 33554432 個の配列要素を持つ 1866359 ティック (33554432 タッチ)。帯域幅: 1.91793 GB/秒 262144 配列要素 (33554432 タッチ) で 154749 ティック。帯域幅: 23.1313 GB/秒 8192 個の配列要素を持つ 108816 ティック (33554432 タッチ)。帯域幅: 32.8954 GB/秒 キャスティングを使用しています... 33554432 配列要素 (33554432 タッチ) で 5236122 ティック。帯域幅: 0.683625 GB/秒 262144 個の配列要素を持つ 2014309 ティック (33554432 タッチ)。帯域幅: 1.77706 GB/秒 8192 個の配列要素で 1967345 ティック (33554432 タッチ)。帯域幅: 1.81948 GB/秒 xs_CRoundToInt を使用しています... 33554432 配列要素 (33554432 タッチ) で 1490583 ティック。帯域幅: 2.40144 GB/秒 262144 配列要素 (33554432 タッチ) で 1079530 ティック。帯域幅: 3.31584 GB/秒 8192 個の配列要素で 1008407 ティック (33554432 タッチ)。帯域幅: 3.5497 GB/秒
(Windows) ソース コード:
// floatToIntTime.cpp : Defines the entry point for the console application.
//
#include <windows.h>
#include <iostream>
using namespace std;
double const _xs_doublemagic = double(6755399441055744.0);
inline int xs_CRoundToInt(double val, double dmr=_xs_doublemagic) {
val = val + dmr;
return ((int*)&val)[0];
}
static size_t const N = 256*1024*1024/sizeof(double);
int I[N];
double F[N];
static size_t const L1CACHE = 128*1024/sizeof(double);
static size_t const L2CACHE = 4*1024*1024/sizeof(double);
static size_t const Sz[] = {N, L2CACHE/2, L1CACHE/2};
static size_t const NIter[] = {1, N/(L2CACHE/2), N/(L1CACHE/2)};
int main(int argc, char *argv[])
{
__int64 freq;
QueryPerformanceFrequency((LARGE_INTEGER*)&freq);
cout << "WRITING ONLY..." << endl;
for (int t=0; t<3; t++) {
__int64 t0,t1;
QueryPerformanceCounter((LARGE_INTEGER*)&t0);
size_t const niter = NIter[t];
size_t const sz = Sz[t];
for (size_t i=0; i<niter; i++) {
for (size_t n=0; n<sz; n++) {
I[n] = 13;
}
}
QueryPerformanceCounter((LARGE_INTEGER*)&t1);
double bandwidth = 8*niter*sz / (((double)(t1-t0))/freq) / 1024/1024/1024;
cout << " " << (t1-t0) << " ticks with " << sz
<< " array elements (" << niter*sz << " touched). "
<< "Bandwidth: " << bandwidth << " GB/s" << endl;
}
cout << "USING CASTING..." << endl;
for (int t=0; t<3; t++) {
__int64 t0,t1;
QueryPerformanceCounter((LARGE_INTEGER*)&t0);
size_t const niter = NIter[t];
size_t const sz = Sz[t];
for (size_t i=0; i<niter; i++) {
for (size_t n=0; n<sz; n++) {
I[n] = (int)F[n];
}
}
QueryPerformanceCounter((LARGE_INTEGER*)&t1);
double bandwidth = 8*niter*sz / (((double)(t1-t0))/freq) / 1024/1024/1024;
cout << " " << (t1-t0) << " ticks with " << sz
<< " array elements (" << niter*sz << " touched). "
<< "Bandwidth: " << bandwidth << " GB/s" << endl;
}
cout << "USING xs_CRoundToInt..." << endl;
for (int t=0; t<3; t++) {
__int64 t0,t1;
QueryPerformanceCounter((LARGE_INTEGER*)&t0);
size_t const niter = NIter[t];
size_t const sz = Sz[t];
for (size_t i=0; i<niter; i++) {
for (size_t n=0; n<sz; n++) {
I[n] = xs_CRoundToInt(F[n]);
}
}
QueryPerformanceCounter((LARGE_INTEGER*)&t1);
double bandwidth = 8*niter*sz / (((double)(t1-t0))/freq) / 1024/1024/1024;
cout << " " << (t1-t0) << " ticks with " << sz
<< " array elements (" << niter*sz << " touched). "
<< "Bandwidth: " << bandwidth << " GB/s" << endl;
}
return 0;
}