Оптимизация сравнения 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 — самая новая выпущенная версия на данный момент) сравнение не удаляется (кроме случая, когда код функции подставляется в место вызова и оптимизируется с окружающим кодом).
Компилировать старый или беззаботно написанный новый код с неопределенным поведением становится все интереснее и интереснее.
Дмитрий Мещеряков,
департамент продуктов для разработчиков