Qu'est-ce que mon compilateur fait? (Optimisation memcpy)
-
26-09-2019 - |
Question
Je compile un peu de code à l'aide des paramètres suivants dans VC ++ 2010: / O2 / Ob2 / Oi / Ot
Cependant, je vais avoir du mal à comprendre certaines parties de l'ensemble généré, je mets quelques questions dans le code sous forme de commentaires.
En outre, quelle distance préchargement est généralement recommandé sur CPUs moderne? J'OfC test sur mon cpu, mais j'espérais une valeur qui va bien travailler sur un plus large éventail de CPUs. Peut-être que l'on pourrait utiliser des distances préchargement dynamique?
<- EDIT:
Une autre chose que je suis surpris que le compilateur ne entrelacer sous une forme les instructions de movdqa et movntdq? Étant donné que ces instructions sont en quelque asynchrone sens de ma compréhension.
Ce code suppose également 32 octets lignes de cache lorsque cependant préchargement, il semble que CPUs haut de gamme ont 64 cachelines d'octets, donc 2 des prélectures peuvent probablement être enlevés.
->
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
}
}
code original:
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);
}
}
La solution
eax + lecture de 70h est déplacé parce que eax + 70h est dans une autre ligne de cache de eax, et le compilateur veut probablement le prefetcher matériel pour obtenir occupés à cette ligne le plus rapidement possible.
Il ne entrelacer soit parce qu'elle veut maximiser les performances en évitant les dépendances de charge à magasin (même si le guide d'optimisation AMD dit explicitement Interleave), ou tout simplement parce qu'il ne sait pas que les magasins seront charges ne écrasent pas. Est-ce que ça change le comportement si vous ajoutez des mots clés __restrict à la source et dest?
Le but du reste de celui-ci me échappe aussi. Peut-être quelques décodage d'instructions obscures ou considérations Prefetcher matériel, que ce soit pour AMD ou Intel, mais je ne peux trouver aucune justification. Je me demande si le code devient plus rapide ou plus lent lorsque vous supprimez ces instructions?
La distance préchargement recommandée dépend de la taille de la boucle. Doit être suffisamment loin que les données a le temps d'arriver de la mémoire au moment où il est nécessaire. Je pense que vous devez généralement il donner au moins 100 tiques d'horloge.
Autres conseils
Je n'ai pas compris ce que le compilateur fait, mais je dirais que je partage certains de mes résultats des tests. Je l'ai réécrit la fonction dans l'assemblage.
Système: Xeon W3520
4,55 Go / s: memcpy régulière
5,52 Go / s: memcpy en question
5,58 Go / s: memcpy ci-dessous
7,48 Go / s: memcpy ci-dessous multithread
void* 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? <--
parce qu'il veut propably de diviser le chemin de données si cette instruction
0052ACAF sub eax,0FFFFFF80h
peut être exécuté en parallèle.
Numéro de Point 4 pourrait être un indice pour la prefetcher ... propably (parce que sinon il ne fait aucun sens, pourrait aussi être un compilateur / optimiseur bug / bizarrerie).
Je n'ai aucune idée sur le point 5