SSE SIMD Optimisation pour boucle
-
04-10-2019 - |
Question
J'ai un code dans une boucle
for(int i = 0; i < n; i++)
{
u[i] = c * u[i] + s * b[i];
}
Ainsi, u et b sont des vecteurs de même longueur, et c et d sont des scalaires. Est-ce code un bon candidat pour la vectorisation pour une utilisation avec SSE afin d'obtenir un gain de vitesse?
UPDATE
Je l'ai appris vectorisation (tour à tour sur c'est pas si difficile si vous utilisez intrinsics) et mis en œuvre ma boucle SSE. Cependant, lors du réglage du drapeau SSE2 dans le compilateur du VC, je reçois les mêmes performances qu'avec mon propre code SSE. Le compilateur Intel d'autre part était beaucoup plus rapide que mon code SSE ou le compilateur de la VC.
Voici le code que j'ai écrit pour référence
double *u = (double*) _aligned_malloc(n * sizeof(double), 16);
for(int i = 0; i < n; i++)
{
u[i] = 0;
}
int j = 0;
__m128d *uSSE = (__m128d*) u;
__m128d cStore = _mm_set1_pd(c);
__m128d sStore = _mm_set1_pd(s);
for (j = 0; j <= i - 2; j+=2)
{
__m128d uStore = _mm_set_pd(u[j+1], u[j]);
__m128d cu = _mm_mul_pd(cStore, uStore);
__m128d so = _mm_mul_pd(sStore, omegaStore);
uSSE[j/2] = _mm_add_pd(cu, so);
}
for(; j <= i; ++j)
{
u[j] = c * u[j] + s * omegaCache[j];
}
La solution
Oui, c'est un excellent candidat pour la vectorisation. Mais, avant de le faire, Assurez-vous que vous avez votre code PROFILES pour être sûr que ce soit réellement une valeur d'optimisation. Cela dit, la vectorisation irait quelque chose comme ceci:
int i;
for(i = 0; i < n - 3; i += 4)
{
load elements u[i,i+1,i+2,i+3]
load elements b[i,i+1,i+2,i+3]
vector multiply u * c
vector multiply s * b
add partial results
store back to u[i,i+1,i+2,i+3]
}
// Finish up the uneven edge cases (or skip if you know n is a multiple of 4)
for( ; i < n; i++)
u[i] = c * u[i] + s * b[i];
Pour encore plus de performance, vous pouvez envisager préchargement d'autres éléments du tableau, et / ou la boucle et dérouler en utilisant le logiciel pour entrelacer le calcul dans une boucle avec la mémoire accède à partir d'une autre itération.
Autres conseils
probablement oui, mais vous devez compilateur d'aide avec quelques conseils.
__restrict__
placé sur les pointeurs dit compilateur qu'il n'y a aucun alias entre deux pointeurs.
si vous connaissez l'alignement de vos vecteurs, communiquer que compilateur (Visual C ++ peut avoir une certaine facilité).
Je ne suis pas familier avec Visual C ++ moi-même, mais je l'ai entendu, il est pas bon pour la vectorisation. Pensez à utiliser le compilateur Intel à la place. Intel permet un contrôle assez de fin sur l'assemblage généré: http://www.intel.com/software/products/compilers/docs/clin/main_cls/cref_cls/common/cppref_pragma_vector.htm
_mm_set_pd
non vectorisé. Pris à la lettre, il lit les deux doubles en utilisant des opérations scalaires, combine ensuite les deux doubles scalaires et les copier dans le registre SSE. Utilisez _mm_load_pd
à la place.
Oui, c'est un excellent candidat pour vectorizaton, en supposant qu'il n'y a pas de chevauchement de la matrice U et B. Mais le code est lié par un accès de mémoire (chargement / stockage). Vectorisation permet de réduire les cycles par boucle, mais les instructions calera en raison de cache-miss sur la matrice U et B. Le processeur Intel C / C ++ compilateur génère le code suivant de drapeaux par défaut pour le processeur Xeon x5500. Le compilateur se déroule la boucle en huit et emploie des instructions ADD SIMD (de addpd) et MULTIPLY (mulpd) en utilisant XMM [0-16] registres SIMD. Dans chaque cycle, le processeur peut émettre 2 instructions SIMD 4 voies donnant ILP scalaire, supposant que vous avez les données prêtes dans les registres.
Ici, U, B, C et S sont double précision (8 octets).
..B1.14: # Preds ..B1.12 ..B1.10
movaps %xmm1, %xmm3 #5.1
unpcklpd %xmm3, %xmm3 #5.1
movaps %xmm0, %xmm2 #6.12
unpcklpd %xmm2, %xmm2 #6.12
# LOE rax rcx rbx rbp rsi rdi r8 r12 r13 r14 r15 xmm0 xmm1 xmm2 xmm3
..B1.15: # Preds ..B1.15 ..B1.14
movsd (%rsi,%rcx,8), %xmm4 #6.21
movhpd 8(%rsi,%rcx,8), %xmm4 #6.21
mulpd %xmm2, %xmm4 #6.21
movaps (%rdi,%rcx,8), %xmm5 #6.12
mulpd %xmm3, %xmm5 #6.12
addpd %xmm4, %xmm5 #6.21
movaps 16(%rdi,%rcx,8), %xmm7 #6.12
movaps 32(%rdi,%rcx,8), %xmm9 #6.12
movaps 48(%rdi,%rcx,8), %xmm11 #6.12
movaps %xmm5, (%rdi,%rcx,8) #6.3
mulpd %xmm3, %xmm7 #6.12
mulpd %xmm3, %xmm9 #6.12
mulpd %xmm3, %xmm11 #6.12
movsd 16(%rsi,%rcx,8), %xmm6 #6.21
movhpd 24(%rsi,%rcx,8), %xmm6 #6.21
mulpd %xmm2, %xmm6 #6.21
addpd %xmm6, %xmm7 #6.21
movaps %xmm7, 16(%rdi,%rcx,8) #6.3
movsd 32(%rsi,%rcx,8), %xmm8 #6.21
movhpd 40(%rsi,%rcx,8), %xmm8 #6.21
mulpd %xmm2, %xmm8 #6.21
addpd %xmm8, %xmm9 #6.21
movaps %xmm9, 32(%rdi,%rcx,8) #6.3
movsd 48(%rsi,%rcx,8), %xmm10 #6.21
movhpd 56(%rsi,%rcx,8), %xmm10 #6.21
mulpd %xmm2, %xmm10 #6.21
addpd %xmm10, %xmm11 #6.21
movaps %xmm11, 48(%rdi,%rcx,8) #6.3
addq $8, %rcx #5.1
cmpq %r8, %rcx #5.1
jl ..B1.15 # Prob 99% #5.1
il dépend de la façon dont vous avez placé u et b en mémoire. si les deux blocs de mémoire sont loin les uns des autres, SSE ne serait pas coup de fouet dans ce scénario.
il est suggéré que le tableau u et b sont AOE (tableau de la structure) au lieu de SOA (structure du tableau), parce que vous pouvez charger tous les deux dans le registre dans l'instruction unique.