Это встроено , но не оптимизировано, потому что вы скомпилировали с -O0
(по умолчанию) . Это генерирует asm для согласованной отладки, позволяя изменить любую переменную C ++, находясь в точке останова на любой строке.
Это означает, что компилятор выдает все из регистров после каждого оператора и перезагружает то, что ему нужно для следующего. Поэтому больше операторов для выражения одной и той же логики = более медленный код, независимо от того, находятся ли они в одной функции или нет.
Почему clang производит неэффективный asm для этой простой суммы с плавающей запятой (с -O0)? объясняет более подробно.
Обычно -O0
не встроенные функции, но он уважает __attribute__((always_inline))
.
Справка по оптимизации цикла C для окончательного назначения объясняет, почему бенчмаркинг или настройка с помощью -O0
абсолютно бессмысленны. Обе версии являются нелепым мусором для производительности.
Если бы оно не было встроенным, в цикле была бы инструкция call
.
Asm фактически создает указатели в регистрах для const WrappedDouble& left
и right
. (очень неэффективно, используя несколько инструкций вместо одной lea
. addq %rdx, %rax
- последний шаг в одной из них.)
Затем эти аргументы-указатели перенаправляются в стековую память, поскольку они являются реальными переменными и должны находиться в памяти, где отладчик может их модифицировать. Вот что делают movq %rax, -16(%rbp)
и %rdx
...
После перезагрузки и разыменования этих указателей результат addsd
(добавление скалярного двойного) сам возвращается в локальную память стека с помощью movsd %xmm0, -8(%rbp)
. Это не именованная переменная, это возвращаемое значение функции.
Затем он перезагружается и снова копируется в другое место в стеке, затем, наконец, arr
и i
загружаются из стека вместе с результатом double
operator+
, который сохраняется в arr[i]
с помощью movq %rsi, (%rax,%rdx,8)
. (Да, LLVM использовал 64-разрядное целое число mov
, чтобы скопировать double
в то время. Ранее использовался SSE2 movsd
.)
Все эти копии возвращаемого значения находятся на критическом пути для цепочки зависимостей, переносимых циклом, потому что следующая итерация читает arr[i-1]
. Эти ~ 5 или 6 циклических задержек пересылки хранилища действительно сложение против 3 или 4 цикла FP add
задержка.
Очевидно, что массово неэффективно. С включенной оптимизацией, gcc и clang без проблем вставляют и оптимизируют вашу оболочку.
Они также оптимизируют, сохраняя результат arr[i]
в регистре для использования в качестве результата arr[i-1]
на следующей итерации. Это позволяет избежать задержки передачи ~ 6 циклов, которая в противном случае была бы внутри цикла, если бы она делала asm как источник.
т.е. оптимизированный ассм выглядит примерно так: C ++:
double tmp = arr[0]; // kept in XMM0
for(...) {
tmp += arr[i]; // no re-read of mmeory
arr[i] = tmp;
}
Забавно, но clang не потрудился инициализировать tmp
(xmm0
) перед циклом, потому что вы не удосужились инициализировать массив . Странно это не предупреждает о UB. На практике большая malloc
с реализацией glibc даст вам свежие страницы из ОС, и все они будут содержать нули, то есть 0.0
. Но Clang даст вам все, что осталось в XMM0! Если вы добавите ((double*)arr)[0] = 1;
, clang загрузит первый элемент перед циклом.
К сожалению, компилятор не знает, как сделать что-то лучше, чем для расчета суммы префикса. Посмотрите параллельную префиксную (накопительную) сумму с SSE и SIMD префиксную сумму на процессоре Intel , чтобы узнать, как ускорить этот процесс еще на 2 раза и / или распараллелить его.
Я предпочитаю синтаксис Intel, но Проводник компилятора Godbolt может предоставить вам синтаксис AT & T, как в вашем вопросе, если хотите.
# gcc8.2 -O3 -march=haswell -Wall
.LC1:
.string "done"
main:
sub rsp, 8
mov edi, 800000000
call malloc # return value in RAX
vmovsd xmm0, QWORD PTR [rax] # load first elmeent
lea rdx, [rax+8] # p = &arr[1]
lea rcx, [rax+800000000] # endp = arr + len
.L2: # do {
vaddsd xmm0, xmm0, QWORD PTR [rdx] # tmp += *p
add rdx, 8 # p++
vmovsd QWORD PTR [rdx-8], xmm0 # p[-1] = tmp
cmp rdx, rcx
jne .L2 # }while(p != endp);
mov rdi, rax
call free
mov edi, OFFSET FLAT:.LC0
call puts
xor eax, eax
add rsp, 8
ret
Clang немного разворачивается, и, как я уже сказал, не удосуживается инициировать его tmp
.
# just the inner loop from clang -O3
# with -march=haswell it unrolls a lot more, so I left that out.
# hence the 2-operand SSE2 addsd instead of 3-operand AVX vaddsd
.LBB0_1: # do {
addsd xmm0, qword ptr [rax + 8*rcx - 16]
movsd qword ptr [rax + 8*rcx - 16], xmm0
addsd xmm0, qword ptr [rax + 8*rcx - 8]
movsd qword ptr [rax + 8*rcx - 8], xmm0
addsd xmm0, qword ptr [rax + 8*rcx]
movsd qword ptr [rax + 8*rcx], xmm0
add rcx, 3 # i += 3
cmp rcx, 100000002
jne .LBB0_1 } while(i!=100000002)
Apple XCode gcc
на самом деле является замаскированным LLVM в современных системах OS X.