[Assembler, C, C++] Девиртуализация в последних версиях gcc и clang

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
16-Июл-2020 03:30

Что это вообще такоеДевиртуализация (devirtualization) — оптимизация виртуальных функций. Если компилятор точно знает тип объекта, он может вызывать его виртуальные функции напрямую, не используя таблицу виртуальных функций.
В этой статье мы проверим насколько хорошо с этой задачей справляются компиляторы gcc и clang.
ТестированиеВсе тесты производились на Arch Linux x86-64. Использовались gcc 4.8.2 и clang 3.3.

вывод gcc -v

SPL
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

SPL
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Всё в одном файле.

код

SPL
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Всё в одном файле, вызываем виртуальные методы через указатель на базовый класс

код

SPL
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 файл

код

SPL
a.hpp
#pragma once
class 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, вызываем виртуальные методы через указатель на базовый класс

код

SPL
a.hpp
#pragma once
class 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
===========

Похожие новости: Теги для поиска: #_assembler, #_c, #_c++, #_assembler, #_c, #_c++, #_gcc, #_clang, #_virtualnye_funktsii (виртуальные функции), #_devirtualizatsija (девиртуализация), #_assembler, #_c, #_c++
Профиль  ЛС 
Показать сообщения:     

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

Текущее время: 22-Ноя 14:35
Часовой пояс: UTC + 5