Вы правильно понимаете влияние final
(за исключением, может быть, внутреннего цикла в случае 2), но ваши оценки стоимости далеко.Мы не должны ожидать большого эффекта в любом месте, потому что mt19937 просто медленный, и все 3 версии проводят в нем большую часть своего времени.
Единственное, что не потеряно / похоронено в шуме /издержки - это эффект встраивания int run_once() override final
во внутренний цикл в FooPlus::run_multiple
, который запускается как для случая 2, так и для случая 3.
Но случай 1 может 't встроенный Foo::run_once()
в Foo::run_multiple()
, поэтому во внутреннем цикле есть издержки вызова функции, в отличие от двух других случаев.
Случай 2 должен повторно вызывать run_multiple
, но это только один разна 100 прогонов run_once
и не имеет поддающегося измерению эффекта.
Для всех 3 случаев большинство времени тратится dist(rng);
, потому что std::mt19937
довольно медленнопо сравнению с дополнительными накладными расходами не встраивая вызов функции.Внеочередное выполнение, вероятно, также может скрывать много этих накладных расходов.Но не все, так что еще есть что измерить.
Case 3 может встроить все в этот цикл asm (из вашей ссылки быстрого доступа):
# percentages are *self* time, not including time spent in the PRNG
# These are from QuickBench's perf report tab,
# presumably sample for core clock cycle perf events.
# Take them with a grain of salt: superscalar + out-of-order exec
# makes it hard to blame one instruction for a clock cycle
VirtualWithFinalCase2(benchmark::State&): # case 3 from QuickBench link
... setup before the loop
.p2align 3
.Louter: # do{
xor %ebp,%ebp # sum = 0
mov $0x64,%ebx # inner = 100
.p2align 3 # nopw 0x0(%rax,%rax,1)
.Linner: # do {
51.82% mov %r13,%rdi
mov %r15,%rsi
mov %r13,%rdx # copy args from call-preserved regs
callq 404d60 # mt PRNG for unsigned long
47.27% add %eax,%ebp # sum += run_once()
add $0xffffffff,%ebx # --inner
jne .Linner # }while(inner);
mov %ebp,0x4(%rsp) # store to volatile local: benchmark::DoNotOptimize(x);
0.91% add $0xffffffffffffffff,%r12 # --outer
jne # } while(outer)
Case2 все еще может встроить run_once
в run_multiple
, потому что class FooPlus
использует int run_once() override final
.Во внешнем цикле есть только накладные расходы на виртуальную диспетчеризацию (только), но эта небольшая дополнительная плата за каждую итерацию внешнего цикла полностью затмевается стоимостью внутреннего цикла (идентичной для случая 2 и случая 3).
внутренний цикл будет по существу идентичен, с непрямыми вызовами косвенного вызова только во внешнем цикле.Неудивительно, что это не измеримо или, по крайней мере, потеряно в шуме в Quickbench.
Случай 1 не может встроить Foo::run_once()
в Foo::run_multiple()
, поэтому там тоже есть служебные вызовы функций.(Тот факт, что это косвенный вызов функции, относительно незначителен; в узком цикле предсказание ветвления будет выполнять почти идеальную работу.)
Варианты 1 и 2 имеют одинаковый asm для своего внешнего цикла,если вы посмотрите на разборку в вашей ссылке Quick-Bench.
Ни один из них не может девиртуализировать и встроить run_multiple
.Случай 1, потому что это виртуальный не финал, Случай 2, потому что это только базовый класс, а не производный класс с переопределением final
.
# case 2 and case 1 *outer* loops
.loop: # do {
mov (%r15),%rax # load vtable pointer
mov $0x64,%esi # first C++ arg
mov %r15,%rdi # this pointer = hidden first arg
callq *0x8(%rax) # memory-indirect call through a vtable entry
mov %eax,0x4(%rsp) # store the return value to a `volatile` local
add $0xffffffffffffffff,%rbx
jne 4049f0 .loop # } while(--i != 0);
Это, вероятно, пропущенная оптимизация: компиляторможет доказать, что Base *f
взято из new FooPlus()
, и, таким образом, статически известно, что он имеет тип FooPlus
.operator new
может быть переопределено, но компилятор по-прежнему генерирует отдельный вызов FooPlus::FooPlus()
(передавая ему указатель на хранилище из new
).Так что это, похоже, бросок кланга, который не использует преимущество в случае 2 и, возможно, также в случае 1.