Разрешено ли LTO удалять неиспользуемый глобальный объект, если в другой единице перевода есть код, полагающийся на побочные эффекты его построения?

Во-первых, просто чтобы избежать проблемы XY: эта проблема исходит от https://github.com/cnjinhao/nana/issues/445#issuecomment-502080177. Код библиотеки, вероятно, не должен делать такие вещи (полагаясь на создание неиспользуемого глобального объекта), но вопрос больше о том, является ли это допустимым поведением LTO, а не о проблемах с качеством кода.


Минимальный код, демонстрирующий ту же проблему (непроверенный, просто для уменьшения примера):

// main.cpp
#include <lib/font.hpp>

int main()
{
    lib::font f;
}
// lib/font.hpp
namespace lib
{
struct font
{
    font();

    int font_id;
};
}
// lib/font.cpp
#include <lib/font.hpp>
#include <lib/font_abstraction.hpp>

namespace lib
{
font::font()
{
    font_id = get_default_font_id();
}
}
// lib/font_abstraction.hpp
namespace lib
{
int get_default_font_id();

void initialize_font();
}
// lib/font_abstraction.cpp
#include <lib/font_abstraction.hpp>

namespace lib
{
static int* default_font_id;

int get_default_font_id()
{
    return *default_font_id;
}

void initialize_font()
{
    default_font_id = new int(1);
}
}
// lib/platform_abstraction.hpp
namespace lib
{
struct platform_abstraction
{
    platform_abstraction();
};
}
// lib/platform_abstraction.cpp
#include <lib/platform_abstraction.hpp>
#include <lib/font_abstraction.hpp>

namespace lib
{
platform_abstraction::platform_abstraction()
{
    initialize_font();
}

static platform_abstraction object;
}

Конструкция объекта font в main.cpp зависит от инициализации указателя. Единственное, что инициализирует указатель, — это глобальный объект object, но он не подлежит судебному преследованию — в случае связанной проблемы этот объект был удален LTO. Разрешена ли такая оптимизация? (См. проект C++ 6.6.5.1.2).

Некоторые примечания:

  • Библиотека была собрана как статическая библиотека и связана с основным файлом с использованием -flto -fno-fat-lto-objects и динамической стандартной библиотеки C++.
  • Этот пример можно собрать вообще без компиляции lib/platform_abstraction.cpp - в таком случае указатель точно не будет инициализирован.

person Xeverous    schedule 17.06.2019    source источник
comment
Разве это не просто другой симптом фиаско статического порядка инициализации ?   -  person Henri Menke    schedule 17.06.2019
comment
В минимальном примере не должно быть семи файлов.   -  person n. 1.8e9-where's-my-share m.    schedule 17.06.2019
comment
@н.м. Эта конкретная проблема связана с зависимостью кода между единицами перевода, поэтому я хотел создать такой же граф зависимостей, как и в библиотеке.   -  person Xeverous    schedule 17.06.2019
comment
Я тоже получаю segfault без lto. --whole-archive исправляет это, это проблема с подключением статической библиотеки. При сборке без статической библиотеки это тоже исправляет. Что касается языка-юриста: конструктор — это нормальная функция, ее нельзя оптимизировать, если у нее есть видимые побочные эффекты.   -  person KamilCuk    schedule 17.06.2019
comment
@HenriMenke Я так не думаю. SIOF — это когда один статический объект полагается на другой статический объект, который инициализируется первым. Здесь проблема не в порядке, а в том, будет ли объект вообще построен.   -  person Xeverous    schedule 17.06.2019


Ответы (3)


Ответ VTT дает ответ GCC, но вопрос помечен как language-lawyer.

Причина ISO C++ заключается в том, что объекты, определенные в трансляции, должны быть инициализированы до первого вызова функции, определенной в той же единице трансляции. Это означает, что platform_abstraction::object должен быть инициализирован до вызова platform_abstraction::platform_abstraction(). Как правильно понял компоновщик, других объектов platform_abstraction нет, поэтому platform_abstraction::platform_abstraction никогда не вызывается, поэтому инициализацию object можно отложить на неопределенное время. Соответствующая программа не может обнаружить это.

person MSalters    schedule 17.06.2019
comment
Таким образом, синглтон Мейера является лучшим подходом как для гарантии правильного порядка (без фиаско статического порядка инициализации), так и для гарантии того, что объект не будет удален компоновщиком, потому что он используется из функции в той же единице перевода. - person Xeverous; 18.06.2019
comment
Причина ISO C++ заключается в том, что объекты, определенные в переводе, должны быть инициализированы до первого вызова функции, определенной в той же единице перевода. Можете ли вы дать ссылку на стандартный проект для этого разрешения или это подразумевается другими правила? - person Xeverous; 18.06.2019
comment
eel.is/c++draft/basic.start.dynamic Это определяется реализацией, выполняется ли динамическая инициализация нелокальной не встроенной переменной со статической продолжительностью хранения до первого оператора main или откладывается. . GCC, по-видимому, откладывает это. - person MSalters; 18.06.2019

Поскольку вы никогда не ссылаетесь на object из статической библиотеки в основном исполняемом файле, она не будет существовать, если вы не свяжете эту статическую библиотеку с -Wl,--whole-archive. В любом случае не рекомендуется полагаться на создание некоторых глобальных объектов для выполнения инициализации. Таким образом, вы должны просто вызвать initialize_font явно, прежде чем использовать другие функции из этой библиотеки.

Дополнительное пояснение к вопросу с тегом language-lawyer:

static platform_abstraction object; не может быть устранено ни при каких обстоятельствах в соответствии с

6.6.4.1 Длительность статического хранения [basic.stc.static]
2 Если переменная со статической продолжительностью хранения имеет инициализацию или деструктор с побочными эффектами, она не должна удаляться, даже если кажется, что быть неиспользованным, за исключением того, что объект класса или его копия/перемещение могут быть удалены, как указано в 15.8.

и так, что здесь происходит? При компоновке статической библиотеки (архива объектных файлов) по умолчанию компоновщик будет выбирать только объектные файлы, необходимые для заполнения неопределенных символов, а так как материал из platform_abstraction.cpp больше нигде не используется, компоновщик полностью пропустит эту единицу трансляции. Параметр --whole-archive изменяет это поведение по умолчанию, заставляя компоновщик связывать все объектные файлы из статической библиотеки.

person user7860670    schedule 17.06.2019
comment
она не будет существовать, если вы не свяжете эту статическую библиотеку с -Wl,--whole-archive. То есть для подтверждения такое удаление LTO разрешено и флаг нужен для явного отключения этой оптимизации? - person Xeverous; 17.06.2019
comment
@Xeverous Он будет удален даже без LTO. Я думаю, что это общая проблема со статическими библиотеками, которые полагаются на создание некоторых глобальных переменных. - person user7860670; 17.06.2019
comment
В любом случае не рекомендуется полагаться на создание некоторых глобальных объектов для выполнения инициализации Не могли бы вы обобщить, почему или дать ссылку на объяснение? - person nada; 17.06.2019
comment
@nada Потому что это прямой путь к фиаско статического (не) порядка инициализации, трудностям тестирования и отладки. Честно говоря, я думаю, что введение этапа динамической инициализации было довольно неудачным решением. - person user7860670; 17.06.2019

Не используйте глобальные статические переменные.

Порядок инициализации не определен (в общем случае).

Поместите статические объекты внутри функций как статические объекты, тогда вы можете гарантировать, что они созданы перед использованием.

namespace lib
{
static int* default_font_id;

int get_default_font_id()
{
    return *default_font_id;
}

void initialize_font()
{
    default_font_id = new int(1);
}
}

// Измените и это:

namespace lib
{

int get_default_font_id()
{
     // This new is guaranteed to only ever be called once.
     static std::unique_ptr<int> default_font_id = new int(1);

     return *default_font_id;
}

void initialize_font()
{
    // Don't need this ever.
}
}
person Martin York    schedule 27.07.2019