Оптимизировал ли компилятор виртуальный вызов?

У меня есть этот С++, который создает два производных объекта, а затем много раз вызывает вызов виртуальной функции:

Parent* d;
Child1 d1[1];
Child2 d2[1];

if(__rdtsc() & 1 != 0){
    d = d1;
}
else{
    d = d2;
}

for(unsigned long long i =0; i<9000000000; ++i){
    sum += d->process2();
}

и он производит этот ассемблер:

        for(unsigned long long i =0; i<9000000000; ++i){
000000013F4241A5  mov         qword ptr [rsp+100h],0  
000000013F4241B1  jmp         main+2B6h (013F4241C6h)  
000000013F4241B3  mov         rax,qword ptr [rsp+100h]  
000000013F4241BB  inc         rax  
000000013F4241BE  mov         qword ptr [rsp+100h],rax  
000000013F4241C6  mov         rax,218711A00h  
000000013F4241D0  cmp         qword ptr [rsp+100h],rax  
000000013F4241D8  jae         main+306h (013F424216h)  
            sum += d->process2();
000000013F4241DA  mov         rax,qword ptr [rsp+0F8h]  
000000013F4241E2  mov         rax,qword ptr [rax]  
000000013F4241E5  mov         rcx,qword ptr [rsp+0F8h]  
000000013F4241ED  call        qword ptr [rax+8]  
000000013F4241F0  mov         qword ptr [rsp+1D8h],rax  
000000013F4241F8  mov         rax,qword ptr [rsp+1D8h]  
000000013F424200  mov         rcx,qword ptr [sum (013F4385D8h)]  
000000013F424207  add         rcx,rax  
000000013F42420A  mov         rax,rcx  
000000013F42420D  mov         qword ptr [sum (013F4385D8h)],rax  
        }

Основываясь на ассемблере, может ли кто-нибудь подтвердить, что компилятор не может оптимизировать виртуальный вызов в цикле (даже если каждая итерация вызывает один и тот же производный объект), потому что компилятор не может знать, был ли выбран d1 или d2 из-за вызова только __rdtsc() быть разрешимым во время выполнения?

(Если бы кто-нибудь мог дать мне совет, как читать ассемблер для вызова d->process2(), я был бы очень признателен)


person user997112    schedule 07.02.2014    source источник
comment
Этот код вообще не выглядит оптимизированным? Вы включили оптимизацию?   -  person Mysticial    schedule 07.02.2014
comment
Только что включил /03, полная оптимизация программы и повышение скорости кода - результат тот же.   -  person user997112    schedule 07.02.2014


Ответы (3)


000000013F4241DA  mov     rax,qword ptr [rsp+0F8h] //load "this" into rax
000000013F4241E2  mov     rax,qword ptr [rax]      //load vtable pointer
000000013F4241E5  mov     rcx,qword ptr [rsp+0F8h] //load "this" into rcx
000000013F4241ED  call    qword ptr [rax+8]        //call second entry in vtable?

Очевидно, что вызов виртуальной функции не оптимизирован. Вы правы, это из-за случайного фактора.

person 4pie0    schedule 07.02.2014
comment
Как этот ответ объясняет отсутствие оптимизации? - person Alexis Wilke; 07.02.2014

Компилятор не сможет встроить виртуальный вызов, потому что да, он не сможет знать, какой объект d1 или d2 будет использоваться, поэтому допускает два возможных встроенных результата. Кроме того, в качестве виртуального вызова могут возникнуть дополнительные накладные расходы на поиск в vtable.

Моя рекомендация, если вы хотите попытаться оптимизировать себя, заключалась бы в том, чтобы вместо этого написать что-то похожее на

if(__rdtsc() & 1 != 0){
    for(unsigned long long i =0; i<9000000000; ++i){
        sum += d1[0].process2();
    }
}
else{
    for(unsigned long long i =0; i<9000000000; ++i){
        sum += d2[0].process2();
    }
}

хотя это по-прежнему может быть невозможно оптимизировать, если process2 является виртуальным вызовом, и всегда есть вероятность, что встраивание не произойдет.

В общем, виртуальные вызовы всегда добавляют накладные расходы, и если тактовые циклы важны, лучше их избегать. Вы можете изучить статический полиморфизм, который теряет некоторую гибкость, но может переносить затраты со времени выполнения на время компиляции.

Редактировать в ответ на user997112 ниже: Статический полиморфизм не работает точно для ситуации, описанной выше, но может быть использован для немного упрощения моего примера, но поместив цикл for в функцию:

void iterate_a_bunch( Parent<Child> &f )
{
    for(unsigned long long i =0; i<9000000000; ++i){
      f.process2();
    }
}

Эта функция будет компилироваться дважды, один раз для Child1 и один раз для Child2, что приведет к увеличению размера кода, но потенциально увеличит время выполнения.

person dvntehn00bz    schedule 07.02.2014
comment
Очевидно, что приведенный выше пример не сможет работать со статическим полиморфизмом, потому что __rdtsc() является функцией времени выполнения? Я предполагаю статический поли. работает только тогда, когда условное выражение может быть разрешено во время компиляции? - person user997112; 07.02.2014
comment
При использовании производных экземпляров поиск в виртуальной таблице не выполняется. - person Eric Fortin; 07.02.2014
comment
@ Эрик, можешь уточнить? Ты только что заставил меня усомниться в собственном понимании?! - person user997112; 07.02.2014
comment
d1 и d2 объявлены как дочерние экземпляры (массив, но в любом случае не ссылка или указатель). В этом случае нет динамической отправки. Если ни один класс не наследуется от Child, даже при использовании указателя поиск в виртуальной таблице будет пропущен. - person Eric Fortin; 07.02.2014
comment
Эрик, я считаю, что использование виртуальной таблицы может различаться в зависимости от компилятора. Скорее всего, нет, но это может зависеть от реализации объекта и компилятора stackoverflow.com/questions/7167320/когда-используется-vtable - person dvntehn00bz; 07.02.2014

Я провел эксперимент с g++, и с -O3 он выглядит оптимизированным. Однако я должен сказать, что оптимизация выглядит точно так же, как предлагает dvntehn00bz в своем ответе.

400860:       48 8b 45 00             mov    0x0(%rbp),%rax
400864:       48 89 ef                mov    %rbp,%rdi
400867:       ff 50 10                callq  *0x10(%rax)
40086a:       48 83 eb 01             sub    $0x1,%rbx
40086e:       75 f0                   jne    400860 <main+0x40>

Весь код состоит из двух циклов, полностью автоматически.

P.S. Конечно, ваши классы, вероятно, намного сложнее, чем моя тонкая версия, поэтому оптимизация, безусловно, намного проще для компилятора.

person Alexis Wilke    schedule 07.02.2014
comment
Как это отвечает на любой вопрос? Это только говорит, что вы сделали некоторые тесты, и вот результаты. Пожалуйста, не публикуйте что-то подобное, когда вы преследуете мой ответ из-за отсутствия смысла. Вопрос в том, оптимизировал ли компилятор виртуальный вызов? - person 4pie0; 07.02.2014
comment
Я говорю, что у dvntehn00bz есть правильный ответ, а у вас все еще нет. - person Alexis Wilke; 07.02.2014