Неопределенное поведение или ошибка оптимизации gcc - PullRequest
7 голосов
/ 18 июня 2019

Вопрос в том, ввели ли мы неопределенное поведение, которое отключает оптимизатор, или мы можем подать отчет об ошибке в gcc?

Извините за отсутствие лучшего заголовка, но оно довольно хрупкое, и мыпочти уверены, что это ошибка.Минимальный пример - не наш любимый дизайн, но он основан на сбойном коде:

#include <iostream>

struct Node
{
    Node(Node* parent) {
        if(parent) {
           parent->child_ = this;
        }
    }
    Node* child()
    {
        return child_;
    }
    Node* child_ = nullptr;
};

void walk(Node* module, int cleanup) {
    if(module != nullptr) {
        if(!cleanup) {
            std::cerr << "No cleanup";
        }
        walk(module->child(), cleanup);
        if(cleanup) {
            delete module;
        }
    }
}

int main (){
    Node* top = new Node(nullptr);
    Node* child = new Node(top);
    walk(top,1);
}

Скомпилировано с -O1 -foptimize-sibling-calls -ftree-vrp.Пример Godbolt: https://gcc.godbolt.org/z/4VijKb

Программа аварийно завершает работу, вызывая module->child(), когда модуль 0x0.Осматривая ассемблер, мы заметили, что if (module != nullptr) пропускается в начале walk.Существует проверка для cleanup, и вызов work кажется безусловным, что приводит к попытке извлечь child_ из недопустимого указателя.

Проверка восстанавливается в сборке (и кажется, что кодработа), если:

  1. Любая из двух оптимизаций по -O1 убрана.
  2. Тело if(!cleanup) удалено.(Никаких побочных эффектов от cerr)
  3. Тело if(cleanup) удалено.(Утечка памяти, но я думаю, что это считается наблюдаемым изменением поведения)
  4. walk вызывается до "Нет очистки" if.(Порядок работы) Тип
  5. cleanup изменен на bool с int.(Изменение типа - но, как мне кажется, не наблюдается заметного изменения поведения).
  6. Вставка безусловных cerr << "text"; перед и if(!cleanup).(Также наблюдаемое изменение.)

Кажется странной комбинацией хвостовой рекурсии и nullptr удаления проверки, которая привела к неправильному коду.Возможно, walk был разделен на функции-братья и сестры на основе cleanup проверок и неправильно прошит (?).

Два кандидата на UB были:

  1. Компоновщик хинтов, который moduleне-nullptr, но я не вижу, как компилятор мог бы вывести результат.
  2. Использование int в bool контексте, но это допустимо AFAIK.

FWIW clang, кажется, выдает правильное время выполнения, gcc 8.3 также имеет сборку для проверки.9.1 и trunk нет.У нас нет эксперта по gcc, поэтому мы не знали, почему оптимизатор может быть введен в заблуждение.

1 Ответ

2 голосов
/ 18 июня 2019

Это похоже на ошибку GCC.Некоторое время я смотрел на этот код, и я не могу найти в нем ничего плохого.

Это также можно воспроизвести с помощью gcc, а не только g++.Разработчикам GCC было бы легче разобраться, если вы напишите минимальную версию этого на C. Этот код C воспроизводит проблему для меня на GCC 9.1.0 с -O1 -foptimize-sibling-calls -ftree-vrp:

#include <stdio.h>
#include <stdlib.h>

struct Node
{
    struct Node* child;
};

void walk(struct Node* module, int cleanup)
{
    if (module == NULL) {
        return;
    }
    if (!cleanup) {
        puts("No cleanup");
    }
    walk(module->child, cleanup);
    if (cleanup) {
        free(module);
    }
}

int main()
{
    struct Node* node = malloc(sizeof(struct Node));
    node->child = NULL;
    walk(node, 1);
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...