Почему этот код SSE в 6 раз медленнее без VZEROUPPER на Skylake?

Я пытался выяснить проблему с производительностью в приложении и, наконец, сузил ее до действительно странной проблемы. Следующий фрагмент кода работает в 6 раз медленнее на процессоре Skylake (i5-6500), еслиVZEROUPPER Инструкция закомментирована. Я тестировал процессоры Sandy Bridge и Ivy Bridge, и обе версии работают с одинаковой скоростью, с или безVZEROUPPER.

Теперь у меня есть довольно хорошее представление о том, чтоVZEROUPPER делает, и я думаю, что этот код не должен иметь никакого значения, когда нет кодированных инструкций VEX и нет вызовов любой функции, которая может их содержать. Факт, что это не на других процессорах с поддержкой AVX, кажется, поддерживает это. Так же как и таблица 11-2 вСправочное руководство по оптимизации архитектур Intel® 64 и IA-32

Так, что происходит?

Единственная теория, которую я оставил, заключается в том, что в процессоре есть ошибка, и она неправильно запускает процедуру «сохранить верхнюю половину регистров AVX» там, где ее не должно быть. Или что-то еще столь же странное.

Это main.cpp:

#include <immintrin.h>

int slow_function( double i_a, double i_b, double i_c );

int main()
{
    /* DAZ and FTZ, does not change anything here. */
    _mm_setcsr( _mm_getcsr() | 0x8040 );

    /* This instruction fixes performance. */
    __asm__ __volatile__ ( "vzeroupper" : : : );

    int r = 0;
    for( unsigned j = 0; j < 100000000; ++j )
    {
        r |= slow_function( 
                0.84445079384884236262,
                -6.1000481519580951328,
                5.0302160279288017364 );
    }
    return r;
}

и это slow_function.cpp:

#include <immintrin.h>

int slow_function( double i_a, double i_b, double i_c )
{
    __m128d sign_bit = _mm_set_sd( -0.0 );
    __m128d q_a = _mm_set_sd( i_a );
    __m128d q_b = _mm_set_sd( i_b );
    __m128d q_c = _mm_set_sd( i_c );

    int vmask;
    const __m128d zero = _mm_setzero_pd();

    __m128d q_abc = _mm_add_sd( _mm_add_sd( q_a, q_b ), q_c );

    if( _mm_comigt_sd( q_c, zero ) && _mm_comigt_sd( q_abc, zero )  )
    {
        return 7;
    }

    __m128d discr = _mm_sub_sd(
        _mm_mul_sd( q_b, q_b ),
        _mm_mul_sd( _mm_mul_sd( q_a, q_c ), _mm_set_sd( 4.0 ) ) );

    __m128d sqrt_discr = _mm_sqrt_sd( discr, discr );
    __m128d q = sqrt_discr;
    __m128d v = _mm_div_pd(
        _mm_shuffle_pd( q, q_c, _MM_SHUFFLE2( 0, 0 ) ),
        _mm_shuffle_pd( q_a, q, _MM_SHUFFLE2( 0, 0 ) ) );
    vmask = _mm_movemask_pd(
        _mm_and_pd(
            _mm_cmplt_pd( zero, v ),
            _mm_cmple_pd( v, _mm_set1_pd( 1.0 ) ) ) );

    return vmask + 1;
}

Функция компилируется до этого с помощью clang:

 0:   f3 0f 7e e2             movq   %xmm2,%xmm4
 4:   66 0f 57 db             xorpd  %xmm3,%xmm3
 8:   66 0f 2f e3             comisd %xmm3,%xmm4
 c:   76 17                   jbe    25 <_Z13slow_functionddd+0x25>
 e:   66 0f 28 e9             movapd %xmm1,%xmm5
12:   f2 0f 58 e8             addsd  %xmm0,%xmm5
16:   f2 0f 58 ea             addsd  %xmm2,%xmm5
1a:   66 0f 2f eb             comisd %xmm3,%xmm5
1e:   b8 07 00 00 00          mov    $0x7,%eax
23:   77 48                   ja     6d <_Z13slow_functionddd+0x6d>
25:   f2 0f 59 c9             mulsd  %xmm1,%xmm1
29:   66 0f 28 e8             movapd %xmm0,%xmm5
2d:   f2 0f 59 2d 00 00 00    mulsd  0x0(%rip),%xmm5        # 35 <_Z13slow_functionddd+0x35>
34:   00 
35:   f2 0f 59 ea             mulsd  %xmm2,%xmm5
39:   f2 0f 58 e9             addsd  %xmm1,%xmm5
3d:   f3 0f 7e cd             movq   %xmm5,%xmm1
41:   f2 0f 51 c9             sqrtsd %xmm1,%xmm1
45:   f3 0f 7e c9             ,movq   %xmm1,%xmm1
49:   66 0f 14 c1             unpcklpd %xmm1,%xmm0
4d:   66 0f 14 cc             unpcklpd %xmm4,%xmm1
51:   66 0f 5e c8             divpd  %xmm0,%xmm1
55:   66 0f c2 d9 01          cmpltpd %xmm1,%xmm3
5a:   66 0f c2 0d 00 00 00    cmplepd 0x0(%rip),%xmm1        # 63 <_Z13slow_functionddd+0x63>
61:   00 02 
63:   66 0f 54 cb             andpd  %xmm3,%xmm1
67:   66 0f 50 c1             movmskpd %xmm1,%eax
6b:   ff c0                   inc    %eax
6d:   c3                      retq   

Сгенерированный код отличается от gcc, но показывает ту же проблему. Более старая версия компилятора intel генерирует еще один вариант функции, который также показывает проблему, но только еслиmain.cpp не построен с компилятором Intel, поскольку он вставляет вызовы для инициализации некоторых своих собственных библиотек, которые, вероятно, в конечном итоге делаютVZEROUPPER где-то.

И, конечно же, если все это построено с поддержкой AVX, поэтому встроенные функции превращаются в инструкции, закодированные в VEX, проблем также нет.

Я попытался профилировать код сperf в linux и большей части времени выполнения обычно используется 1-2 инструкции, но не всегда одни и те же в зависимости от того, какую версию кода я профилирую (gcc, clang, intel). Укорочение функции, по-видимому, постепенно уменьшает разницу в производительности, поэтому похоже, что некоторые инструкции вызывают проблему.

РЕДАКТИРОВАТЬ: Вот чистая версия сборки, для Linux. Комментарии ниже.

    .text
    .p2align    4, 0x90
    .globl _start
_start:

    #vmovaps %ymm0, %ymm1  # This makes SSE code crawl.
    #vzeroupper            # This makes it fast again.

    movl    $100000000, %ebp
    .p2align    4, 0x90
.LBB0_1:
    xorpd   %xmm0, %xmm0
    xorpd   %xmm1, %xmm1
    xorpd   %xmm2, %xmm2

    movq    %xmm2, %xmm4
    xorpd   %xmm3, %xmm3
    movapd  %xmm1, %xmm5
    addsd   %xmm0, %xmm5
    addsd   %xmm2, %xmm5
    mulsd   %xmm1, %xmm1
    movapd  %xmm0, %xmm5
    mulsd   %xmm2, %xmm5
    addsd   %xmm1, %xmm5
    movq    %xmm5, %xmm1
    sqrtsd  %xmm1, %xmm1
    movq    %xmm1, %xmm1
    unpcklpd    %xmm1, %xmm0
    unpcklpd    %xmm4, %xmm1

    decl    %ebp
    jne    .LBB0_1

    mov $0x1, %eax
    int $0x80

Итак, как и предполагалось в комментариях, использование VEX-кодированных инструкций вызывает замедление. С помощьюVZEROUPPER очищает это. Но это все еще не объясняет почему.

Как я понял, не пользуюсьVZEROUPPER Предполагается, что это потребует затрат на переход к старым инструкциям SSE, но не приведет к их постоянному замедлению. Особенно не такой большой. Принимая во внимание издержки цикла, это соотношение должно быть не менее 10х, а может быть и больше.

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

Ответы на вопрос(1)

Ваш ответ на вопрос