Voici un exemple utilisant le Bibliothèque Eigen :
#include <Eigen/Core>
std::complex<float> f(const std::complex<float> *x, int n)
{
return Eigen::VectorXcf::Map(x, n).prod();
}
Si vous compilez ceci avec clang ou g++ et sse ou avx activé (et -O2), vous devriez obtenir un code machine assez décent. Cela fonctionne également pour d'autres architectures comme Altivec ou NEON. Si vous savez que la première entrée de x
est aligné, vous pouvez utiliser MapAligned
au lieu de Map
.
Vous obtiendrez un code encore meilleur, si vous connaissez la taille de votre vecteur au moment de la compilation :
template<int n>
std::complex<float> f(const std::complex<float> *x)
{
return Eigen::Matrix<std::complex<float>, n, 1> >::MapAligned(x).prod();
}
Note : Les fonctions ci-dessus correspondent directement à la fonction f
de l'OP. Cependant, comme @PeterCordes l'a souligné, il est généralement mauvais de stocker des nombres complexes entrelacés, car cela nécessitera beaucoup de brassage pour la multiplication. Au lieu de cela, on devrait stocker les parties réelles et imaginaires de manière à ce qu'elles puissent être directement chargées d'un paquet à la fois.
Édition/Addendum : Pour implémenter une structure de tableaux comme la multiplication complexe, vous pouvez en fait écrire quelque chose comme :
typedef Eigen::Array<float, 8, 1> v8sf; // Eigen::Array allows element-wise standard operations
typedef std::complex<v8sf> complex8;
complex8 prod(const complex8& a, const complex8& b)
{
return a*b;
}
Ou plus générique (en utilisant C++11) :
template<int size, typename Scalar = float> using complexX = std::complex<Eigen::Array<Scalar, size, 1> >;
template<int size>
complexX<size> prod(const complexX<size>& a, const complexX<size>& b)
{
return a*b;
}
Lorsqu'il est compilé avec -mavx -O2
la compilation donne quelque chose comme ceci (avec g++-5.4) :
vmovaps 32(%rsi), %ymm1
movq %rdi, %rax
vmovaps (%rsi), %ymm0
vmovaps 32(%rdi), %ymm3
vmovaps (%rdi), %ymm4
vmulps %ymm0, %ymm3, %ymm2
vmulps %ymm4, %ymm1, %ymm5
vmulps %ymm4, %ymm0, %ymm0
vmulps %ymm3, %ymm1, %ymm1
vaddps %ymm5, %ymm2, %ymm2
vsubps %ymm1, %ymm0, %ymm0
vmovaps %ymm2, 32(%rdi)
vmovaps %ymm0, (%rdi)
vzeroupper
ret
Pour des raisons qui ne me sont pas évidentes, ceci est en fait caché dans une méthode qui est appelée par la méthode actuelle, qui ne fait que déplacer de la mémoire -- je ne sais pas pourquoi Eigen/gcc ne suppose pas que les arguments sont déjà correctement alignés. Si je compile la même chose avec clang 3.8.0 (et les mêmes arguments), elle est compilée en juste :
vmovaps (%rsi), %ymm0
vmovaps %ymm0, (%rdi)
vmovaps 32(%rsi), %ymm0
vmovaps %ymm0, 32(%rdi)
vmovaps (%rdi), %ymm1
vmovaps (%rdx), %ymm2
vmovaps 32(%rdx), %ymm3
vmulps %ymm2, %ymm1, %ymm4
vmulps %ymm3, %ymm0, %ymm5
vsubps %ymm5, %ymm4, %ymm4
vmulps %ymm3, %ymm1, %ymm1
vmulps %ymm0, %ymm2, %ymm0
vaddps %ymm1, %ymm0, %ymm0
vmovaps %ymm0, 32(%rdi)
vmovaps %ymm4, (%rdi)
movq %rdi, %rax
vzeroupper
retq
Encore une fois, le déplacement de la mémoire au début est bizarre, mais au moins il est vectorisé. Pour gcc et clang, cela est optimisé lorsqu'il est appelé dans une boucle, cependant :
complex8 f8(complex8 x[], int n) {
if(n==0)
return complex8(v8sf::Ones(),v8sf::Zero()); // I guess you want p = 1 + 0*i at the beginning?
complex8 p = x[0];
for (int i = 1; i < n; i++) p = prod(p, x[i]);
return p;
}
La différence ici est que clang va dérouler cette boucle extérieure à 2 multiplications par boucle. D'un autre côté, gcc utilisera des instructions de type fused-multiply-add lorsqu'il est compilé avec le paramètre -mfma
.
Le site f8
peut bien sûr aussi être généralisée à des dimensions arbitraires :
template<int size>
complexX<size> fX(complexX<size> x[], int n) {
using S= typename complexX<size>::value_type;
if(n==0)
return complexX<size>(S::Ones(),S::Zero());
complexX<size> p = x[0];
for (int i = 1; i < n; i++) p *=x[i];
return p;
}
Et pour réduire la complexX<N>
à un seul std::complex
la fonction suivante peut être utilisée :
// only works for powers of two
template<int size> EIGEN_ALWAYS_INLINE
std::complex<float> redux(const complexX<size>& var) {
complexX<size/2> a(var.real().template head<size/2>(), var.imag().template head<size/2>());
complexX<size/2> b(var.real().template tail<size/2>(), var.imag().template tail<size/2>());
return redux(a*b);
}
template<> EIGEN_ALWAYS_INLINE
std::complex<float> redux(const complexX<1>& var) {
return std::complex<float>(var.real()[0], var.imag()[0]);
}
Cependant, selon que j'utilise clang ou g++, j'obtiens une sortie assembleur très différente. En général, g++ a tendance à ne pas charger en ligne les arguments d'entrée, et clang n'utilise pas les opérations FMA (YMMV ...). Essentiellement, vous devez inspecter le code assembleur généré de toute façon. Et plus important encore, vous devriez évaluer le code (je ne suis pas sûr de l'impact de cette routine sur votre problème global).
Aussi, je voulais noter que Eigen est en fait une bibliothèque d'algèbre linéaire. L'exploiter pour la génération de code SIMD purement portable n'est pas vraiment ce pour quoi elle a été conçue.
0 votes
La première chose à faire serait de vérifier si certaines macros reflètent les paramètres du compilateur.
2 votes
Je n'ai rien pu obtenir avec GCC, mais Clang autovectorise si vous l'aidez un peu, en utilisant la largeur de vecteur disponible.
2 votes
Si vous recherchez une approche totalement générique qui soit optimale pour toutes les tailles de vecteurs, vous ne l'obtiendrez pas avec un type unique comme
float4
. Soit vous faites en sorte que les types de vecteurs soient très grands, soit vous écrivez votre code pour gérer des vecteurs de taille variable.1 votes
Vous obtiendrez de meilleures performances en déroulant avec plusieurs accumulateurs. Indépendamment de la largeur du vecteur, l'asm dans la boucle dans votre question, il embouteille sur les chaînes de dépendance portées par la boucle (vmulps / vfmaddps ont une latence de 4 cycles sur Skylake, mais un débit de 0,5c, donc vous devez exposer suffisamment de parallélisme pour que le CPU garde 8 FMA en vol pour saturer les unités d'exécution). Clang déroule généralement avec plusieurs accumulateurs par défaut, mais gcc ne le fait pas.
0 votes
@Mystical Je pense que si je couvre sse,avx2 et avx512 cela devrait être suffisant. Pourriez-vous expliquer plus en détail le fonctionnement de vos suggestions ? Le tableau x aura une longueur maximale de 50 nombres complexes.
0 votes
@PeterCordes Est-ce que cela implique simplement de dérouler la boucle ou quelque chose de plus sophistiqué ?
1 votes
@eleanora : Si le compilateur ne le fait pas pour vous, déroulez manuellement la boucle et utilisez quatre différents types de
p
variables. Commep0=p1=p2=p3 = {one,one};
. Puis dans la boucle,p0 = complex4_mul(p0, x[i+0]);
p1 = complex4_mul(p1, x[i+1]);
etc. À la fin, combinez les accumulateurs ensemble.p0 = complex4_mul(p0, p1);
Il en va de même pour 2 et 3, puis la finale se résume à un seul vecteur de résultats.0 votes
Avec la syntaxe vectorielle native de GNU C, vous n'avez pas besoin de l'union.
v4sf
supporte déjà[]
pour accéder aux éléments. (Mais évitez généralement de le faire à l'intérieur de boucles internes, car c'est tout aussi lent que de le faire à travers l'union).0 votes
@PeterCordes A quoi ressemblerait le code sans l'union ?
1 votes
Partout où vous utilisez
float4
utiliserv4sf
. (Et puis vous pouvez nettoyer tous les.v
dans le code qui l'utilise).