Voici du code que GCC 6 et 7 ne parviennent pas à optimiser lors de l'utilisation de std::array
:
#include <array>
static constexpr size_t my_elements = 8;
class Foo
{
public:
#ifdef C_ARRAY
typedef double Vec[my_elements] alignas(32);
#else
typedef std::array<double, my_elements> Vec alignas(32);
#endif
void fun1(const Vec&);
Vec v1{{}};
};
void Foo::fun1(const Vec& __restrict__ v2)
{
for (unsigned i = 0; i < my_elements; ++i)
{
v1[i] += v2[i];
}
}
Compilation de ce qui précède avec g++ -std=c++14 -O3 -march=haswell -S -DC_ARRAY
produit du code Nice:
vmovapd ymm0, YMMWORD PTR [rdi]
vaddpd ymm0, ymm0, YMMWORD PTR [rsi]
vmovapd YMMWORD PTR [rdi], ymm0
vmovapd ymm0, YMMWORD PTR [rdi+32]
vaddpd ymm0, ymm0, YMMWORD PTR [rsi+32]
vmovapd YMMWORD PTR [rdi+32], ymm0
vzeroupper
Il s'agit essentiellement de deux itérations non déroulées consistant à ajouter quatre doubles à la fois via des registres 256 bits. Mais si vous compilez sans -DC_ARRAY
, vous obtenez un énorme gâchis en commençant par ceci:
mov rax, rdi
shr rax, 3
neg rax
and eax, 3
je .L7
Le code généré dans ce cas (en utilisant std::array
au lieu d'un simple tableau C) semble vérifier l'alignement du tableau d'entrée - même s'il est spécifié dans le typedef comme aligné sur 32 octets.
Il semble que GCC ne comprenne pas que le contenu d'un std::array
sont alignés de la même manière que std::array
lui-même. Cela rompt l'hypothèse selon laquelle l'utilisation de std::array
au lieu des tableaux C n'entraîne pas de coût d'exécution.
Y a-t-il quelque chose de simple qui me manque qui pourrait résoudre ce problème? Jusqu'à présent, je suis venu avec un hack laid:
void Foo::fun2(const Vec& __restrict__ v2)
{
typedef double V2 alignas(Foo::Vec);
const V2* v2a = static_cast<const V2*>(&v2[0]);
for (unsigned i = 0; i < my_elements; ++i)
{
v1[i] += v2a[i];
}
}
Notez également: si my_elements
est 4 au lieu de 8, le problème ne se produit pas. Si vous utilisez Clang, le problème ne se produit pas.
Vous pouvez le voir en direct ici: https://godbolt.org/g/IXIOst
Fait intéressant, si vous remplacez v1[i] += v2a[i];
Par v1._M_elems[i] += v2._M_elems[i];
(Ce qui n'est évidemment pas portable), gcc parvient à optimiser le cas std :: array ainsi que le cas du tableau C.
Interprétation possible: dans les dumps gcc (-fdump-tree-all-all
), On peut voir MEM[(struct FooD.25826 *)this_7(D) clique 1 base 0].v1D.25832[i_15]
dans le cas du tableau C, et MEM[(const value_typeD.25834 &)v2_7(D) clique 1 base 1][_1]
pour std :: array. Autrement dit, dans le deuxième cas, gcc peut avoir oublié que cela fait partie du type Foo et se souvient seulement qu'il accède à un double.
Il s'agit d'une pénalité d'abstraction qui provient de toutes les fonctions en ligne que l'on doit traverser pour enfin voir l'accès au tableau. Clang parvient toujours à bien vectoriser (même après avoir supprimé les alignas!). Cela signifie probablement que clang vectorise sans se soucier de l'alignement, et en fait il utilise des instructions comme vmovupd
qui ne nécessitent pas d'adresse alignée.
Le hack que vous avez trouvé, transtypé en Vec, est une autre façon de laisser le compilateur voir, lorsqu'il gère l'accès à la mémoire, que le type manipulé est aligné. Pour un opérateur std :: array :: standard [], l'accès à la mémoire se produit à l'intérieur d'une fonction membre de std :: array, qui n'a aucun indice que *this
Se trouve être aligné.
Gcc a également une fonction intégrée pour informer le compilateur de l'alignement:
const double*v2a=static_cast<const double*>(__builtin_assume_aligned(v2.data(),32));