Подтвердить что ты не робот

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

4b9b3361

Ответ 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));