Editar: Voir la réponse d'Adam ci-dessus pour une version utilisant les intrinsèques SSE. C'est mieux que ce que j'avais ici ...
Pour rendre cela plus utile, regardons ici le code généré par le compilateur. J'utilise gcc 4.8.0 et oui, il il est utile de vérifier votre compilateur spécifique (version) car il y a des différences assez significatives dans les résultats pour, disons, gcc 4.4, 4.8, clang 3.2 ou l'icc d'Intel.
Votre original, utilisant g++ -O8 -msse4.2 ...
se traduit par la boucle suivante :
.L2:
cvtsi2sd (%rcx,%rax,4), %xmm0
mulsd %xmm1, %xmm0
addl $1, %edx
movsd %xmm0, (%rsi,%rax,8)
movslq %edx, %rax
cmpq %rdi, %rax
jbe .L2
donde %xmm1
tient 1.0/32768.0
donc le compilateur transforme automatiquement la division en multiplication par l'inverse.
D'autre part, en utilisant g++ -msse4.2 -O8 -funroll-loops ...
le code créé pour la boucle change de manière significative :
[ ... ]
leaq -1(%rax), %rdi
movq %rdi, %r8
andl $7, %r8d
je .L3
[ ... insert a duff's device here, up to 6 * 2 conversions ... ]
jmp .L3
.p2align 4,,10
.p2align 3
.L39:
leaq 2(%rsi), %r11
cvtsi2sd (%rdx,%r10,4), %xmm9
mulsd %xmm0, %xmm9
leaq 5(%rsi), %r9
leaq 3(%rsi), %rax
leaq 4(%rsi), %r8
cvtsi2sd (%rdx,%r11,4), %xmm10
mulsd %xmm0, %xmm10
cvtsi2sd (%rdx,%rax,4), %xmm11
cvtsi2sd (%rdx,%r8,4), %xmm12
cvtsi2sd (%rdx,%r9,4), %xmm13
movsd %xmm9, (%rcx,%r10,8)
leaq 6(%rsi), %r10
mulsd %xmm0, %xmm11
mulsd %xmm0, %xmm12
movsd %xmm10, (%rcx,%r11,8)
leaq 7(%rsi), %r11
mulsd %xmm0, %xmm13
cvtsi2sd (%rdx,%r10,4), %xmm14
mulsd %xmm0, %xmm14
cvtsi2sd (%rdx,%r11,4), %xmm15
mulsd %xmm0, %xmm15
movsd %xmm11, (%rcx,%rax,8)
movsd %xmm12, (%rcx,%r8,8)
movsd %xmm13, (%rcx,%r9,8)
leaq 8(%rsi), %r9
movsd %xmm14, (%rcx,%r10,8)
movsd %xmm15, (%rcx,%r11,8)
movq %r9, %rsi
.L3:
cvtsi2sd (%rdx,%r9,4), %xmm8
mulsd %xmm0, %xmm8
leaq 1(%rsi), %r10
cmpq %rdi, %r10
movsd %xmm8, (%rcx,%r9,8)
jbe .L39
[ ... out ... ]
Il bloque donc les opérations vers le haut, mais convertit toujours une valeur à la fois.
Si vous modifiez votre boucle originale pour opérer sur quelques éléments par itération :
size_t i;
for (i = 0; i < uIntegers.size() - 3; i += 4)
{
uDoubles[i] = uIntegers[i] / 32768.0;
uDoubles[i+1] = uIntegers[i+1] / 32768.0;
uDoubles[i+2] = uIntegers[i+2] / 32768.0;
uDoubles[i+3] = uIntegers[i+3] / 32768.0;
}
for (; i < uIntegers.size(); i++)
uDoubles[i] = uIntegers[i] / 32768.0;
le compilateur, gcc -msse4.2 -O8 ...
(c'est-à-dire même sans dérouler les requêtes), identifie le potentiel d'utilisation de CVTDQ2PD
/ MULPD
et le cœur de la boucle devient :
.p2align 4,,10
.p2align 3
.L4:
movdqu (%rcx), %xmm0
addq $16, %rcx
cvtdq2pd %xmm0, %xmm1
pshufd $238, %xmm0, %xmm0
mulpd %xmm2, %xmm1
cvtdq2pd %xmm0, %xmm0
mulpd %xmm2, %xmm0
movlpd %xmm1, (%rdx,%rax,8)
movhpd %xmm1, 8(%rdx,%rax,8)
movlpd %xmm0, 16(%rdx,%rax,8)
movhpd %xmm0, 24(%rdx,%rax,8)
addq $4, %rax
cmpq %r8, %rax
jb .L4
cmpq %rdi, %rax
jae .L29
[ ... duff's device style for the "tail" ... ]
.L29:
rep ret
C'est à dire que maintenant le compilateur reconnaît l'opportunité de mettre deux double
par registre SSE, et effectuer des multiplications / conversions parallèles. Ceci est assez proche du code que la version SSE intrinsèque d'Adam générerait.
Le code dans son ensemble (je n'en ai montré qu'environ 1/6e) est beaucoup plus complexe que les intrinsèques "directs", en raison du fait que, comme mentionné, le compilateur essaie de prépendre/appliquer des "têtes" et des "queues" non alignées/non-bloquées multiples à la boucle. Cela dépend largement des tailles moyennes/attendues de vos vecteurs si cela sera bénéfique ou non ; pour le cas "générique" (vecteurs de plus de deux fois la taille du bloc traité par la boucle "innermost"), cela aidera.
Le résultat de cet exercice est, en grande partie ... que, si vous contraignez (par les options/optimisation du compilateur) ou incitez (en réarrangeant légèrement le code) votre compilateur à faire la bonne chose, alors pour ce type spécifique de boucle de copie/conversion, il produit un code qui ne sera pas très en retard sur les intrinsèques écrits à la main.
Expérience finale ... faire le code :
static double c(int x) { return x / 32768.0; }
void Convert(const std::vector<int>& uIntegers, std::vector<double>& uDoubles)
{
std::transform(uIntegers.begin(), uIntegers.end(), uDoubles.begin(), c);
}
et (pour la sortie assembleur la plus agréable à lire, cette fois en utilisant gcc 4.4 avec gcc -O8 -msse4.2 ...
) la boucle de base de l'assemblage généré (encore une fois, il y a un bit pré/post) devient :
.p2align 4,,10
.p2align 3
.L8:
movdqu (%r9,%rax), %xmm0
addq $1, %rcx
cvtdq2pd %xmm0, %xmm1
pshufd $238, %xmm0, %xmm0
mulpd %xmm2, %xmm1
cvtdq2pd %xmm0, %xmm0
mulpd %xmm2, %xmm0
movapd %xmm1, (%rsi,%rax,2)
movapd %xmm0, 16(%rsi,%rax,2)
addq $16, %rax
cmpq %rcx, %rdi
ja .L8
cmpq %rbx, %rbp
leaq (%r11,%rbx,4), %r11
leaq (%rdx,%rbx,8), %rdx
je .L10
[ ... ]
.L10:
[ ... ]
ret
Avec cela, qu'apprenons-nous ? Si vous voulez utiliser le C++, utiliser réellement C++ ;-)