Добавь + Муль медленнее с Intrinsics - где я не прав? - PullRequest
0 голосов
/ 18 декабря 2018

Имея этот массив:

alignas(16) double c[voiceSize][blockSize];

Это функция, которую я пытаюсь оптимизировать:

inline void Process(int voiceIndex, int blockSize) {    
    double *pC = c[voiceIndex];
    double value = start + step * delta;
    double deltaValue = rate * delta;

    for (int sampleIndex = 0; sampleIndex < blockSize; sampleIndex++) {
        pC[sampleIndex] = value + deltaValue * sampleIndex;
    }
}

И это моя попытка встроенной функции (SSE2):

inline void Process(int voiceIndex, int blockSize) {    
    double *pC = c[voiceIndex];
    double value = start + step * delta;
    double deltaValue = rate * delta;

    __m128d value_add = _mm_set1_pd(value);
    __m128d deltaValue_mul = _mm_set1_pd(deltaValue);

    for (int sampleIndex = 0; sampleIndex < blockSize; sampleIndex += 2) {
        __m128d result_mul = _mm_setr_pd(sampleIndex, sampleIndex + 1);
        result_mul = _mm_mul_pd(result_mul, deltaValue_mul);
        result_mul = _mm_add_pd(result_mul, value_add);

        _mm_store_pd(pC + sampleIndex, result_mul);
    }   
}

Что медленнее, чем "скалярный" (даже если он автоматически оптимизирован) оригинальный код, к сожалению:)

Где, по вашему мнению, узкое место?Где я не прав?

Я использую MSVC, Release/x86, /02 флаг оптимизации (Favor fast code).

РЕДАКТИРОВАТЬ : делаю это(предложено @wim), похоже, что производительность стала лучше, чем версия C:

inline void Process(int voiceIndex, int blockSize) {    
    double *pC = c[voiceIndex];
    double value = start + step * delta;
    double deltaValue = rate * delta;

    __m128d value_add = _mm_set1_pd(value);
    __m128d deltaValue_mul = _mm_set1_pd(deltaValue);

    __m128d sampleIndex_acc = _mm_set_pd(-1.0, -2.0);
    __m128d sampleIndex_add = _mm_set1_pd(2.0);

    for (int sampleIndex = 0; sampleIndex < blockSize; sampleIndex += 2) {
        sampleIndex_acc = _mm_add_pd(sampleIndex_acc, sampleIndex_add);
        __m128d result_mul = _mm_mul_pd(sampleIndex_acc, deltaValue_mul);
        result_mul = _mm_add_pd(result_mul, value_add);

        _mm_store_pd(pC + sampleIndex, result_mul);
    }
}

Почему?_mm_setr_pd дорого?

Ответы [ 2 ]

0 голосов
/ 19 декабря 2018

Почему?Дорого ли _mm_setr_pd?

В некотором смысле;это займет хотя бы случайное перемешивание.Что еще более важно в этом случае, вычисление каждого скалярного операнда стоит дорого, и, как показывает ответ @spectras, gcc по крайней мере не может автоматически векторизовать это в paddd / cvtdq2pd.Вместо этого он заново вычисляет каждый операнд из скалярного целого числа, выполняя преобразование int -> double отдельно, а затем перемешивает их вместе.

Это функция, которую я пытаюсь оптимизировать:

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

т.е. вы вычисляете a[i] = c + i*scale отдельно для каждого шага.Но вместо вы можете уменьшить его до a[i+n] = a[i] + (n*scale).Таким образом, у вас есть только одна addpd инструкция на вектор результатов.

Это приведет к некоторой ошибке округления, которая накапливается по сравнению с повторным вычислением с нуля, но double, вероятно, излишне для вас.в любом случае.

Это также происходит за счет введения последовательной зависимости от добавления FP вместо целого числа.Но в вашей «оптимизированной» версии уже есть цепочка зависимостей добавления FP, переносимая в цикле, которая использует внутри цикла sampleIndex_acc = _mm_add_pd(sampleIndex_acc, sampleIndex_add);, используя FP + = 2.0 вместо повторного преобразования из целого числа.

Итаквам нужно будет развернуть несколько векторов, чтобы скрыть задержку FP, и сохранить как минимум 3 или 4 дополнения FP в полете одновременно .(Haswell: задержка 3 цикла, один на тактовую пропускную способность. Skylake: задержка 4 цикла, 2 на тактовую пропускную способность.) См. Также Почему mulss занимает только 3 цикла на Haswell, в отличие от таблиц инструкций Агнера? для получения дополнительной информациио развертывании с несколькими аккумуляторами для аналогичной проблемы с зависимостями, переносимыми циклом (точечный продукт).

void Process(int voiceIndex, int blockSize) {    
    double *pC = c[voiceIndex];
    double val0 = start + step * delta;
    double deltaValue = rate * delta;

    __m128d vdelta2 = _mm_set1_pd(2 * deltaValue);
    __m128d vdelta4 = _mm_add_pd(vdelta2, vdelta2);

    __m128d v0 = _mm_setr_pd(val0, val0 + deltaValue);
    __m128d v1 = _mm_add_pd(v0, vdelta2);
    __m128d v2 = _mm_add_pd(v0, vdelta4);
    __m128d v3 = _mm_add_pd(v1, vdelta4);

    __m128d vdelta8 = _mm_mul_pd(vdelta2, _mm_set1_pd(4.0));

    double *endp = pC + blocksize - 7;  // stop if there's only room for 7 or fewer doubles
      // or use -8 and have your cleanup handle lengths of 1..8
      // since the inner loop always calculates results for next iteration
    for (; pC < endp ; pC += 8) {
        _mm_store_pd(pC, v0);
        v0 = _mm_add_pd(v0, vdelta8);

        _mm_store_pd(pC+2, v1);
        v1 = _mm_add_pd(v1, vdelta8);

        _mm_store_pd(pC+4, v2);
        v2 = _mm_add_pd(v2, vdelta8);

        _mm_store_pd(pC+6, v3);
        v3 = _mm_add_pd(v3, vdelta8);
    }
    // if (blocksize % 8 != 0) ... store final vectors
}

Выбор, добавлять или умножать при создании vdelta4 / vdelta8, не оченьзначительное;Я пытался избежать слишком длинной цепочки зависимостей, прежде чем появятся первые магазины.Поскольку от 1041 * до v3 также необходимо вычислить, казалось, что имеет смысл создать vdelta4 вместо того, чтобы просто создавать цепочку v2 = v1+vdelta2.Возможно, было бы лучше создать vdelta4 с умножением от 4.0*delta и удвоить его, чтобы получить vdelta8.Это может иметь отношение к очень маленькому размеру блока, особенно если вы кешируете свой код, генерируя только небольшие куски этого массива по мере необходимости, прямо перед его чтением.

В любом случае, это компилируется в очень эффективныйвнутренний цикл с gcc и MSVC ( в проводнике компилятора Godbolt ).

;; MSVC -O2
$LL4@Process:                    ; do {
    movups  XMMWORD PTR [rax], xmm5
    movups  XMMWORD PTR [rax+16], xmm0
    movups  XMMWORD PTR [rax+32], xmm1
    movups  XMMWORD PTR [rax+48], xmm2
    add     rax, 64                             ; 00000040H
    addpd   xmm5, xmm3              ; v0 += vdelta8
    addpd   xmm0, xmm3              ; v1 += vdelta8
    addpd   xmm1, xmm3              ; v2 += vdelta8
    addpd   xmm2, xmm3              ; v3 += vdelta8
    cmp     rax, rcx
    jb      SHORT $LL4@Process   ; }while(pC < endp)

Это имеет 4 отдельных цепочки зависимостей, через xmm0, 1, 2 и 5. Так что достаточно инструкции-уровень параллелизма для сохранения 4 addpd инструкций в полете.Этого более чем достаточно для Haswell, но половина того, что может выдержать Skylake.

Тем не менее, с пропускной способностью магазина 1 вектор на такт, более 1 addpd на такт бесполезна. Теоретически это может быть около 16 байтов за такт, и насыщать пропускную способность хранилища. т.е. 1 вектор / 2 double с на такт.

AVX с более широкими векторами (4 double s) все еще может идти с 1 вектором за такт в Haswell и позже, то есть 32 байта за такт.(Предполагая, что выходной массив горячий в кеше L1d или, возможно, даже в L2.)


Еще лучше: вообще не хранить эти данные в памяти;сгенерируйте на лету.

Создайте его на лету, когда это необходимо, если код, потребляющий его, только читает его несколько раз, а также векторизует вручную.

0 голосов
/ 18 декабря 2018

В моей системе g++ test.cpp -march=native -O2 -c -o test

Это выведет для обычной версии (извлечение тела цикла):

  30:   c5 f9 57 c0             vxorpd %xmm0,%xmm0,%xmm0
  34:   c5 fb 2a c0             vcvtsi2sd %eax,%xmm0,%xmm0
  38:   c4 e2 f1 99 c2          vfmadd132sd %xmm2,%xmm1,%xmm0
  3d:   c5 fb 11 04 c2          vmovsd %xmm0,(%rdx,%rax,8)
  42:   48 83 c0 01             add    $0x1,%rax
  46:   48 39 c8                cmp    %rcx,%rax
  49:   75 e5                   jne    30 <_Z11ProcessAutoii+0x30>

И для встроенной версии:

  88:   c5 f9 57 c0             vxorpd %xmm0,%xmm0,%xmm0
  8c:   8d 50 01                lea    0x1(%rax),%edx
  8f:   c5 f1 57 c9             vxorpd %xmm1,%xmm1,%xmm1
  93:   c5 fb 2a c0             vcvtsi2sd %eax,%xmm0,%xmm0
  97:   c5 f3 2a ca             vcvtsi2sd %edx,%xmm1,%xmm1
  9b:   c5 f9 14 c1             vunpcklpd %xmm1,%xmm0,%xmm0
  9f:   c4 e2 e9 98 c3          vfmadd132pd %xmm3,%xmm2,%xmm0
  a4:   c5 f8 29 04 c1          vmovaps %xmm0,(%rcx,%rax,8)
  a9:   48 83 c0 02             add    $0x2,%rax
  ad:   48 39 f0                cmp    %rsi,%rax
  b0:   75 d6                   jne    88 <_Z11ProcessSSE2ii+0x38>

Короче говоря: компилятор автоматически генерирует AVX-код из версии C.

Отредактируйте после воспроизведения немного с флагами, чтобы иметь SSE2 только в обоих случаях:

g++ test.cpp -msse2 -O2 -c -o test

Компилятор все еще делает что-то отличное от того, что вы генерируете с помощью встроенных функций.Версия компилятора:

  30:   66 0f ef c0             pxor   %xmm0,%xmm0
  34:   f2 0f 2a c0             cvtsi2sd %eax,%xmm0
  38:   f2 0f 59 c2             mulsd  %xmm2,%xmm0
  3c:   f2 0f 58 c1             addsd  %xmm1,%xmm0
  40:   f2 0f 11 04 c2          movsd  %xmm0,(%rdx,%rax,8)
  45:   48 83 c0 01             add    $0x1,%rax
  49:   48 39 c8                cmp    %rcx,%rax
  4c:   75 e2                   jne    30 <_Z11ProcessAutoii+0x30>

Версия встроенной функции:

  88:   66 0f ef c0             pxor   %xmm0,%xmm0
  8c:   8d 50 01                lea    0x1(%rax),%edx
  8f:   66 0f ef c9             pxor   %xmm1,%xmm1
  93:   f2 0f 2a c0             cvtsi2sd %eax,%xmm0
  97:   f2 0f 2a ca             cvtsi2sd %edx,%xmm1
  9b:   66 0f 14 c1             unpcklpd %xmm1,%xmm0
  9f:   66 0f 59 c3             mulpd  %xmm3,%xmm0
  a3:   66 0f 58 c2             addpd  %xmm2,%xmm0
  a7:   0f 29 04 c1             movaps %xmm0,(%rcx,%rax,8)
  ab:   48 83 c0 02             add    $0x2,%rax
  af:   48 39 f0                cmp    %rsi,%rax
  b2:   75 d4                   jne    88 <_Z11ProcessSSE2ii+0x38>

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

...