Оптимизация сравнения this с нулевым указателем в gcc 6.1

5270b5ceaf61460caa64ab25232fa8bd.jpg

Хорошие новости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)

© Habrahabr.ru