Что это вообще такое
Девиртуализация (devirtualization) оптимизация виртуальных функций. Если компилятор точно знает тип объекта, он может вызывать его виртуальные функции напрямую, не используя таблицу виртуальных функций.В этой статье мы проверим насколько хорошо с этой задачей справляются компиляторы gcc и clang.
Тестирование
Все тесты производились на Arch Linux x86-64. Использовались gcc 4.8.2 и clang 3.3.COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-unknown-linux-gnu/4.8.2/lto-wrapper
Target: x86_64-unknown-linux-gnu
Configured with: /build/gcc-multilib/src/gcc-4.8.2/configure --prefix=/usr --libdir=/usr/lib --libexecdir=/usr/lib --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=https://bugs.archlinux.org/ --enable-languages=c,c++,ada,fortran,go,lto,objc,obj-c++ --enable-shared --enable-threads=posix --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-clocale=gnu --disable-libstdcxx-pch --disable-libssp --enable-gnu-unique-object --enable-linker-build-id --enable-cloog-backend=isl --disable-cloog-version-check --enable-lto --enable-plugin --with-linker-hash-style=gnu --enable-multilib --disable-werror --enable-checking=release
Thread model: posix
gcc version 4.8.2 (GCC)
Target: x86_64-unknown-linux-gnu
Thread model: posix
Чтобы было проще разбираться в дизассемблированном коде, использовался флаг
-nostartfiles
. Если его указать, то
компилятор не будет генерировать код, вызывающий функцию
main
с нужными параметрами. Функция, которая получает
управление первой, называется _start
.В коде, который мы будем компилировать, содержится два класса:
- класс A абстрактный класс с трёмя методами: increment(),
decrement() и get()
class A {public:virtual ~A() {}virtual void increment() = 0;virtual void decrement() = 0;virtual int get() = 0;};
- класс B класс наследующийся от А и реализующий все абстрактные
методы
class B : public A {public:B() : x(0) {}virtual void increment() {x++;}virtual void decrement() {x--;}virtual int get() {return x;}private:int x;};
Версия 1
Всё в одном файле.
class A {public:virtual ~A() {}virtual void increment() = 0;virtual void decrement() = 0;virtual int get() = 0;};class B : public A {public:B() : x(0) {}virtual void increment() {x++;}virtual void decrement() {x--;}virtual int get() {return x;}private:int x;};extern "C" {int printf(const char * format, ...);void exit(int status);void _start() {B b;b.increment();b.increment();b.decrement();printf("%d\n", b.get());exit(0);}}
Результат: gcc с флагами -O1, -O2, -O3, -Os и clang с флагами -O2, -O3, -Os произвели девиртуализацию и поняли, что второй аргумент функции printf всегда равен 1. Код, сгенерированный с помощью gcc -O1:
<_start>: sub rsp,0x8 ; вызов printf mov esi,0x1 ; записываем значение b.get() в ESI mov edi,0x4003a2 ; записываем адрес строки "%s\n" в EDI mov eax,0x0 call 400360 <printf@plt> ; вызываем printf ; вызов exit mov edi,0x0 ; записываем код ошибки в регистр EDI call 400370 <exit@plt> ; вызываем exit
Версия 2
Всё в одном файле, вызываем виртуальные методы через указатель на базовый класс
class A {public:virtual ~A() {}virtual void increment() = 0;virtual void decrement() = 0;virtual int get() = 0;};class B : public A {public:B() : x(0) {}virtual void increment() {x++;}virtual void decrement() {x--;}virtual int get() {return x;}private:int x;};extern "C" {int printf(const char * format, ...);void exit(int status);void _start() {A * a = new B;a->increment();a->increment();a->decrement();printf("%d\n", a->get());exit(0);}}
Результат: clang с флагами -O2, -O3, -Os генерирует такой же код, что и в варианте 1. gcc ведёт себя странно: с флагами -O1, -O2, -O3, -Os он генерирует такой код:
<_start>: push rbx ; выделение памяти mov edi,0x10 ; кол-во байт (16) call 400560 <_Znwm@plt> ; вызываем функцию, выделяющую память (возвращает указатель в RAX) mov rbx,rax ; сохраняем указатель на экземпляр класса в RBX ; конструктор mov QWORD PTR [rax],0x4006d0 ; инициализируем таблицу виртуальных функций mov DWORD PTR [rax+0x8],0x1 ; инициализируем поле x единицей (первый вызов increment заинлайнился) ; второй вызов increment mov rdi,rax ; записываем указатель на экземпляр класса в RDI call 4005ca <_ZN1B9incrementEv> ; вызываем increment ; вызов decrement mov rax,QWORD PTR [rbx] ; записываем указатель на таблицу виртуальных функций в RAX mov rdi,rbx ; записываем указатель на экземпляр класса в RDI call QWORD PTR [rax+0x18] ; вызываем decrement через таблицу виртуальных функций ; вызов get mov rax,QWORD PTR [rbx] ; записываем указатель на таблицу виртуальных функций в RAX mov rdi,rbx ; записываем указатель на экземпляр класса в RDI call QWORD PTR [rax+0x20] ; вызываем get через таблицу виртуальных функций (результат в EAX) ; вызов printf mov esi,eax ; записываем значение b.get() в ESI mov edi,0x400620 ; записываем адрес строки "%s\n" в EDI mov eax,0x0 call 400520 <printf@plt> ; вызываем printf ; вызов exit mov edi,0x0 ; записываем код ошибки в регистр EDI call 400370 <exit@plt> ; вызываем exit
Версия 3
Для каждого класса отдельный .hpp и .cpp файл
#pragma onceclass A {public:virtual ~A();virtual void increment() = 0;virtual void decrement() = 0;virtual int get() = 0;};
a.cpp
#include "a.hpp"A::~A() {}
b.hpp
#pragma once#include "a.hpp"class B : public A {public:B();virtual void increment();virtual void decrement();virtual int get();private:int x;};
b.cpp
#include "b.hpp"B::B() : x(0) {}void B::increment() {x++;}void B::decrement() {x--;}int B::get() {return x;}
test.cpp
#include "b.hpp"extern "C" {int printf(const char * format, ...);void exit(int status);void _start() {B b;b.increment();b.increment();b.decrement();printf("%d\n", b.get());exit(0);}}
Результат: оба компилятора успешно девиртуализировали все функции, но не смогли их заинлайнить, так как они находятся в разных единицах трансляции:
<_start>: push rbx sub rsp,0x10 ; выделяем пямять на стеке ; вызов конструктора lea rbx,[rsp] ; сохраняем указатель на экземпляр класса в RBX mov rdi,rbx ; записываем указатель на экземпляр класса в RDI call 400720 <_ZN1BC1Ev> ; вызываем конструктор ; вызов increment mov rdi,rbx ; записываем указатель на экземпляр класса в RDI call 400740 <_ZN1B9incrementEv> ; вызываем increment ; вызов increment lea rdi,[rsp] ; записываем указатель на экземпляр класса в RDI call 400740 <_ZN1B9incrementEv> ; вызываем increment ; вызов decrement lea rdi,[rsp] ; записываем указатель на экземпляр класса в RDI call 400750 <_ZN1B9decrementEv> ; вызываем decrement ; вызов get lea rdi,[rsp] ; записываем указатель на экземпляр класса в RDI call 400760 <_ZN1B3getEv> ; вызываем get ; вызов printf mov edi,0x400820 ; записываем адрес строки "%s\n" в EDI mov esi,eax ; записываем значение b.get() в ESI xor al,al call 4005d0 <printf@plt> ; вызываем printf ; вызов exit xor edi,edi ; записываем код ошибки в регистр EDI call 4005e0 <exit@plt> ; вызываем exit
Версия 4
Для каждого класса отдельный .hpp и .cpp файл, LTO (Link Time Optimization, она же Interprocedural optimization, флаг-flto
)Код тот же, что и в предыдущем примере
Результат: clang девиртуализировал и заинлайнил все методы (ассемблерный код как в примере 1), gcc по какой-то причине заинлайнил всё кроме конструктора:
<_start>: push rbx sub rsp,0x10 ; выделяем пямять на стеке ; вызов конструктора mov rdi,rsp ; записываем указатель на экземпляр класса в регистр RDI call 400660 <_ZN1BC1Ev.2444> ; вызываем конструктор ; вычисление значения поля x mov eax,DWORD PTR [rsp+0x8] ; загружаем старое значение поля x (0) lea esi,[rax+0x1] ; увеличиваем его не 1 mov DWORD PTR [rsp+0x8],esi ; записываем результат ; вызов printf mov edi,0x400700 ; записываем адрес строки "%s\n" в EDI mov eax,0x0 ; записываем значение b.get() в ESI call 4005f0 <printf@plt> ; вызываем printf ; вызов exit mov edi,0x0 ; записываем код ошибки в регистр EDI call 400620 <exit@plt> ; вызываем exit
Версия 5
Для каждого класса отдельный .hpp и .cpp файл, LTO, вызываем виртуальные методы через указатель на базовый класс
#pragma onceclass A {public:virtual ~A();virtual void increment() = 0;virtual void decrement() = 0;virtual int get() = 0;};
a.cpp
#include "a.hpp"A::~A() {}
b.hpp
#pragma once#include "a.hpp"class B : public A {public:B();virtual void increment();virtual void decrement();virtual int get();private:int x;};
b.cpp
#include "b.hpp"B::B() : x(0) {}void B::increment() {x++;}void B::decrement() {x--;}int B::get() {return x;}
test.cpp
#include "b.hpp"extern "C" {int printf(const char * format, ...);void exit(int status);void _start() {A * a = new B;a->increment();a->increment();a->decrement();printf("%d\n", a->get());exit(0);}}
Результат: и gcc, и clang смогли девиртуализировать только первый вызов increment:
<_start>: push rbx ; выделение памяти mov edi,0x10 ; кол-во байт (16) call 400480 <_Znwm@plt> ; вызываем функцию, выделяющую память (возвращает указатель в RAX) mov rbx,rax ; сохраняем указатель на экземпляр класса в RBX ; конструктор mov QWORD PTR [rbx],0x4005b0 ; инициализируем таблицу виртуальных функций mov DWORD PTR [rbx+0x8],0x0 ; инициализируем поле x ; первый вызов increment mov rdi,rbx ; записываем указатель на экземпляр класса в RDI call 400520 <_ZN1B9incrementEv> ; вызываем increment ; второй вызов increment mov rax,QWORD PTR [rbx] ; записываем указатель на таблицу виртуальных функций в RAX mov rdi,rbx ; записываем указатель на экземпляр класса в RDI call QWORD PTR [rax+0x10] ; вызываем increment ; вызов decrement mov rax,QWORD PTR [rbx] ; записываем указатель на таблицу виртуальных функций в RAX mov rdi,rbx ; записываем указатель на экземпляр класса в RDI call QWORD PTR [rax+0x18] ; вызываем decrement ; вызов get mov rax,QWORD PTR [rbx] ; записываем указатель на таблицу виртуальных функций в RAX mov rdi,rbx ; записываем указатель на экземпляр класса в RDI call QWORD PTR [rax+0x20] ; вызываем get ; вызов printf mov edi,0x400570 ; записываем адрес строки "%s\n" в EDI mov esi,eax ; записываем значение b.get() в ESI xor al,al call 400490 <printf@plt> ; вызываем printf ; вызов exit xor edi,edi ; записываем код ошибки в регистр EDI pop rbx jmp 4004a0 <exit@plt> ; вызываем exit
Выводы
- Наилучший результат достигается когда все классы в одной единице трансляции
- Во всех тестах результаты clang не хуже или лучше результатов gcc
Исходники: github.com/alkedr/devirtualize-test