Русский
Русский
English
Статистика
Реклама

Из песочницы Девиртуализация в последних версиях gcc и clang

Что это вообще такое

Девиртуализация (devirtualization) оптимизация виртуальных функций. Если компилятор точно знает тип объекта, он может вызывать его виртуальные функции напрямую, не используя таблицу виртуальных функций.
В этой статье мы проверим насколько хорошо с этой задачей справляются компиляторы gcc и clang.

Тестирование

Все тесты производились на Arch Linux x86-64. Использовались gcc 4.8.2 и clang 3.3.

вывод gcc -v
Using built-in specs.
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)

вывод clang -v
clang version 3.3 (tags/RELEASE_33/final)
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 файл
код
a.hpp
#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, вызываем виртуальные методы через указатель на базовый класс
код
a.hpp
#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
Источник: habr.com
К списку статей
Опубликовано: 16.07.2020 02:04:59
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Assembler

C

C++

Gcc

Clang

Виртуальные функции

Девиртуализация

Категории

Последние комментарии

© 2006-2020, personeltest.ru