LTO优化全局变量

7

如果某个TU中没有任何函数被另一个TU显式调用,LTO会优化掉其中的一些全局对象。

以下摘录试图描述相关的关键类和文件(请注意,此处仅为示例目的,有些地方可能不完全准确):

我有一个名为Registrar的单例类,它维护了所有类型为Foo的对象列表。为避免静态构造顺序错误,当第一个Foo对象被构造时,我动态地构造此对象的实例。

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

接下来,我们有 Foo 类。该类的实例如上所述向 Registrar 注册。
// Foo.hpp
class Foo
{
public:
  Foo()
  {
    if (Registrar::sRegistrar == nullptr)
      Registrar::sRegistrar = new Registrar();

    Registrar::sRegistrar->objectList.push_back(this);
  }
};
< p > Foo 的实例是全局的,可以从多个文件中创建。在其中一个文件中,我们定义了另一个函数,它会从其他地方调用:

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

namespace 
{
  __attribute__((used, retain))
  Foo f1;
}

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

// file2.hpp
namespace 
{
  __attribute__((used, retain))
  Foo f2;
}

我看到的是,尽管在所有声明Foo类时添加了__attribute__((used, retain))f1并没有被优化掉,而f2被优化掉了。
我该如何防止LTO优化掉这些实例? 为什么属性没有起作用?
编辑: 我能够编写一个小示例以重现此问题。
1. main.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;
}
  1. Foo.hpp
#pragma once

class Foo
{
public:
    Foo();
};
  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);
}
  1. Registrar.hpp:
#pragma once

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

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

    Registrar() = default;
};
  1. Registrar.cpp:
#include "Registrar.hpp"

Registrar * Registrar::sRegistrar = nullptr;
  1. File1.cpp:
#include <iostream>
#include "Foo.hpp"

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

namespace
{
    __attribute__((used, retain))
    Foo f1;
}
  1. File2.cpp:
#include "Foo.hpp"

#ifdef FORCE_LINKAGE
int i = 0;
#endif

namespace
{
  __attribute__((used, retain))
  Foo f2;
}
  1. Makefile:
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)

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

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

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

它们似乎没有被使用。 - Eljay
但是f2的构造函数应该运行并且具有副作用。因此,它不应该被省略。我认为任何允许省略构造的规则,如RVO,在这里都不适用。 - TrentP
1
顺便说一句,只是猜测(我可能完全错了),但你难道没有遇到静态初始化顺序混乱的问题吗? - alagner
@alagner 很抱歉,我无法展示实际的源代码。至于静态初始化顺序混乱问题,f1和f2创建的顺序并不重要。 - Virag Doshi
没有任何细节,这将非常难以解决。我已经尝试了您的代码,并且它对我有效https://godbolt.org/z/9aq8xY1ex - alagner
显示剩余6条评论
1个回答

7

好的,我进行了一些调查,事实证明你正在链接 .a 库是罪魁祸首,而不是 LTO 或其他任何优化。

这在 Stack Overflow 上已经被提出过了,见:Static initialization and destruction of a static library's globals not happening with 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})

在链接时,它将以类似以下方式调用命令: g++ -o test -Wl, --whole-archive -l:libFiles.a -Wl, --no-whole-archive Foo.o Registrar.o main.o

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接