私のコンパイラは何をしているのでしょうか?(memcpyの最適化)
-
26-09-2019 - |
質問
VC++2010 で次の設定を使用してコードをコンパイルしています。/O2 /Ob2 /Oi /Ot
ただし、生成されたアセンブリの一部を理解するのに苦労しているため、コードにコメントとしていくつかの質問を追加しました。
また、最新の CPU では一般的にどれくらいのプリフェッチ距離が推奨されますか?自分の CPU で OFC テストを行うことはできますが、より広範囲の CPU で適切に動作する何らかの値を期待していました。おそらく動的なプリフェッチ距離を使用できるでしょうか?
<--編集:
もう 1 つ驚いたのは、コンパイラーが movdqa 命令と movntdq 命令を何らかの形式でインターリーブしないことです。私の理解では、これらの指示はある意味非同期であるためです。
このコードもプリフェッチ時に 32 バイトのキャッシュラインを想定していますが、ハイエンド CPU には 64 バイトのキャッシュラインがあるようですので、プリフェッチのうちの 2 つは削除できる可能性があります。
-->
void memcpy_aligned_x86(void* dest, const void* source, size_t size)
{
0052AC20 push ebp
0052AC21 mov ebp,esp
const __m128i* source_128 = reinterpret_cast<const __m128i*>(source);
for(size_t n = 0; n < size/16; n += 8)
0052AC23 mov edx,dword ptr [size]
0052AC26 mov ecx,dword ptr [dest]
0052AC29 mov eax,dword ptr [source]
0052AC2C shr edx,4
0052AC2F test edx,edx
0052AC31 je copy+9Eh (52ACBEh)
__m128i xmm0 = _mm_setzero_si128();
__m128i xmm1 = _mm_setzero_si128();
__m128i xmm2 = _mm_setzero_si128();
__m128i xmm3 = _mm_setzero_si128();
__m128i xmm4 = _mm_setzero_si128();
__m128i xmm5 = _mm_setzero_si128();
__m128i xmm6 = _mm_setzero_si128();
__m128i xmm7 = _mm_setzero_si128();
__m128i* dest_128 = reinterpret_cast<__m128i*>(dest);
0052AC37 push esi
0052AC38 push edi
0052AC39 lea edi,[edx-1]
0052AC3C shr edi,3
0052AC3F inc edi
{
_mm_prefetch(reinterpret_cast<const char*>(source_128+8), _MM_HINT_NTA);
_mm_prefetch(reinterpret_cast<const char*>(source_128+10), _MM_HINT_NTA);
_mm_prefetch(reinterpret_cast<const char*>(source_128+12), _MM_HINT_NTA);
_mm_prefetch(reinterpret_cast<const char*>(source_128+14), _MM_HINT_NTA);
xmm0 = _mm_load_si128(source_128++);
xmm1 = _mm_load_si128(source_128++);
xmm2 = _mm_load_si128(source_128++);
xmm3 = _mm_load_si128(source_128++);
xmm4 = _mm_load_si128(source_128++);
xmm5 = _mm_load_si128(source_128++);
xmm6 = _mm_load_si128(source_128++);
xmm7 = _mm_load_si128(source_128++);
0052AC40 movdqa xmm6,xmmword ptr [eax+70h] // 1. Why is this moved before the pretecthes?
0052AC45 prefetchnta [eax+80h]
0052AC4C prefetchnta [eax+0A0h]
0052AC53 prefetchnta [eax+0C0h]
0052AC5A prefetchnta [eax+0E0h]
0052AC61 movdqa xmm0,xmmword ptr [eax+10h]
0052AC66 movdqa xmm1,xmmword ptr [eax+20h]
0052AC6B movdqa xmm2,xmmword ptr [eax+30h]
0052AC70 movdqa xmm3,xmmword ptr [eax+40h]
0052AC75 movdqa xmm4,xmmword ptr [eax+50h]
0052AC7A movdqa xmm5,xmmword ptr [eax+60h]
0052AC7F lea esi,[eax+70h] // 2. What is happening in these 2 lines?
0052AC82 mov edx,eax //
0052AC84 movdqa xmm7,xmmword ptr [edx] // 3. Why edx? and not simply eax?
_mm_stream_si128(dest_128++, xmm0);
0052AC88 mov esi,ecx // 4. Is esi never used?
0052AC8A movntdq xmmword ptr [esi],xmm7
_mm_stream_si128(dest_128++, xmm1);
0052AC8E movntdq xmmword ptr [ecx+10h],xmm0
_mm_stream_si128(dest_128++, xmm2);
0052AC93 movntdq xmmword ptr [ecx+20h],xmm1
_mm_stream_si128(dest_128++, xmm3);
0052AC98 movntdq xmmword ptr [ecx+30h],xmm2
_mm_stream_si128(dest_128++, xmm4);
0052AC9D movntdq xmmword ptr [ecx+40h],xmm3
_mm_stream_si128(dest_128++, xmm5);
0052ACA2 movntdq xmmword ptr [ecx+50h],xmm4
_mm_stream_si128(dest_128++, xmm6);
0052ACA7 movntdq xmmword ptr [ecx+60h],xmm5
_mm_stream_si128(dest_128++, xmm7);
0052ACAC lea edx,[ecx+70h]
0052ACAF sub eax,0FFFFFF80h
0052ACB2 sub ecx,0FFFFFF80h
0052ACB5 dec edi
0052ACB6 movntdq xmmword ptr [edx],xmm6 // 5. Why not simply ecx?
0052ACBA jne copy+20h (52AC40h)
0052ACBC pop edi
0052ACBD pop esi
}
}
元のコード:
void memcpy_aligned_x86(void* dest, const void* source, size_t size)
{
assert(dest != nullptr);
assert(source != nullptr);
assert(source != dest);
assert(size % 128 == 0);
__m128i xmm0 = _mm_setzero_si128();
__m128i xmm1 = _mm_setzero_si128();
__m128i xmm2 = _mm_setzero_si128();
__m128i xmm3 = _mm_setzero_si128();
__m128i xmm4 = _mm_setzero_si128();
__m128i xmm5 = _mm_setzero_si128();
__m128i xmm6 = _mm_setzero_si128();
__m128i xmm7 = _mm_setzero_si128();
__m128i* dest_128 = reinterpret_cast<__m128i*>(dest);
const __m128i* source_128 = reinterpret_cast<const __m128i*>(source);
for(size_t n = 0; n < size/16; n += 8)
{
_mm_prefetch(reinterpret_cast<const char*>(source_128+8), _MM_HINT_NTA);
_mm_prefetch(reinterpret_cast<const char*>(source_128+10), _MM_HINT_NTA);
_mm_prefetch(reinterpret_cast<const char*>(source_128+12), _MM_HINT_NTA);
_mm_prefetch(reinterpret_cast<const char*>(source_128+14), _MM_HINT_NTA);
xmm0 = _mm_load_si128(source_128++);
xmm1 = _mm_load_si128(source_128++);
xmm2 = _mm_load_si128(source_128++);
xmm3 = _mm_load_si128(source_128++);
xmm4 = _mm_load_si128(source_128++);
xmm5 = _mm_load_si128(source_128++);
xmm6 = _mm_load_si128(source_128++);
xmm7 = _mm_load_si128(source_128++);
_mm_stream_si128(dest_128++, xmm0);
_mm_stream_si128(dest_128++, xmm1);
_mm_stream_si128(dest_128++, xmm2);
_mm_stream_si128(dest_128++, xmm3);
_mm_stream_si128(dest_128++, xmm4);
_mm_stream_si128(dest_128++, xmm5);
_mm_stream_si128(dest_128++, xmm6);
_mm_stream_si128(dest_128++, xmm7);
}
}
解決
EAX + EAX + 70Hは、EAXは異なるキャッシュラインであり、コンパイラは、おそらくハードウェアのプリフェッチャをできるだけ早くその行を取得して忙しくしたいので、70Hの読み取りが上方に移動されます。
それは(AMDの最適化ガイドは明示的にインターリーブすると言うにもかかわらず)のロード・ツー・ストアの依存関係を回避することにより、パフォーマンスを最大化したい、またはそれは確か店が上書きロードしないことではないという理由だけであるため、これは、インターリーブありませんかん。あなたがソースに__restrictキーワードを追加している場合には、動作を変更していDEST?
はそれの残りの目的は、あまりにも私を見逃さ。 AMDまたはIntelのためのいずれかのいくつかのあいまいな命令デコードまたはハードウェアのプリフェッチャの配慮、かもしれないが、私はそれのために任意の正当化を見つけることができません。あなたはこれらの命令を削除すると、コードが速くなったり遅くなるかしら?
推奨プリフェッチ距離は、ループのサイズによって異なります。これまで十分なデータは、それが必要なのは時間によってメモリから到着する時間を持っていることをする必要があります。私はあなたが通常それには少なくとも100クロックティックを与える必要があることだと思います。
他のヒント
しかし私は私、私は私のテスト結果の一部を共有したいとかかわらず、コンパイラが何をするか考え出したていません。私は、アセンブリ内の関数を書き換えました。
システム:XeonプロセッサW3520
4.55ギガバイト/秒:通常のmemcpyの
5.52ギガバイト/秒:問題のmemcpyの
5.58ギガバイト/秒:
以下のmemcpy7.48ギガバイト/秒:マルチスレッド
以下のmemcpyvoid* memcpy(void* dest, const void* source, size_t num)
{
__asm
{
mov esi, source;
mov edi, dest;
mov ebx, num;
shr ebx, 7;
cpy:
prefetchnta [esi+80h];
prefetchnta [esi+0C0h];
movdqa xmm0, [esi+00h];
movdqa xmm1, [esi+10h];
movdqa xmm2, [esi+20h];
movdqa xmm3, [esi+30h];
movntdq [edi+00h], xmm0;
movntdq [edi+10h], xmm1;
movntdq [edi+20h], xmm2;
movntdq [edi+30h], xmm3;
movdqa xmm4, [esi+40h];
movdqa xmm5, [esi+50h];
movdqa xmm6, [esi+60h];
movdqa xmm7, [esi+70h];
movntdq [edi+40h], xmm4;
movntdq [edi+50h], xmm5;
movntdq [edi+60h], xmm6;
movntdq [edi+70h], xmm7;
lea edi, [edi+80h];
lea esi, [esi+80h];
dec ebx;
jnz cpy;
}
return dest;
}
void* memcpy_tbb(void* dest, const void* source, size_t num)
{
tbb::parallel_for(tbb::blocked_range<size_t>(0, num/128), [&](const tbb::blocked_range<size_t>& r)
{
memcpy_SSE2_3(reinterpret_cast<char*>(dest) + r.begin()*128, reinterpret_cast<const char*>(source) + r.begin()*128, r.size()*128);
}, tbb::affinity_partitioner());
return dest;
}
0052AC82 mov edx,eax //
0052AC84 movdqa xmm7,xmmword ptr [edx] // 3. Why edx? and not simply eax? <--
データパスを適切に分割したいため、この命令は
0052ACAF sub eax,0FFFFFF80h
を並行して実行することができます。
ポイント番号 4 これはプリフェッチャーのヒントである可能性があります...おそらく (そうでないと意味がありません。また、コンパイラ/オプティマイザーのバグ/癖である可能性もあります)。
ポイントについては全く分かりません 5