Оптимізація порівняння this з нульовим покажчиком у gcc 6.1



Хороші новиниTM чекають користувачів gcc при перехід на версію 6.1 Код такого виду (взято звідси):

class CWindow {
HWND handle;
public:
HWND GetSafeHandle() const
{
return this == 0 ? 0 : handle;
}
};

«зламається» — при виклику методу через нульовий покажчик на об'єкт тепер може відбуватися розіменування нульового покажчика, тому що компілятор тепер може просто взяти і видалити перевірку. Код, звичайно, з самого початку зламаний, а gcc 6.1 його тільки трохи доломает.

Перевіряти зручно на gcc.godbolt.org У налаштуваннях вкажемо -O2

Код для початку буде такий:

struct CWindow {
CWindow() : handle() {}
int handle;
int GetSafeHandle() const
{
return (this == 0) ? 1 : handle;
}
};
int main()
{
CWindow* wnd = 0;
return wnd->GetSafeHandle();
}

Вибираємо в списку gcc 6.1 і отримуємо…

main:
movl 0, %eax
ud2

ОХ ЩІ~ Спрацювала підстановка тіла функції за місцем виклику, gcc помітив розіменування нульового покажчика і додав виклик __builtin_trap(), який потім привів до появи в машинному коді «неприпустимою інструкції», яка в свою чергу при роботі програми повинна призводити до її аварійного завершення.

Для порівняння gcc 5.3 для того ж коду видає:

main:
movl $1, %eax
ret

gcc 5.З тут не помічає розіменування нульового покажчика і компілює «як написали».

Щоб, нарешті, отримати код з порівнянням покажчика, додамо на CWindow::GetSafeHandle() атрибут __attribute__ ((noinline)), щоб заборонити підстановку коду в місце виклику. Оголошення методу буде виглядати так:

int GetSafeHandle() const __attribute__ ((noinline))

Тепер gcc 5.3 видає такий машинний код:

CWindow::GetSafeHandle() const:
testq %rdi, %rdi
je .L1
movl (%rdi), %eax
ret
.L1:
movl $1, %eax
ret
main:
xorl %edi, %edi
jmp CWindow::GetSafeHandle() const

Тут на самому початку GetSafeHandle() виконується порівняння вказівника this з нулем (інструкція testq) і умовний перехід (інструкція je). Для порівняння gcc 6.1:

CWindow::GetSafeHandle() const:
movl (%rdi), %eax
ret
main:
xorl %edi, %edi
jmp CWindow::GetSafeHandle() const

Тут немає ніякого порівняння – відразу виконується розіменування. Це і є те саме відмінність, яка доломает код, «успішно працював» багато років і десятки років.

Окремої уваги заслуговує використання регістра rdi. Викликає обнуляє код edi – половину rdi або код ДОСИТЬ НЕСПОДІВАНО – використовує наполовину обнулений rdi. Звичайно, в цьому немає ніякого сенсу, але оскільки код спочатку містить невизначений поведінка (розіменування нульового покажчика), до компілятору ніяких претензій бути не може – компілює як вважатиме за потрібне, Стандартом не заборонено.

Нова поведінка задіюється за замовчуванням, починаючи з рівня оптимізації O1. Воно відключається параметром -fno-delete-null-pointer-checks – поведінка стає такою ж, як у gcc 5.3

Читачі, можливо, обурюються – знову компілятор «ламає» код і не видає попереджень! Видає, але не дуже. Попередження -Wnonnull-compare («завідомо ненульовий this порівнюється з нульовим покажчиком») вимкнено за замовчуванням, його можна включити, вказавши -Wall. У коді нижче воно видається в залежності від наявності додаткових пар дужок навколо порівняння:

int GetSafeHandle() const __attribute__ ((noinline))
{
if (this == 0) // -Wnonnull-compare
return 1;
if((this == 0))
return 1; // немає попередження
return this == 0 ? 1 : handle; // -Wnonnull-compare
return (this == 0) ? 1 : handle; // немає попередження
}

Такий вплив дужок — це явно помилка в gcc.

Крім того, якщо прибрати виклик GetSafeHandle() main(), попередження також більше не видається – компілятор знає, що одиниця трансляції одна і цей код свідомо не викликається, код функції видаляється раніше, ніж відпрацьовує пошук порівнянь this з нульовим покажчиком. Рішення, видавати попередження, приймається «занадто пізно» — в момент, коли код видалений.

Попередження -Wnonnull-compare не видається, якщо використовується параметр -fno-delete-null-pointer-checks

Тепер clang…

Починаючи з версії 3.5 з налаштуваннями за замовчуванням clang видає -Wtautological-undefined-compare на фрагменти:

if (this == 0);
(this == 0) ? 1 : handle;
if(this != 0);
(this != 0) ? handle : 1;

-Wundefined-bool-conversion на фрагменти:

if(this);
this ? handle : 1;
if(!this);
!this ? 1 : handle;

При цьому до версії 3.8 включно (3.8 — сама нова випущена версія на даний момент) порівняння не видаляється (крім випадку, коли код функції підставляється на місце виклику і оптимізується з навколишнім кодом).

Компілювати старий або безтурботно написаний новий код з невизначеним поведінкою стає все цікавіше і цікавіше.

Дмитро Мещеряков,
департамент продуктів для розробників

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.