LTO 优化全局变量

Vir*_*shi 7 c++ clang lto

我看到 LTO 优化了某个 TU 中的一些全局对象,如果该 TU 中没有明确来自另一个 TU 的函数。

以下摘录尝试描述所涉及的关键类和文件(请注意,这仅用于演示目的,可能并不完全准确):

我有一个单例类,它维护已构造的Registrar所有类型对象的列表。Foo为了避免静态构造顺序失败,我在构造第一个 Foo 类型的对象时动态构造该对象的实例。

// Registrar.hpp
class Registrar
{
public:
  static Registrar * sRegistrar;
  std::vector<Foo *> objectList;
  Registrar() = default;
};
Run Code Online (Sandbox Code Playgroud)

接下来,我们上课了Foo。此类的实例Registrar如上所述进行注册。

// Foo.hpp
class Foo
{
public:
  Foo()
  {
    if (Registrar::sRegistrar == nullptr)
      Registrar::sRegistrar = new Registrar();

    Registrar::sRegistrar->objectList.push_back(this);
  }
};
Run Code Online (Sandbox Code Playgroud)

的实例Foo是可以从多个文件创建的全局变量。在一个这样的文件中,我们碰巧定义了另一个从其他地方调用的函数:

// file1.hpp
void someFunctionThatIsCalledExplicitly()
{
  doSomething();
}

namespace 
{
  __attribute__((used, retain))
  Foo f1;
}
Run Code Online (Sandbox Code Playgroud)

但在另一个文件中,我们只是创建了一个实例Foo

// file2.hpp
namespace 
{
  __attribute__((used, retain))
  Foo f2;
}
Run Code Online (Sandbox Code Playgroud)

我看到的是f2正在优化,而f1没有,尽管添加了__attribute__((used, retain))class 的所有声明Foo

我应该如何防止 LTO 优化这些实例?为什么属性没有区别?

编辑:我能够编写一个小示例来重现所述问题。

  1. 主要.cpp:
#include <iostream>
#include "Registrar.hpp"

#ifdef FORCE_LINKAGE
extern int i;
#endif

extern void someFunctionThatIsCalledExplicitly();

int main()
{
    #ifdef FORCE_LINKAGE
    i++;
    #endif

    someFunctionThatIsCalledExplicitly();

    if (Registrar::sRegistrar == nullptr)
    {
        std::cout << "No instances of foo";
    }
    else
    {
        std::cout << Registrar::sRegistrar->objectList.size() << " instances of foo\n";
    }

    return 0;
}
Run Code Online (Sandbox Code Playgroud)
  1. Foo.hpp
#pragma once

class Foo
{
public:
    Foo();
};
Run Code Online (Sandbox Code Playgroud)
  1. Foo.cpp:
#include "Foo.hpp"
#include "Registrar.hpp"

Foo::Foo()
{
    if (Registrar::sRegistrar == nullptr)
    {
        Registrar::sRegistrar = new Registrar();
    }

    Registrar::sRegistrar->objectList.push_back(this);
}
Run Code Online (Sandbox Code Playgroud)
  1. 注册商.hpp:
#pragma once

#include <vector>
#include "Foo.hpp"

class Registrar
{
public:
    static Registrar * sRegistrar;
    std::vector<Foo *> objectList;

    Registrar() = default;
};
Run Code Online (Sandbox Code Playgroud)
  1. 注册器.cpp:
#include "Registrar.hpp"

Registrar * Registrar::sRegistrar = nullptr;
Run Code Online (Sandbox Code Playgroud)
  1. 文件1.cpp:
#include <iostream>
#include "Foo.hpp"

void someFunctionThatIsCalledExplicitly()
{
    std::cout << "someFunctionThatIsCalledExplicitly() called\n";
}

namespace
{
    __attribute__((used, retain))
    Foo f1;
}
Run Code Online (Sandbox Code Playgroud)
  1. 文件2.cpp:
#include "Foo.hpp"

#ifdef FORCE_LINKAGE
int i = 0;
#endif

namespace
{
  __attribute__((used, retain))
  Foo f2;
}
Run Code Online (Sandbox Code Playgroud)
  1. 生成文件:
CC          = clang++
LIBTOOL     = libtool
BUILDDIR    = build
BINFILE     = lto

BUILDFLAGS  = -flto -std=c++17
LINKFLAGS   = -flto

.PHONY:     all
all:        $(BUILDDIR) $(BINFILE)

.PHONY:     force
force:      def all

.PHONY:     def
def:
    $(eval BUILDFLAGS += -DFORCE_LINKAGE)

$(BINFILE): foo files
    $(CC) -o $(BUILDDIR)/$@ $(LINKFLAGS) -L$(BUILDDIR) $(addprefix -l, $^)

foo:        Foo.o main.o Registrar.o
    $(LIBTOOL) $(STATIC) -o $(BUILDDIR)/lib$@.a $(addprefix $(BUILDDIR)/, $^)

files:  File1.o File2.o
    $(LIBTOOL) $(STATIC) -o $(BUILDDIR)/lib$@.a $(addprefix $(BUILDDIR)/, $^)

%.o:        %.cpp
    $(CC) $(BUILDFLAGS) -c -o $(addprefix $(BUILDDIR)/, $@) $<

.PHONY:     $(BUILDDIR)
$(BUILDDIR):
    mkdir -p $(BUILDDIR)

.PHONY:     clean
clean:
    rm -rf $(BUILDDIR)
Run Code Online (Sandbox Code Playgroud)

我有两种变体,一种与上面类似(我只看到 1 个实例),另一种是通过声明我在其他地方引用的全局变量来强制链接(在这里我看到两个实例):

$ make
$ ./build/lto
someFunctionThatIsCalledExplicitly() called
1 instances of foo

$ make force
$ ./build/lto
someFunctionThatIsCalledExplicitly() called
2 instances of foo
Run Code Online (Sandbox Code Playgroud)

ala*_*ner 7

好吧,我做了一些挖掘,事实上你链接的 .a 库才是罪魁祸首,而不是 LTO,也不是任何其他优化。

顺便说一句,这已经在 SO 上提出了,请参阅:Static初始化和静态库全局变量的销毁不会发生在g ++中

当链接 .o 文件时(就像我在 godbolt 上所做的那样),一切都会进入并且可以正常工作。

对于 .a 文件,仅链接引用的代码,其余部分则不链接。创建虚拟变量是一种解决方法,但正确的方法是将其传递--whole-archive给链接器。

由于 libtool 问题,我无法运行基于 makefile 的示例,但请查看我的 CMake 配置:

cmake_minimum_required(VERSION 3.18)
project(LINK)


set(CMAKE_CXX_STANDARD 17)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")

add_library(Files File1.cpp File2.cpp)


target_include_directories(Files
                           INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
                           )
target_compile_definitions(Files PUBLIC ${FORCE})

add_executable(test Foo.cpp main.cpp Registrar.cpp)
# note the line below
target_link_libraries(test -Wl,--whole-archive Files -Wl,--no-whole-archive)
target_compile_definitions(test PUBLIC ${FORCE})
Run Code Online (Sandbox Code Playgroud)

链接时,它将按以下方式调用命令:

g++ -o test -Wl, --whole-archive -l:libFiles.a -Wl, --no-whole-archive Foo.o Registrar.o main.o