c++ - programa - GCC no puede optimizar el std:: array alineado como la matriz C
matriz constante c++ (1)
Aquí hay algunos códigos que GCC 6 y 7 no pueden optimizar al usar 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];
}
}
Compilando lo anterior con g++ -std=c++14 -O3 -march=haswell -S -DC_ARRAY
produce un buen código:
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
Eso es básicamente dos iteraciones desenrolladas de agregar cuatro dobles a la vez a través de registros de 256 bits. Pero si compilas sin -DC_ARRAY
, obtienes un gran lío comenzando con esto:
mov rax, rdi
shr rax, 3
neg rax
and eax, 3
je .L7
El código generado en este caso (usando std::array
lugar de una matriz C simple) parece verificar la alineación de la matriz de entrada, incluso si está especificado en typedef como alineado a 32 bytes.
Parece que GCC no comprende que los contenidos de una std::array
estén alineados de la misma manera que la std::array
misma. Esto rompe la suposición de que usar std::array
lugar de C arrays no implica un costo en tiempo de ejecución.
¿Hay algo simple que me falta que solucione esto? Hasta ahora se me ocurrió un truco feo:
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];
}
}
También tenga en cuenta: si my_elements
es 4 en lugar de 8, el problema no ocurre. Si usa Clang, el problema no ocurre.
Puede verlo en vivo aquí: https://godbolt.org/g/IXIOst
Curiosamente, si reemplaza v1[i] += v2a[i];
con v1._M_elems[i] += v2._M_elems[i];
(que obviamente no es portátil), gcc logra optimizar el caso std :: array así como el caso de la matriz C.
Posible interpretación: en los volcados de gcc ( -fdump-tree-all-all
), se puede ver MEM[(struct FooD.25826 *)this_7(D) clique 1 base 0].v1D.25832[i_15]
en la matriz C case, y MEM[(const value_typeD.25834 &)v2_7(D) clique 1 base 1][_1]
para std :: array. Es decir, en el segundo caso, gcc puede haber olvidado que esto es parte del tipo Foo y solo recuerda que está accediendo a un doble.
Esta es una penalización de abstracción que proviene de todas las funciones en línea que uno tiene que atravesar para finalmente ver el acceso a la matriz. Clang todavía logra vectorizar muy bien (¡incluso después de eliminar las alignas!). Esto probablemente significa que clang vectoriza sin preocuparse por la alineación, y de hecho utiliza instrucciones como vmovupd
que no requieren una dirección alineada.
El truco que encontraste, enviar a Vec, es otra forma de dejar que el compilador vea, cuando maneja el acceso a la memoria, que el tipo que se maneja está alineado. Para un ordinario std :: array :: operator [], el acceso a memoria ocurre dentro de una función miembro de std :: array, que no tiene ninguna pista de que *this
está alineado.
Gcc también tiene incorporado para informar al compilador sobre la alineación:
const double*v2a=static_cast<const double*>(__builtin_assume_aligned(v2.data(),32));