如果不同翻译单元中的代码依赖于其构造的副作用,是否允许 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?
首先,为了避免 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++ draft 6.6.5.1.2)
一些注意事项:
- 该库是作为静态库构建的,并使用
-flto -fno-fat-lto-objects
和动态 C++ 标准库与主文件链接。
- 此示例完全无需编译即可构建
lib/platform_abstraction.cpp
- 在这种情况下,指针肯定不会被初始化。
由于您从未在主可执行文件中从静态库中引用 object
,除非您 link 带有 -Wl,--whole-archive
的静态库,否则它不会存在。无论如何,依靠构造某些全局对象来执行初始化并不是一个好主意。因此,您应该在使用该库中的其他函数之前显式调用 initialize_font
。
对标记为 language-lawyer 的问题的补充说明:
static platform_abstraction object;
在任何情况下都不能根据
淘汰
6.6.4.1 Static storage duration [basic.stc.static]
2 If a variable with static storage duration has initialization or a destructor with side effects, it shall not be eliminated even if it appears to be unused, except that a class object or its copy/move may be eliminated as
specified in 15.8.
那么这是怎么回事?默认情况下 linking 静态库(目标文件的存档)时 linker 将只选择填充未定义符号所需的目标文件,因为来自 platform_abstraction.cpp
的内容不会在其他任何地方使用 linker 将完全省略此翻译单元。 --whole-archive
选项通过强制 linker link 静态库中的所有目标文件来改变此默认行为。
VTT 的答案给出了 GCC 答案,但问题被标记为 language-lawyer。
ISO C++ 的原因是翻译中定义的对象必须在第一次调用同一翻译单元中定义的函数之前进行初始化。这意味着必须在调用 platform_abstraction::platform_abstraction()
之前初始化 platform_abstraction::object
。正如链接器正确计算出的那样,没有其他 platform_abstraction
对象,因此永远不会调用 platform_abstraction::platform_abstraction
,因此 object
的初始化可以无限期推迟。合格的程序无法检测到这一点。
没有全局静态变量。
初始化顺序未定义(一般情况下)。
将静态对象作为静态对象放在函数中,然后您可以保证它们在使用前创建。
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.
}
}
首先,为了避免 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++ draft 6.6.5.1.2)
一些注意事项:
- 该库是作为静态库构建的,并使用
-flto -fno-fat-lto-objects
和动态 C++ 标准库与主文件链接。 - 此示例完全无需编译即可构建
lib/platform_abstraction.cpp
- 在这种情况下,指针肯定不会被初始化。
由于您从未在主可执行文件中从静态库中引用 object
,除非您 link 带有 -Wl,--whole-archive
的静态库,否则它不会存在。无论如何,依靠构造某些全局对象来执行初始化并不是一个好主意。因此,您应该在使用该库中的其他函数之前显式调用 initialize_font
。
对标记为 language-lawyer 的问题的补充说明:
static platform_abstraction object;
在任何情况下都不能根据
6.6.4.1 Static storage duration [basic.stc.static]
2 If a variable with static storage duration has initialization or a destructor with side effects, it shall not be eliminated even if it appears to be unused, except that a class object or its copy/move may be eliminated as specified in 15.8.
那么这是怎么回事?默认情况下 linking 静态库(目标文件的存档)时 linker 将只选择填充未定义符号所需的目标文件,因为来自 platform_abstraction.cpp
的内容不会在其他任何地方使用 linker 将完全省略此翻译单元。 --whole-archive
选项通过强制 linker link 静态库中的所有目标文件来改变此默认行为。
VTT 的答案给出了 GCC 答案,但问题被标记为 language-lawyer。
ISO C++ 的原因是翻译中定义的对象必须在第一次调用同一翻译单元中定义的函数之前进行初始化。这意味着必须在调用 platform_abstraction::platform_abstraction()
之前初始化 platform_abstraction::object
。正如链接器正确计算出的那样,没有其他 platform_abstraction
对象,因此永远不会调用 platform_abstraction::platform_abstraction
,因此 object
的初始化可以无限期推迟。合格的程序无法检测到这一点。
没有全局静态变量。
初始化顺序未定义(一般情况下)。
将静态对象作为静态对象放在函数中,然后您可以保证它们在使用前创建。
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.
}
}