如果 LTO 中的代码依赖于其构造的副作用,是否允许 LTO 删除未使用的全局对象?

Is LTO allowed to remove unused global object if there is code in a different translation unit relying on side effects of its construction?

本文关键字:LTO 删除 未使用 对象 是否 全局 副作用 依赖于 代码 如果      更新时间:2023-10-16

首先,为了避免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;
}

fontmain.cpp中对象的构造依赖于指针的初始化。唯一初始化指针的是全局对象object但它是未被起诉的 - 在链接问题的情况下,该对象已被 LTO 删除。是否允许这种优化?(见草案6.6.5.1.2 C++)

一些注意事项:

  • 该库被构建为静态库,并使用-flto -fno-fat-lto-objects和动态C++标准库与主文件链接。
  • 此示例可以在不编译lib/platform_abstraction.cpp的情况下构建 - 在这种情况下,指针肯定不会被初始化。

VTT的答案给出了GCC的答案,但这个问题被标记为语言律师。

ISO C++原因是,在首次调用同一翻译单元中定义的函数之前,必须初始化翻译中定义的对象。这意味着platform_abstraction::object必须在调用platform_abstraction::platform_abstraction()之前进行初始化。正如链接器正确发现的那样,没有其他platform_abstraction对象,因此永远不会调用platform_abstraction::platform_abstraction,因此object的初始化可以无限期推迟。符合要求的程序无法检测到这一点。

由于您从不在主可执行文件中引用静态库中的object,因此除非您将该静态库与-Wl,--whole-archive链接,否则它不会存在。无论如何,依靠某些全局对象的构造来执行初始化并不是一个好主意。因此,您应该在使用该库中的其他函数之前显式调用initialize_font

问题标记语言律师的附加说明:

static platform_abstraction object;在任何情况下都不能根据

6.6.4.1 静态存储持续时间 [basic.stc.static]
2 如果具有静态存储持续时间的变量具有初始化或析构函数具有副作用,则即使它看起来未使用,也不应消除它,除非类对象或其复制/移动可以作为 在 15.8 中指定。

这到底是怎么回事呢?当链接静态库(对象文件的存档)时,默认情况下链接器将只选择填充未定义符号所需的对象文件,并且由于platform_abstraction.cpp中的内容未在其他任何地方使用,链接器将完全省略此翻译单元。--whole-archive选项通过强制链接器链接静态库中的所有对象文件来更改此默认行为。

没有全局静态变量。

初始化顺序未定义(在一般情况下)。

将静态对象作为静态对象放入功能中,然后您可以保证它们在使用前创建。

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.
}
}