Вопрос или проблема
Код 1:
#include <iostream>
struct Интерфейс
{
virtual void pr_fn() = 0;
virtual void pr_fn2() = 0;
virtual void pr_fn3() = 0;
};
struct Основание : Интерфейс
{
void pr_fn2() final
{
std::cout << "Основание\n";
}
};
struct Производный : Основание
{
void pr_fn() final
{
std::cout << "Производный2\n";
}
void pr_fn3() final
{
pr_fn2(); pr_fn();
}
};
int main()
{
Производный d;
d.pr_fn3();
return 0;
}
Код 2:
#include <iostream>
struct Интерфейс
{
virtual void pr_fn() = 0;
virtual void pr_fn2() = 0;
virtual void pr_fn3() = 0;
};
void Интерфейс::pr_fn3()
{
pr_fn2();
pr_fn();
}
struct Основание : Интерфейс
{
void pr_fn2() final
{
std::cout << "Основание\n";
}
};
struct Производный : Основание
{
void pr_fn() final
{
std::cout << "Производный\n";
}
void pr_fn3() final
{
Интерфейс::pr_fn3();
}
};
int main()
{
Производный d;
d.pr_fn3();
return 0;
}
Сборка кода 1: (Компилятор: x86-64 gcc 14.2, флаги: -O3)
.LC0:
.string "Основание\n"
.LC1:
.string "Производный2\n"
main:
sub rsp, 8
mov edx, 5
mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:std::cout
call std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
mov edx, 9
mov esi, OFFSET FLAT:.LC1
mov edi, OFFSET FLAT:std::cout
call std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
xor eax, eax
add rsp, 8
ret
Сборка кода 2: (Компилятор: x86-64 gcc 14.2, флаги: -O3)
.LC0:
.string "Основание\n"
Base::pr_fn2():
mov edx, 5
mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:std::cout
jmp std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
.LC1:
.string "Производный\n"
Derived::pr_fn():
mov edx, 8
mov esi, OFFSET FLAT:.LC1
mov edi, OFFSET FLAT:std::cout
jmp std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
Derived::pr_fn3():
sub rsp, 8
mov edx, 5
mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:std::cout
call std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
mov edx, 8
mov esi, OFFSET FLAT:.LC1
mov edi, OFFSET FLAT:std::cout
add rsp, 8
jmp std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
Interface::pr_fn3():
push rbx
mov rax, QWORD PTR [rdi]
mov rbx, rdi
mov rax, QWORD PTR [rax+8]
cmp rax, OFFSET FLAT:Base::pr_fn2()
jne .L7
mov edx, 5
mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:std::cout
call std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
mov rax, QWORD PTR [rbx]
mov rax, QWORD PTR [rax]
cmp rax, OFFSET FLAT:Derived::pr_fn()
jne .L9
.L11:
mov edx, 8
mov esi, OFFSET FLAT:.LC1
mov edi, OFFSET FLAT:std::cout
pop rbx
jmp std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
.L7:
call rax
mov rax, QWORD PTR [rbx]
mov rax, QWORD PTR [rax]
cmp rax, OFFSET FLAT:Derived::pr_fn()
je .L11
.L9:
mov rdi, rbx
pop rbx
jmp rax
main:
sub rsp, 8
mov edx, 5
mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:std::cout
call std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
mov edx, 8
mov esi, OFFSET FLAT:.LC1
mov edi, OFFSET FLAT:std::cout
call std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
xor eax, eax
add rsp, 8
ret
Наблюдения:
- Для
Base::pr_fn2()
,Derived::pr_fn()
иDerived::pr_fn3()
в Коде 1 не сгенерирована сборка. - Все вышеупомянутые функции встроены в сборку
main()
в Коде 1. - Сборка сгенерирована для
Interface::pr_fn3()
,Base::pr_fn2()
,Derived::pr_fn()
иDerived::pr_fn3()
в Коде 2. - Все вышеупомянутые функции встроены в сборку
main()
в Коде 2. - Вызовы к
Interface::pr_fn3()
,Base::pr_fn2()
иDerived::pr_fn()
встроены в сборкуDerived::pr_fn3()
в Коде 2.
Мой вопрос:
Когда программа выполняется и вызывается функция main в обоих случаях, выполняются одни и те же инструкции сборки. Почему компилятор генерирует сборку для других функций в Коде 2, но не в Коде 1?
Примечание: Пожалуйста, поправьте меня, если я где-то ошибаюсь в понимании.
Я провел эксперименты с некоторыми виртуальными методами и наблюдал сборку. Я ожидал не увидеть разницы в сборке с оптимизацией -O3
, но обнаружил, что генерируется несколько ненужных устройств. Я искал в Google, но безуспешно. Поэтому я задаю вопрос здесь.
Ответ или решение
Проблема, о которой идет речь, касается обработки чисто виртуальных методов и их вызовов в контексте классов на C++. Для анализа, давайте более подробно рассмотрим два представленных фрагмента кода и их соответствующий ассемблерный вывод.
Понимание кода
В первом коде (Code 1) классы реализуют взаимодействие с чисто виртуальным классом Interface
, где методы pr_fn
, pr_fn2
и pr_fn3
объявлены как виртуальные. Однако в этом случае вызов метода pr_fn3
происходит непосредственно в производном классе Derived
, и он вызывает pr_fn2
и pr_fn()
без промежуточного уровня.
Во втором коде (Code 2) метод pr_fn3
реализован в самом базовом классе Interface
, что предъявляет иную структуру вызовов. Метод pr_fn3
из Interface
сначала вызывает pr_fn2
и pr_fn()
, что приводит к более продолжительной цепочке вызовов.
Различия в ассемблерном выводе
-
Отсутствие ассемблера для функций в Code 1:
- В первом коде компилятор на этапе оптимизации
-O3
принял решение встраивать функцииpr_fn2
,pr_fn
, иpr_fn3
непосредственно в код функцииmain()
. Это может происходить из-за того, что компилятор определяет, что вызовы этих методов могут быть полностью разрешены на этапе компиляции. Директиваfinal
говорит о том, что метод не может быть переопределен в производных классах, что дает компилятору больше уверенности в возможности инлайн-оптимизации.
- В первом коде компилятор на этапе оптимизации
-
Генерация ассемблера для функций в Code 2:
- Во втором коде компилятор должен обработать виртуальную таблицу для методов, и хотя он также оптимизирует вызовы, он не может отказаться от явного определения функций, которые уже объявлены в
Interface
. Посколькуpr_fn3
реализован в интерфейсе и вызывает другие виртуальные методы, он не может быть инлайновым, поскольку это взаимодействие требует разрешения на уровне выполнения.
- Во втором коде компилятор должен обработать виртуальную таблицу для методов, и хотя он также оптимизирует вызовы, он не может отказаться от явного определения функций, которые уже объявлены в
Влияние стиля кодирования на производительность
При использовании чисто виртуальных функций и их взаимодействий на уровне базового класса имеет значение, где именно реализуются методы. В первом случае, избегая уровня абстракции через базовый класс, компилятор может оптимизировать и встраивать методы более эффективно. Во втором же случае, из-за наличия дополнительной абстракции и зависимости от виртуальных вызовов, компилятор вынужден соблюдать более сложные правила и, как следствие, генерировать дополнительный ассемблерный код для управления виртуальной таблицей.
В завершение, различия в сгенерированном ассемблерном коде обоих фрагментов исходного кода обусловлены оптимизациями, применяемыми компилятором в зависимости от структуры кода и иерархии классов. Следует учитывать, что даже при использовании одинаковых флагов компиляции результаты могут различаться от сценария к сценарию, отражая уникальные аспекты реализации и компиляции.