Способны ли Clang или GCC автовекторизировать развернутые вручную циклы?

У меня есть идея стиля кода для написания определенных типов числовых алгоритмов, где вы пишете свой алгоритм исключительно в агностической манере размещения данных.

то есть все ваши функции принимают (один или несколько) скалярных аргументов и возвращают (через указатель) одно или несколько скалярных возвращаемых значений. Так, например, если у вас есть функция, которая принимает трехмерный вектор с плавающей запятой, вместо структуры с тремя элементами или float[3] xyz вы берете float x, float y, float z.

Идея состоит в том, что вы можете изменить макет ваших входных и выходных данных, то есть вы можете играть со структурой массива против массива структуры данных, мозаичными макетами для эффективности кэша, SIMD против многоядерной детализации и т. д. БЕЗ наличия переписать весь ваш код для всех комбинаций макетов данных.

Стратегия имеет ряд очевидных недостатков:

  • Вы не можете использовать циклы for внутри своих функций, чтобы сделать ваш код более компактным.
  • Вашим функциям нужно больше параметров в сигнатурах

... но это приемлемо, если ваши массивы короткие, и это избавляет вас от необходимости многократно переписывать код, чтобы сделать его быстрым.

Но, в частности, меня беспокоит, что компиляторы не смогут принять такие вещи, как x+=a; у+=б; г+=с; w+=d и автовекторизовать его в одно векторное добавление SIMD в случае, когда вы хотите выполнять SIMD в нижней части стека вызовов, а не выполнять SIMD в верхней части стека встроенных функций.

Способны ли clang и/или gcc «перекатывать» развернутые вручную циклы в коде C и/или C++ (вероятно, после встраивания функций) и генерировать векторизованный машинный код?


person Andrew Wagner    schedule 06.12.2016    source источник
comment
Это невозможно в C. Выберите один язык. И весь вопрос слишком широк. Мы не дискуссионный сайт.   -  person too honest for this site    schedule 06.12.2016
comment
Привет, Олаф, возможно, твой ответ «нет» правильный, и нет необходимости в 4 копиях одного и того же вопроса.   -  person Andrew Wagner    schedule 06.12.2016
comment
То есть вы намеренно разместили дубликат?   -  person too honest for this site    schedule 06.12.2016
comment
Я должен согласиться, этот вопрос должен выбрать только один язык.   -  person Vality    schedule 06.12.2016
comment
Встроенные функции SIMD одинаковы в C и C++, выбор одной из них не имеет никакого значения.   -  person harold    schedule 06.12.2016


Ответы (2)


Я написал код для тривиальной проверки своей идеи:

// Compile using gcc -O4 main.c && objdump -d a.out

void add4(float x0, float x1, float x2, float x3, 
          float y0, float y1, float y2, float y3, 
          float* out0, float* out1, float* out2, float* out3) {
  // Non-inlined version of this uses xmm registers and four separate
  // SIMD operations
    *out0 = x0 + y0;
    *out1 = x1 + y1;
    *out2 = x2 + y2;
    *out3 = x3 + y3;
}
void sub4(float x0, float x1, float x2, float x3,
          float y0, float y1, float y2, float y3,
          float* out0, float* out1, float* out2, float* out3) {
    *out0 = x0 - y0;
    *out1 = x1 - y1;
    *out2 = x2 - y2;
    *out3 = x3 - y3;
}
void add4_then_sub4(float x0, float x1, float x2, float x3,
          float y0, float y1, float y2, float y3,
          float z0, float z1, float z2, float z3,
          float* out0, float* out1, float* out2, float* out3) {
    // In non-inlined version of this, add4 and sub4 get inlined.
    // xmm regiesters get re-used for the add and subtract,
    // but there is still no 4-way SIMD
  float temp0,temp1,temp2,temp3;
  // temp= x + y
  add4(x0,x1,x2,x3,
       y0,y1,y2,y3,
       &temp0,&temp1,&temp2,&temp3);
  // out = temp - z
  sub4(temp0,temp1,temp2,temp3,
       z0,z1,z2,z3,
       out0,out1,out2,out3);
}
void add4_then_sub4_arrays(const float x[4],
                                const float y[4],
                                const float z[4],
                                float out[4])
{
    // This is a stand-in for the main function below, but since the arrays are aguments,
    // they can't be optimized out of the non-inlined version of this function.
    // THIS version DOES compile into (I think) a bunch of non-aligned moves,
    // and a single vectorized add a single vectorized subtract
    add4_then_sub4(x[0],x[1],x[2],x[3],
            y[0],y[1],y[2],y[3],
            z[0],z[1],z[2],z[3],
            &out[0],&out[1],&out[2],&out[3]
            );
}

int main(int argc, char **argv) 
{
}

Рассмотрим сгенерированную сборку для add4_then_sub4_arrays:

0000000000400600 <add4_then_sub4_arrays>:
  400600:       0f 57 c0                xorps  %xmm0,%xmm0
  400603:       0f 57 c9                xorps  %xmm1,%xmm1
  400606:       0f 12 06                movlps (%rsi),%xmm0
  400609:       0f 12 0f                movlps (%rdi),%xmm1
  40060c:       0f 16 46 08             movhps 0x8(%rsi),%xmm0
  400610:       0f 16 4f 08             movhps 0x8(%rdi),%xmm1
  400614:       0f 58 c1                addps  %xmm1,%xmm0
  400617:       0f 57 c9                xorps  %xmm1,%xmm1
  40061a:       0f 12 0a                movlps (%rdx),%xmm1
  40061d:       0f 16 4a 08             movhps 0x8(%rdx),%xmm1
  400621:       0f 5c c1                subps  %xmm1,%xmm0
  400624:       0f 13 01                movlps %xmm0,(%rcx)
  400627:       0f 17 41 08             movhps %xmm0,0x8(%rcx)
  40062b:       c3                      retq   
  40062c:       0f 1f 40 00             nopl   0x0(%rax)

Массивы не выровнены, поэтому операций перемещения намного больше, чем идеально, и я не уверен, что там делает этот xor, но действительно есть одно 4-стороннее сложение и одно 4-стороннее вычитание по желанию.

Таким образом, ответ заключается в том, что у gcc есть, по крайней мере, некоторая способность упаковывать скалярные операции с плавающей запятой обратно в SIMD-операции.

Обновление: более жесткий код с обоими gcc-4.8 -O3 -march=native main.c && objdump -d a.out:

0000000000400600 <add4_then_sub4_arrays>:
  400600:       c5 f8 10 0e             vmovups (%rsi),%xmm1
  400604:       c5 f8 10 07             vmovups (%rdi),%xmm0
  400608:       c5 f0 58 c0             vaddps %xmm0,%xmm1,%xmm0
  40060c:       c5 f8 10 0a             vmovups (%rdx),%xmm1
  400610:       c5 f8 5c c1             vsubps %xmm1,%xmm0,%xmm0
  400614:       c5 f8 11 01             vmovups %xmm0,(%rcx)
  400618:       c3                      retq   
  400619:       0f 1f 80 00 00 00 00    nopl   0x0(%rax)

и с clang-4.0 -O3 -march=native main.c && llvm-objdump -d a.out:

add4_then_sub4_arrays:
  4005e0:       c5 f8 10 07                                     vmovups (%rdi), %xmm0
  4005e4:       c5 f8 58 06                                     vaddps  (%rsi), %xmm0, %xmm0
  4005e8:       c5 f8 5c 02                                     vsubps  (%rdx), %xmm0, %xmm0
  4005ec:       c5 f8 11 01                                     vmovups %xmm0, (%rcx)
  4005f0:       c3                                              ret
  4005f1:       66 66 66 66 66 66 2e 0f 1f 84 00 00 00 00 00    nopw    %cs:(%rax,%rax)
person Andrew Wagner    schedule 06.12.2016
comment
xor используется для разрыва зависимости, которую movlps в противном случае имело бы от старого значения. - person harold; 06.12.2016
comment
xorps + movlps — это мертвая альтернатива movsd (%rdx), %xmm1. И затем за ним следуют movhps из смежных байтов? Что за черт? Какой компилятор использовали, с какими настройками? Очевидно, что movups (%rdx), %xmm1 будет более эффективным, особенно на любом недавнем процессоре. Выполнение невыровненной загрузки пополам было разумной стратегией на некоторых достаточно старых процессорах. - person Peter Cordes; 07.12.2016
comment
проголосовали за тестирование и показали, что эта реализация вашей идеи нежизнеспособна с компилятором + опциями, которые вы тестировали. От 3x до 4x количество инструкций для исходных данных памяти просто смешно. (И все эти пары movlps + movhps будут узким местом в порту тасования, поскольку они являются инструкциями загрузки и смешивания. См. agner. org/optimize для таблиц инструкций и вики тегов x86). - person Peter Cordes; 07.12.2016
comment
Спасибо Питер! Я пойду дальше и выберу компилятор и выясню правильные флаги для архитектуры. - person Andrew Wagner; 07.12.2016

Ваша забота верна. Ни один компилятор не будет автоматически векторизовать эти 4 добавления. Это просто не стоит того, учитывая, что входы не являются смежными и не выровнены. Стоимость сбора аргументов в регистр SIMD намного выше, чем экономия на добавлении вектора.

Конечно, причина, по которой компилятор не может использовать выровненную потоковую загрузку, заключается в том, что вы передали аргументы как скаляры.

person MSalters    schedule 06.12.2016
comment
Привет! Идея состоит в том, что вы размещаете эти скалярные аргументы линейно вне функции. Если функция встроена, у компилятора есть структура данных и то же самое определение того, что происходит со значениями, но не выраженное с помощью цикла for. Я работаю над более конкретным примером. - person Andrew Wagner; 06.12.2016
comment
В наши дни регистры SIMD часто используются, даже если это всего лишь одна операция с плавающей запятой. - person Andrew Wagner; 06.12.2016
comment
Компиляторы могут и делают векторизацию одной операции с шириной вектора, если были переданы непрерывные указатели и функция была встроена, чтобы компилятор знал об этом. Особенно clang хорош в этом. (могу нарыть пример, если хотите). - person Peter Cordes; 07.12.2016