2017-01-01 20 views
3

У меня есть простой цикл, который принимает произведение n комплексных чисел. Поскольку я выполняю этот цикл миллионы раз, я хочу, чтобы он был как можно быстрее. Я понимаю, что это можно сделать быстро, используя SSE3 и gcc intrinsics, но меня интересует, можно ли получить gcc для автоматической векторизации кода. Вот некоторые примеры кодаКак включить autovectorization sse3 в gcc

#include <complex.h> 
complex float f(complex float x[], int n) { 
    complex float p = 1.0; 
    for (int i = 0; i < n; i++) 
    p *= x[i]; 
    return p; 
} 

Сборку вы получаете от GCC-S -O3 -ffast-математика:

 .file "test.c" 
     .section  .text.unlikely,"ax",@progbits 
.LCOLDB2: 
     .text 
.LHOTB2: 
     .p2align 4,,15 
     .globl f 
     .type f, @function 
f: 
.LFB0: 
     .cfi_startproc 
     testl %esi, %esi 
     jle  .L4 
     leal -1(%rsi), %eax 
     pxor %xmm2, %xmm2 
     movss .LC1(%rip), %xmm3 
     leaq 8(%rdi,%rax,8), %rax 
     .p2align 4,,10 
     .p2align 3 
.L3: 
     movaps %xmm3, %xmm5 
     movaps %xmm3, %xmm4 
     movss (%rdi), %xmm0 
     addq $8, %rdi 
     movss -4(%rdi), %xmm1 
     mulss %xmm0, %xmm5 
     mulss %xmm1, %xmm4 
     cmpq %rdi, %rax 
     mulss %xmm2, %xmm0 
     mulss %xmm2, %xmm1 
     movaps %xmm5, %xmm3 
     movaps %xmm4, %xmm2 
     subss %xmm1, %xmm3 
     addss %xmm0, %xmm2 
     jne  .L3 
     movaps %xmm2, %xmm1 
.L2: 
     movss %xmm3, -8(%rsp) 
     movss %xmm1, -4(%rsp) 
     movq -8(%rsp), %xmm0 
     ret 
.L4: 
     movss .LC1(%rip), %xmm3 
     pxor %xmm1, %xmm1 
     jmp  .L2 
     .cfi_endproc 
.LFE0: 
     .size f, .-f 
     .section  .text.unlikely 
.LCOLDE2: 
     .text 
.LHOTE2: 
     .section  .rodata.cst4,"aM",@progbits,4 
     .align 4 
.LC1: 
     .long 1065353216 
     .ident "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.4) 5.4.0 20160609" 
     .section  .note.GNU-stack,"",@progbits 
+0

Какова ваша версия gcc? – user902384

+0

@Bugbugbuggerbuggered Я использую gcc 5.4.0, но при необходимости могу обновить. (Версия gcc также находится внизу кода сборки.) – eleanora

+0

Он использует инструкцию SSE, как видно из http://www.felixcloutier.com/x86/MULSS.html – user902384

ответ

2

Проблема в том, что тип complex не является дружественным к SIMD. Я никогда не был поклонником типа complex, потому что это композитный объект, который обычно не сопоставляется с примитивным типом или одной операцией на оборудовании (конечно, не с аппаратным обеспечением x86).

Чтобы сделать сложную арифметическую SIMD-совместимостью, вам необходимо одновременно работать с несколькими сложными номерами. Для SSE вам необходимо работать с четырьмя комплексными номерами одновременно.

Мы можем использовать GCC vector extensions, чтобы упростить синтаксис.

typedef float v4sf __attribute__ ((vector_size (16))); 

Тогда мы можем delcare объединения массива и расширение вектора

typedef union { 
    v4sf v; 
    float e[4]; 
} float4 

И, наконец, мы определяем блок из четырех комплексных чисел как этого

typedef struct { 
    float4 x; 
    float4 y; 
} complex4; 

где x есть четыре реальные части и y - это четыре мнимых компонента.

После того, как мы это мы можем несколько 4 комплексных чисел сразу, как этот

static complex4 complex4_mul(complex4 a, complex4 b) { 
    return (complex4){a.x.v*b.x.v -a.y.v*b.y.v, a.y.v*b.x.v + a.x.v*b.y.v}; 
} 

и, наконец, мы получаем в вашу функцию модифицированной для работы на четырех комплексных чисел одновременно.

complex4 f4(complex4 x[], int n) { 
    v4sf one = {1,1,1,1}; 
    complex4 p = {one,one}; 
    for (int i = 0; i < n; i++) p = complex4_mul(p, x[i]); 
    return p; 
} 

Давайте посмотрим на сборке (Intel синтаксис), чтобы увидеть, если это оптимальное

.L3: 
    movaps xmm4, XMMWORD PTR [rsi] 
    add  rsi, 32 
    movaps xmm1, XMMWORD PTR -16[rsi] 
    cmp  rdx, rsi 
    movaps xmm2, xmm4 
    movaps xmm5, xmm1 
    mulps xmm1, xmm3 
    mulps xmm2, xmm3 
    mulps xmm5, xmm0 
    mulps xmm0, xmm4 
    subps xmm2, xmm5 
    addps xmm0, xmm1 
    movaps xmm3, xmm2 
    jne  .L3 

Это ровно четыре 4-шириной умножений, один 4-широкое добавление и один 4-широкое вычитания. Переменная p остается в регистре, и только массив x загружается из памяти так же, как мы хотим.

Давайте посмотрим на алгебре для произведения комплексных чисел

{a, bi}*{c, di} = {(ac - bd),(bc + ad)i} 

Это точно четыре умножений, одно сложение и одно вычитание.

Как я объяснил в этом answer, эффективная SIMD алгебраически часто идентична скалярной арифметике. Таким образом, мы заменили четыре однократных умножения, сложения и вычитания с четырьмя 4-кратными умножениями, сложениями и вычитанием. Это лучшее, что вы можете сделать с 4-мерным SIMD: четыре по цене одного.

Обратите внимание, что это не требует каких-либо инструкций, кроме SSE, и никаких дополнительных инструкций SSE (кроме FMA4) не будет лучше. Итак, на 64-битной системе вы можете скомпилировать с -O3.

Тривиально расширять его для 8-SIM SIMD с помощью AVX.

Одним из основных преимуществ использования векторных расширений GCC является то, что вы получаете FMA без каких-либо дополнительных усилий. Например. если вы скомпилируете с -O3 -mfma4, основной цикл

.L3: 
    vmovaps xmm0, XMMWORD PTR 16[rsi] 
    add  rsi, 32 
    vmulps xmm1, xmm0, xmm2 
    vmulps xmm0, xmm0, xmm3 
    vfmsubps  xmm1, xmm3, XMMWORD PTR -32[rsi], xmm1 
    vmovaps xmm3, xmm1 
    vfmaddps  xmm2, xmm2, XMMWORD PTR -32[rsi], xmm0 
    cmp  rdx, rsi 
    jne  .L3 
+0

Спасибо за это. Можно ли использовать тот факт, что вам нужно всего три умножения, чтобы умножить два комплексных числа? См. http://math.stackexchange.com/q/465070/66307 – eleanora

+1

@eleanora, вы могли бы это сделать, но я не думаю, что вы этого захотите. Он использует одно меньшее умножение, но еще три добавления/вычитания. Умножение SSE и AVX происходит так же быстро, как сложение/вычитание более или менее (а на добавление Хасуэлла - половину пропускной способности). Я думаю, что он также менее поддается операциям FMA. Давным-давно умножение с плавающей запятой было намного медленнее, чем сложение/вычитание. –

+0

Еще раз спасибо. Как только у меня будет время через несколько дней, я отвечу на ваш ответ в сравнении с тем, что «gcc -march = native -O3 -ffast-math» дает код в вопросе. – eleanora

1

Я не являюсь экспертом в сборе, но мне удалось следующее. Я бы прокомментировал, но это слишком большой:

cat test.s 
    .file "test.c" 
    .text 
    .p2align 4,,15 
    .globl f 
    .type f, @function 
f: 
.LFB0: 
    .cfi_startproc 
    testl %esi, %esi 
    jle  .L4 
    leal -1(%rsi), %eax 
    pxor %xmm0, %xmm0 
    movss .LC1(%rip), %xmm1 
    leaq 8(%rdi,%rax,8), %rax 
    .p2align 4,,10 
    .p2align 3 
.L3: 
    movaps %xmm1, %xmm4 
    movss (%rdi), %xmm3 
    movss 4(%rdi), %xmm2 
    mulss %xmm3, %xmm1 
    mulss %xmm2, %xmm4 
    addq $8, %rdi 
    mulss %xmm0, %xmm2 
    cmpq %rdi, %rax 
    mulss %xmm3, %xmm0 
    subss %xmm2, %xmm1 
    addss %xmm4, %xmm0 
    jne  .L3 
.L1: 
    movss %xmm1, -8(%rsp) 
    movss %xmm0, -4(%rsp) 
    movq -8(%rsp), %xmm0 
    ret 
.L4: 
    movss .LC1(%rip), %xmm1 
    pxor %xmm0, %xmm0 
    jmp  .L1 
    .cfi_endproc 
.LFE0: 
    .size f, .-f 
    .section  .rodata.cst4,"aM",@progbits,4 
    .align 4 
.LC1: 
    .long 1065353216 
    .ident "GCC: (Ubuntu 6.2.0-5ubuntu12) 6.2.0 20161005" 
    .section  .note.GNU-stack,"",@progbits 

Моя команда компиляции была gcc -S -O3 -ffast-math -ftree-vectorizer-verbose=3 -ftree-slp-vectorize -ftree-vectorize -msse3 test.c вам не нужны все из них, как мало получает включен в -O3. См. https://gcc.gnu.org/projects/tree-ssa/vectorization.html

Хотя у меня нет ответа, я попытался помочь. Когда я указываю мой процессор архитектуры (сборки), а я получаю следующее:

.file "test.c" 
    .text 
    .p2align 4,,15 
    .globl f 
    .type f, @function 
f: 
.LFB0: 
    .cfi_startproc 
    testl %esi, %esi 
    jle  .L4 
    vmovss .LC1(%rip), %xmm1 
    leal -1(%rsi), %eax 
    vxorps %xmm0, %xmm0, %xmm0 
    leaq 8(%rdi,%rax,8), %rax 
    .p2align 4,,10 
    .p2align 3 
.L3: 
    vmovss (%rdi), %xmm2 
    vmovss 4(%rdi), %xmm3 
    addq $8, %rdi 
    vmulss %xmm3, %xmm0, %xmm4 
    vmulss %xmm2, %xmm0, %xmm0 
    vfmadd231ss  %xmm3, %xmm1, %xmm0 
    vfmsub132ss  %xmm2, %xmm4, %xmm1 
    cmpq %rdi, %rax 
    jne  .L3 
.L1: 
    vmovss %xmm1, -8(%rsp) 
    vmovss %xmm0, -4(%rsp) 
    vmovq -8(%rsp), %xmm0 
    ret 
.L4: 
    vmovss .LC1(%rip), %xmm1 
    vxorps %xmm0, %xmm0, %xmm0 
    jmp  .L1 
    .cfi_endproc 
.LFE0: 
    .size f, .-f 
    .section  .rodata.cst4,"aM",@progbits,4 
    .align 4 
.LC1: 
    .long 1065353216 
    .ident "GCC: (Ubuntu 6.2.0-5ubuntu12) 6.2.0 20161005" 
    .section  .note.GNU-stack,"",@progbits 

Теперь команда является gcc -S -O3 -ffast-math -msse4 -march=haswell test.c где Haswell мой i7 4770HQ процессор. Обратитесь к this за ваш процессор.

Итак, как вы видите, набор инструкций AVX входит в изображение во второй версии.

Образец эталоном для следующего кода:

$time ./a.out 
0.000000 
real 0m0.684s 
user 0m0.620s 
sys  0m0.060s 


#include <stdio.h> 
#include <complex.h> 
complex float f(complex float x[], long n) { 
    complex float p = 1.0; 
    for (long i = 0; i < n; i++) 
    p *= x[i]; 
    return p; 
} 

int main() 
{ 
    static complex float x[200000000] = {0.0, 1.0, 2.0, 4.0, 5.0, 6.0}; 
    complex float p = f(x, 200000000); 

    printf("%f", creal(p)); 

    return 0; 
} 

Массив является статическим, так большинство из них находится на диске, т.е. SSD жесткого диска. Вы можете выделить его в памяти для еще более быстрой обработки. Это 200M петель. Двоичный - 1.5G, поэтому большую часть времени это IO. CPU пылает его даже без -msse3 и -march. Все, что вам нужно, это -ffast-math. Это вызывает большую разницу.

Я изменил программу следующим:

#include <stdio.h> 
#include <complex.h> 
float f(float x[], long n) { 
    float p = 1.0; 
    for (long i = 0; i < 8; i++) { 
     p = p * x[i]; 
    } 
    return p; 
} 

int main() { 
    float x[8] = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}; 

    printf("%f\n", f(x, 8)); 

    return 0; 
} 

и скомпилирован с gcc -S -O3 -ffast-math -msse3 -mfpmath=sse -mavx -march=haswell test.c, что приводит к:

f: 
.LFB23: 
    .cfi_startproc 
    vmovups (%rdi), %ymm2 
    vxorps %xmm1, %xmm1, %xmm1 
    vperm2f128  $33, %ymm1, %ymm2, %ymm0 
    vmulps %ymm2, %ymm0, %ymm0 
    vperm2f128  $33, %ymm1, %ymm0, %ymm2 
    vshufps $78, %ymm2, %ymm0, %ymm2 
    vmulps %ymm2, %ymm0, %ymm0 
    vperm2f128  $33, %ymm1, %ymm0, %ymm1 
    vpalignr  $4, %ymm0, %ymm1, %ymm1 
    vmulps %ymm1, %ymm0, %ymm0 
    vzeroupper 
    ret 
    .cfi_endproc 

Так что мне кажется, что заставить GCC использовать SSE3 вы узел для кода в некотором роде. http://sci.tuomastonteri.fi/programming/sse будет вам полезна.

Заключительные замечания: Если вы экспериментируете с разными значениями верхнего предела для i, вы увидите, что производятся разные инструкции. Я думаю, что причина этого в том, что gcc не оценивает переменную, поэтому вы можете использовать шаблоны C++, которые способны собирать вычисления времени и делать это.

+0

Вывод с -march = haswell представляет интерес. Я считаю, что канонический код SSE3 для этой проблемы должен использовать mulps, shufps и addsubps. – eleanora

+0

Они были введены в микроархитектуре Хасуэлла. https://software.intel.com/en-us/articles/introduction-to-intel-advanced-vector-extensions – user902384

+0

@eleanora у вас есть то, что вы хотели. – user902384