web-dev-qa-db-fra.com

GCC ne parvient pas à optimiser le tableau std :: array aligné comme le tableau C

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

25
John Zwinck

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));
18
Marc Glisse