Ответ 1
Перед голосованием по этому вопросу, пожалуйста, проверьте (и проверьте) это на своем компьютере и прокомментируйте/добавьте результаты. Обратите внимание, что для моих тестов я использовал векторный размер 1000 * 1000 * 1000. В настоящее время этот ответ содержит 19 upvotes, но только один опубликованный результат, и эти результаты не показали эффект, описанный ниже (хотя полученный с другим тестовым кодом, см. Комментарии).
Кажется, что ошибка/артефакт оптимизатора. Сравните время:
template<typename _ForwardIterator, typename _Compare>
_ForwardIterator
my_max_element_orig(_ForwardIterator __first, _ForwardIterator __last,
_Compare __comp)
{
if (__first == __last) return __first;
_ForwardIterator __result = __first;
while(++__first != __last)
if (__comp(__result, __first))
__result = __first;
return __result;
}
template<typename _ForwardIterator, typename _Compare>
_ForwardIterator
my_max_element_changed(_ForwardIterator __first, _ForwardIterator __last,
_Compare __comp)
{
if (__first == __last) return __first;
_ForwardIterator __result = __first;
++__first;
for(; __first != __last; ++__first)
if (__comp(__result, __first))
__result = __first;
return __result;
}
Первая - это оригинальная реализация libstdС++, вторая - преобразование без каких-либо изменений в поведении или требованиях. Clang++ производит очень похожие времена выполнения для этих двух функций, тогда как g++ 4.8.2 в четыре раза быстрее со второй версией.
Следуя предложению Максима, изменяя вектор от int
до int64_t
, измененная версия не 4, а только в 1,7 раза быстрее, чем исходная версия (g++ 4.8.2).
Разница заключается в прогностическом распространении *result
, то есть при хранении значения текущего элемента max, так что его не нужно перезагружать из памяти каждый раз. Это дает гораздо более чистый доступ к кэшу:
w/o commoning with commoning
* *
** *
** *
** *
* * *
* * *
* * *
Здесь asm для сравнения (rdi
/rsi
содержит первый и последний итераторы соответственно):
С циклом while (2.88743 мс; gist):
movq %rdi, %rax
jmp .L49
.L51:
movl (%rdi), %edx
cmpl %edx, (%rax)
cmovl %rdi, %rax
.L49:
addq $4, %rdi
cmpq %rsi, %rdi
jne .L51
С циклом for (1235,55 мкс):
leaq 4(%rdi), %rdx
movq %rdi, %rax
cmpq %rsi, %rdx
je .L53
movl (%rdi), %ecx
.L54:
movl (%rdx), %r8d
cmpl %r8d, %ecx
cmovl %rdx, %rax
cmovl %r8d, %ecx
addq $4, %rdx
cmpq %rdx, %rsi
jne .L54
.L53:
Если я принудительно обобщаю, явно сохраняя *result
в переменной prev
в начале и всякий раз, когда result
обновляется, и используя prev
вместо *result
в сравнении, я получаю еще более быстрый цикл (377,601 мкс): </p>
movl (%rdi), %ecx
movq %rdi, %rax
.L57:
addq $4, %rdi
cmpq %rsi, %rdi
je .L60
.L59:
movl (%rdi), %edx
cmpl %edx, %ecx
jge .L57
movq %rdi, %rax
addq $4, %rdi
movl %edx, %ecx
cmpq %rsi, %rdi
jne .L59
.L60:
Причина, по которой это быстрее, чем цикл for
, заключается в том, что условные перемещения (cmovl
) в приведенном выше примере являются пессимизацией, поскольку они выполняются так редко (Линус говорит, что cmov - это только хорошая идея, если ветвь непредсказуема). Обратите внимание, что для случайно распределенных данных ожидается, что ветвь будет считаться H n, что является незначительной долей (H n логарифмически растет, поэтому H n/n быстро приближается к 0). Код условного перемещения будет только лучше на патологические данные, например. [1, 0, 3, 2, 5, 4,...].