GCC не может оптимизировать выровненный std:: array как массив C
Здесь некоторый код, который GCC 6 и 7 не может оптимизировать при использовании 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];
}
}
Компиляция выше с помощью g++ -std=c++14 -O3 -march=haswell -S -DC_ARRAY
создает хороший код:
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
Это в основном две разворачиваемые итерации добавления четырех удвоений за один раз через 256-битные регистры. Но если вы компилируете без -DC_ARRAY
, вы получите огромный беспорядок, начиная с этого:
mov rax, rdi
shr rax, 3
neg rax
and eax, 3
je .L7
Код, сгенерированный в этом случае (используя std::array
вместо простого массива C), похоже, проверяет выравнивание входного массива - даже если он указан в typedef как выровненный до 32 байтов.
Кажется, что GCC не понимает, что содержимое std::array
выравнивается так же, как и сам std::array
. Это нарушает предположение, что использование std::array
вместо массивов C не требует затрат времени выполнения.
Есть ли что-то простое, что мне не хватает, что бы это исправить? До сих пор я придумал уродливый взлом:
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];
}
}
Также обратите внимание: если my_elements
равен 4 вместо 8, проблема не возникает. Если вы используете Clang, проблема не возникает.
Здесь вы можете увидеть его здесь: https://godbolt.org/g/IXIOst
Ответы
Ответ 1
Интересно, что если вы замените v1[i] += v2a[i];
на v1._M_elems[i] += v2._M_elems[i];
(что явно не переносимо), gcc может оптимизировать случай std:: array, а также случай массива C.
Возможная интерпретация: в gcc-дампах (-fdump-tree-all-all
) можно увидеть MEM[(struct FooD.25826 *)this_7(D) clique 1 base 0].v1D.25832[i_15]
в массиве массивов C и MEM[(const value_typeD.25834 &)v2_7(D) clique 1 base 1][_1]
для std:: array. То есть во втором случае gcc, возможно, забыл, что это часть типа Foo и только помнит, что он обращается к двойному.
Это штраф за абстракцию, который исходит от всех встроенных функций, которые нужно пройти, чтобы наконец увидеть доступ к массиву. Клану по-прежнему удается красиво векторизовать (даже после удаления alignas!). Вероятно, это означает, что clang векторизовывает, не заботясь о выравнивании, и действительно использует команды типа vmovupd
, для которых не требуется выровненный адрес.
Взломанный вами поиск, отбрасывающий на Vec, является еще одним способом, позволяющим компилятору видеть, когда он обрабатывает доступ к памяти, чтобы обрабатываемый тип был выровнен. Для регулярного std:: array:: operator [] доступ к памяти происходит внутри функции-члена std:: array, которая не имеет никакой подсказки, чтобы выравнивание *this
.
Gcc также имеет встроенную информацию, чтобы компилятор знал о выравнивании:
const double*v2a=static_cast<const double*>(__builtin_assume_aligned(v2.data(),32));