Я пытался выяснить проблему производительности в приложении и, наконец, сузил ее до действительно странной проблемы. Следующий фрагмент кода работает в 6 раз медленнее на процессоре Skylake (i5-6500), если команда VZEROUPPER
закомментирована. Я тестировал процессоры Sandy Bridge и Ivy Bridge, и обе версии работают с одинаковой скоростью, с или без VZEROUPPER
.
Теперь у меня неплохое представление о том, что делает VZEROUPPER
, и я думаю, что это не должно иметь никакого значения для этого кода, когда нет кодированных инструкций VEX и нет вызовов какой-либо функции, которая может их содержать. Тот факт, что он не поддерживает другие процессоры, совместимые с AVX, похоже, поддерживает это. Так же, как и таблица 11-2 в Справочное руководство по оптимизации архитектуры Intel® 64 и IA-32
Так что происходит?
Единственная теория, которую я оставил, состоит в том, что в CPU есть ошибка, и она неправильно запускает процедуру "сохранить верхнюю половину регистров 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), Ускорение функции означает, что разница в производительности постепенно исчезает, поэтому похоже, что некоторые инструкции вызывают проблему.
EDIT: Здесь доступна чистая версия сборки для 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, но не постоянное их замедление. Особенно не такой большой. Принимая во внимание накладные расходы на цикл, отношение составляет не менее 10x, возможно, больше.
Я попытался немного возиться с сборкой, а инструкции float так же плохи, как и двойные. Я не мог точно определить проблему для одной инструкции.