Вызов инструкции fsincos в LLVM медленнее, чем вызов функций libc sin/cos?
Я работаю над языком, который компилируется с LLVM. Ради интереса я хотел сделать несколько микробенчмарков. В одном из них я запускаю несколько миллионов вычислений sin / cos в цикле. В псевдокоде это выглядит так:
var x: Double = 0.0
for (i <- 0 to 100 000 000)
x = sin(x)^2 + cos(x)^2
return x.toInteger
Если я вычисляю sin / cos, используя встроенную сборку LLVM IR в форме:
%sc = call { double, double } asm "fsincos", "={st(1)},={st},1,~{dirflag},~{fpsr},~{flags}" (double %"res") nounwind
это быстрее, чем использовать fsin и fcos отдельно вместо fsincos. Тем не менее, это медленнее, чем если бы я звонил llvm.sin.f64
а также llvm.cos.f64
intrinsics отдельно, которые компилируются для вызовов функций C math lib, по крайней мере с целевыми настройками, которые я использую (x86_64 с включенным SSE).
Кажется, LLVM вставляет некоторые преобразования между FP одинарной / двойной точности - это может быть причиной. Это почему? Извините, я относительный новичок на сборке:
.globl main
.align 16, 0x90
.type main,@function
main: # @main
.cfi_startproc
# BB#0: # %loopEntry1
xorps %xmm0, %xmm0
movl $-1, %eax
jmp .LBB44_1
.align 16, 0x90
.LBB44_2: # %then4
# in Loop: Header=BB44_1 Depth=1
movss %xmm0, -4(%rsp)
flds -4(%rsp)
#APP
fsincos
#NO_APP
fstpl -16(%rsp)
fstpl -24(%rsp)
movsd -16(%rsp), %xmm0
mulsd %xmm0, %xmm0
cvtsd2ss %xmm0, %xmm1
movsd -24(%rsp), %xmm0
mulsd %xmm0, %xmm0
cvtsd2ss %xmm0, %xmm0
addss %xmm1, %xmm0
.LBB44_1: # %loop2
# =>This Inner Loop Header: Depth=1
incl %eax
cmpl $99999999, %eax # imm = 0x5F5E0FF
jle .LBB44_2
# BB#3: # %break3
cvttss2si %xmm0, %eax
ret
.Ltmp160:
.size main, .Ltmp160-main
.cfi_endproc
Тот же тест с вызовами llvm sin / cos intrinsics:
.globl main
.align 16, 0x90
.type main,@function
main: # @main
.cfi_startproc
# BB#0: # %loopEntry1
pushq %rbx
.Ltmp162:
.cfi_def_cfa_offset 16
subq $16, %rsp
.Ltmp163:
.cfi_def_cfa_offset 32
.Ltmp164:
.cfi_offset %rbx, -16
xorps %xmm0, %xmm0
movl $-1, %ebx
jmp .LBB44_1
.align 16, 0x90
.LBB44_2: # %then4
# in Loop: Header=BB44_1 Depth=1
movsd %xmm0, (%rsp) # 8-byte Spill
callq cos
mulsd %xmm0, %xmm0
movsd %xmm0, 8(%rsp) # 8-byte Spill
movsd (%rsp), %xmm0 # 8-byte Reload
callq sin
mulsd %xmm0, %xmm0
addsd 8(%rsp), %xmm0 # 8-byte Folded Reload
.LBB44_1: # %loop2
# =>This Inner Loop Header: Depth=1
incl %ebx
cmpl $99999999, %ebx # imm = 0x5F5E0FF
jle .LBB44_2
# BB#3: # %break3
cvttsd2si %xmm0, %eax
addq $16, %rsp
popq %rbx
ret
.Ltmp165:
.size main, .Ltmp165-main
.cfi_endproc
Можете ли вы предложить, как будет выглядеть идеальная сборка с fsincos? PS. Добавление -enable-unsafe-fp-math к llc приводит к исчезновению конверсий и переключению на удвоения (fldl и т. Д.), Но скорость остается неизменной.
.globl main
.align 16, 0x90
.type main,@function
main: # @main
.cfi_startproc
# BB#0: # %loopEntry1
xorps %xmm0, %xmm0
movl $-1, %eax
jmp .LBB44_1
.align 16, 0x90
.LBB44_2: # %then4
# in Loop: Header=BB44_1 Depth=1
movsd %xmm0, -8(%rsp)
fldl -8(%rsp)
#APP
fsincos
#NO_APP
fstpl -24(%rsp)
fstpl -16(%rsp)
movsd -24(%rsp), %xmm1
mulsd %xmm1, %xmm1
movsd -16(%rsp), %xmm0
mulsd %xmm0, %xmm0
addsd %xmm1, %xmm0
.LBB44_1: # %loop2
# =>This Inner Loop Header: Depth=1
incl %eax
cmpl $99999999, %eax # imm = 0x5F5E0FF
jle .LBB44_2
# BB#3: # %break3
cvttsd2si %xmm0, %eax
ret
.Ltmp160:
.size main, .Ltmp160-main
.cfi_endproc
2 ответа
Аппаратная синхронизация идет медленно.
Слишком много документов утверждают, что инструкции x87, такие как fsin
или же fsincos
являются самым быстрым способом выполнения тригонометрических функций. Эти претензии часто ошибочны.
Самый быстрый способ зависит от вашего процессора. Поскольку процессоры становятся быстрее, старые инструкции по запуску оборудования, такие как fsin
не успевал. В некоторых процессорах программная функция, использующая полиномиальное приближение для синуса или другой функции триггера, теперь быстрее, чем аппаратная инструкция.
Короче, fsincos
слишком медленно
Аппаратный триг устарел.
Существует достаточно доказательств того, что платформа x86-64 отошла от аппаратного триггера.
- amd64 предпочитает SSE вместо x87 для поплавков. Тем не менее, SSE не имеет эквивалентов для команд x87, таких как
fsin
, - Для amd64 libm как во FreeBSD, так и в glibc реализует sin () и такие функции в программном обеспечении, а не с триггером x87. glibc оптимизировал сборку x86-64 для sinf () (синус одинарной точности) с полиномиальным приближением, а не с x87
fsin
, NetBSD и OpenBSD сделали противоположный выбор: их libm для amd64 использует инструкции x87. - Steel Bank Common Lisp использует
fsin
в бэкэнде x86, но не в бэкенде x86-64. Для x86-64 SBCL компилирует код, который вызывает sin () в libm.
Аппаратный триггер проигрывает гонку.
Я рассчитал аппаратное и программное обеспечение на AMD Phenom II X2 560 (3,3 ГГц) с 2010 года. Я написал программу на C с таким циклом:
volatile double a, s;
/* ... */
for (i = 0; i < 100000000; i++)
s = sin(a);
Я скомпилировал эту программу дважды, с двумя различными реализациями sin (). Жесткий грех () использует x87 fsin
, Мягкий грех () использует полиномиальное приближение. Мой компилятор C, gcc -O2
, не заменил мой вызов sin () на встроенный fsin
,
Вот результаты для греха (0.5):
$ time race-hard 0.5
0m3.40s real 0m3.40s user 0m0.00s system
$ time race-soft 0.5
0m1.13s real 0m1.15s user 0m0.00s system
Здесь soft sin(0.5) настолько быстр, что этот процессор будет делать soft sin(0.5) и soft cos(0.5) быстрее, чем один x87 fsin
,
И за грех (123):
$ time race-hard 123
0m3.61s real 0m3.62s user 0m0.00s system
$ time race-soft 123
0m3.08s real 0m3.07s user 0m0.01s system
Мягкий грех (123) медленнее мягкого греха (0,5), потому что 123 слишком много для полинома, поэтому функция должна вычесть несколько кратных 2π. Если я тоже хочу cos(123), есть вероятность, что x87 fsincos
будет быстрее, чем soft sin(123) и soft cos(123), для этого процессора с 2010 года.
fsincos
— это инструкция FPU x87, которая работает с числами с плавающей запятой 80-битной точности. Он не поддерживает автовекторизацию, но обеспечивает гораздо более высокую точность, чем 64-битные инструкции.
sin
иcos
оперируйте инструкциями с 64-битной точностью, поэтому даже более низкая точность сделает их быстрее. Код, который выполняется на FPU (80-битный тип), никогда не будет автоматически векторизован, поскольку он не поддерживается, а обычный 64-битный код (доdouble
type) будет, так что это может сделать его в несколько раз быстрее с SSE/AVX/NEON и т. д.
FPU следует использовать только тогда, когда вам действительно нужна 80-битная точность. Сказать, что оно устарело, не совсем верно. Он устарел только в 99% случаев и все еще нужен в 1% случаев.
Чтобы увидетьfsin
иfcos
генерируется с помощью компилятораlong double
тип (80-битный с плавающей запятой) сsinl
cosl
функции.