在仅标头库中使用 '[[GNU::Noinline]]'

using `[[gnu::noinline]]` in header-only library

提问人:陈浩南 提问时间:5/20/2022 更新时间:9/18/2022 访问量:608

问:

应声明仅标头库中的函数,以防止在不同的翻译单元中使用多个定义。也就是说,例如,我写了一个仅标头的库:inlinemylib.hpp

void do_something(int) {}

我在两个不同的 cpp 文件中使用了这个标头:

// a.cpp
# include "mylib.hpp"
void func1() {do_something(1);}
// b.cpp
# include "mylib.hpp"
void func2() {do_something(2);}

构建它们时,GCC 会抱怨“多重定义”。为了防止这种情况,定义函数,使其在每个翻译单元中都有一个副本(即,在最后一个输出中有两个副本),或者将函数定义为在最后一个输出中只有一个副本,这就是我们想要的。g++ -o main a.cpp b.cppdo_something(int)static void do_something(int) {}inline void do_something(int) {}

但是,如果我想强制不被编译器内联(例如,我想使用回溯库来动态找出哪个函数调用了),我应该这样写:do_somethingdo_something

[[gnu::noinline]] inline void do_something(int) {}

然而,海湾合作委员会抱怨说:

Warning: inline declaration of ‘do_something(int)’ follows declaration with attribute ‘noinline’ [-Wattributes]

那么,做这些事情的正确方法是什么呢?

C++ 标头

评论

1赞 ixSci 5/21/2022
正确的方法是将函数定义设为 cpp 文件。但是,既然你的方式不合适,为什么不直接忽略(禁用)警告呢?
0赞 陈浩南 5/21/2022
@ixSci但如果是函数模板,可能不方便放到cpp文件中。do_something
1赞 ixSci 5/21/2022
如果它是一个函数模板,你一开始就不会有这个关键词。inline
0赞 KamilCuk 5/21/2022
请发布重现问题所需的完整代码。请从编译器中发布确切的完整逐字错误消息。 没有然后吗?代码中有两个声明,这是错误的原因。Warning: inline declaration offile:line: first declared here ---->file:line: second time declared here --->do_something()

答:

1赞 Goswin von Brederlow 5/21/2022 #1

关键字并不意味着代码必须内联,或者甚至会内联。 表示此函数可能有多个(相同)版本,链接器最多会选择一个代表。inlineinline

如果编译器认为函数值得,则可以内联函数,并且可以使其更有可能内联。但为什么不让编译器来决定呢?它不会无缘无故地内联函数。inline

但是,如果你一定要试过吗?函数不会导出到翻译单元的外部。每个翻译单元都可以具有相同函数的静态版本,而不会发生冲突。但请注意,这将是同一函数的多个副本,每个副本都有一个唯一的地址。staticstatic

-1赞 KamilCuk 5/21/2022 #2

如果我想强制编译器不内联do_something

没有便携式方法可以做到这一点。

inline有一个令人困惑的名字,这与内联无关。你只需要:

[[gnu::noinline]] static void do_something(int) {}

我想使用回溯库来动态找出哪个函数调用了do_something

有功能属性 https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#Common-Function-Attributesartificial

做这些事情的正确方法是什么?

在标头中定义函数的正确方法是使用关键字。static

如果你的意思是不内联一个函数,你就不会 - 编译器优化代码,函数被内联。如果希望功能可用于调试,请将编译器配置为不内联代码(如果要强制执行)。-fno-inline

评论

1赞 Human-Compiler 5/21/2022
正如问题中提到的,将每个翻译单元中的符号标记为具有内部链接,从而导致符号重复。虽然这可行,但这实际上并不能解决函数是否很大或多次使用的问题。static
0赞 KamilCuk 5/21/2022
我不明白。 什么问题没有解决?this doesn't actually solve the problem
0赞 Pharap 9/18/2022 #3

消除警告的“正确”方法是将代码拆分为头 () 和源 () 文件。.h.cpp

// mylib.h
[[gnu::noinline]]
void do_something(int);
// mylib.cpp
void do_something(int)
{
  // Implementation
}
// a.cpp
#include "mylib.h"

void func1()
{
  do_something(1);
}
// b.cpp
#include "mylib.h"

void func2()
{
  do_something(2);
}

坦率地说,“仅标头”库是一个被高估的属性。 将代码拆分为头文件和源文件可以缩短编译时间,因为函数体只被分析和编译一次,而不是被多次分析和检查。(如果在标头中定义函数,编译器可能需要多次检查该函数,以检查该函数在转换单元之间是否未更改。

如果“仅标头”对您来说真的那么重要,那么其他选项是:

  • 用。这将使警告静音,但存在生成同一函数的多个副本的风险 - 每个翻译单元一个副本。链接时间优化器可能能够消除重复项,但假设情况确实如此可能不是一个好主意。static
  • 将函数设为模板会使函数隐式化,同时仍遵循 ,但最终会得到一个虚拟模板参数,并强制人们在调用该函数时指定该参数。inline[[gnu::noinline]]
  • 您可以将该函数设为类成员,这样就可以隐式地使用它,而不会破坏属性,并且只需要用户 () 进行一些额外的键入。staticinlineClass::do_something(1)

另一种可行的方法,但确实很糟糕,应该避免:

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wattributes"
[[gnu::noinline]]
inline void do_something(int)
{
  // Implementation
}
#pragma GCC diagnostic pop

但这是一个非常棘手的解决方案。当然,只有当 GCC 是编译器时,它才会起作用,并且会破坏许多其他编译器。(Clang 也许可以处理它,但 VC++ 会窒息它。


当然,您实际上可能不需要强制编译器来避免内联函数。通常,编译器会自行做出正确的决定。

就我个人而言,我唯一需要使用的时间是当函数使用内联程序集时,当编译器内联它时中断时,以及在测量函数调用的开销时。(后者可能是该属性的预期用途。[[gnu::noinline]]