【发布时间】:2010-09-13 01:29:32
【问题描述】:
我是一名 C/C++ 开发人员,这里有几个问题一直困扰着我。
- “常规”代码和内联代码有很大区别吗?
- 主要区别是什么?
- 内联代码只是宏的“形式”吗?
- 在选择内联代码时必须做什么样的权衡?
谢谢
【问题讨论】:
标签: c++ optimization inline-functions tradeoff
我是一名 C/C++ 开发人员,这里有几个问题一直困扰着我。
谢谢
【问题讨论】:
标签: c++ optimization inline-functions tradeoff
正如在之前的答案中所建议的,使用 inline 关键字可以通过内联函数调用使代码更快,通常以增加可执行文件为代价。 “内联函数调用”只是意味着在相应地填写参数后,用函数的实际代码替换对目标函数的调用。
然而,现代编译器非常擅长在设置为高度优化时自动内联函数调用无需用户提示。实际上,编译器通常更好在确定什么调用内联以获得速度增益。
为了提高性能而显式声明函数inline(几乎?)总是不必要的!
此外,编译器可以并将 忽略 inline 请求(如果它适合他们)。如果对函数的调用不可能内联(即使用非平凡递归或函数指针),而且如果函数太大而无法获得有意义的性能提升,编译器就会这样做。
但是,使用 inline 关键字 has other effects 声明内联函数,实际上可能需要满足单一定义规则 (ODR):C++ 标准中的这条规则规定给定的符号可以声明多次,但只能定义一次。如果链接编辑器(=链接器)遇到几个相同的符号定义,就会产生错误。
解决此问题的一种方法是通过声明 static 来提供内部链接来确保编译单元不会导出给定符号。
但是,通常最好将函数标记为inline。这告诉链接器将此函数的所有定义跨编译单元合并为一个定义,具有一个地址和共享的函数静态变量。
例如,考虑以下程序:
// header.hpp
#ifndef HEADER_HPP
#define HEADER_HPP
#include <cmath>
#include <numeric>
#include <vector>
using vec = std::vector<double>;
/*inline*/ double mean(vec const& sample) {
return std::accumulate(begin(sample), end(sample), 0.0) / sample.size();
}
#endif // !defined(HEADER_HPP)
// test.cpp
#include "header.hpp"
#include <iostream>
#include <iomanip>
void print_mean(vec const& sample) {
std::cout << "Sample with x̂ = " << mean(sample) << '\n';
}
// main.cpp
#include "header.hpp"
void print_mean(vec const&); // Forward declaration.
int main() {
vec x{4, 3, 5, 4, 5, 5, 6, 3, 8, 6, 8, 3, 1, 7};
print_mean(x);
}
请注意,两个.cpp 文件都包含头文件,因此也包含mean 的函数定义。尽管保存文件时使用了包含防止双重包含的保护措施,但这将导致同一函数的两个定义,尽管在不同的编译单元中。
现在,如果您尝试链接这两个编译单元 - 例如使用以下命令:
⟩⟩⟩ g++ -std=c++11 -pedantic main.cpp test.cpp
您将收到一条错误消息,提示“重复符号 __Z4meanRKNSt3__16vectorIdNS_9allocatorIdEEEE”(这是我们函数 mean 的 mangled name)。
但是,如果您取消注释函数定义前面的 inline 修饰符,代码将正确编译和链接。
函数模板是一种特殊情况:它们总是内联,无论它们是否以这种方式声明。这并不意味着编译器会内联对它们的调用,但它们不会违反 ODR。对于在类或结构中定义的成员函数也是如此。
【讨论】:
static 或 inline 更好的方法(不是我能想到的'不要限制你的函数可以使用的类型数量)......这两种情况不会导致更大的二进制文件吗?此外,如果编译器“选择忽略提示”因为您的模板函数是 25 行代码,那么使用 inline 关键字如何发挥作用?结果是否与static 说明符相同?为什么inline比static更好?
inline 请求时,这只是为了优化。编译器仍必须确保满足 ODR(如何做到这一点取决于编译器,并不意味着函数调用实际上是内联的;它只是意味着编译器会跟踪多个定义并合并它们)。最后,我的示例选择不当,因为函数模板始终是内联的。
using type aliases 完全没问题,您可能会将其与using namespace declarations 混淆,后者并不好。关于static 和inline 之间的区别:static 定义仅在编译单元内可见(内部链接),如果您有具有内部链接的重复函数,链接器将不合并这些.这意味着static,而不是inline,将创建重复代码。
static 函数中,函数静态变量将引用不同的实体,而在 inline 函数中,函数静态变量将引用相同的实体跨翻译单元的实体。请参阅此示例:gist.github.com/klmr/9e06284a1ca9152bee2bfc3b1df14003。一般来说,在 C++ 中使用 static 函数的理由很少。
- “常规”代码和内联代码有很大区别吗?
是和不是。不,因为内联函数或方法与常规函数或方法具有完全相同的特征,最重要的是它们都是类型安全的。是的,因为编译器生成的汇编代码会有所不同;对于常规函数,每次调用都将转换为几个步骤:将参数压入堆栈,跳转到函数,弹出参数等,而对内联函数的调用将被其实际代码替换,例如宏。
- 内联代码只是宏的“形式”吗?
不!宏是简单的文本替换,可能会导致严重错误。考虑以下代码:
#define unsafe(i) ( (i) >= 0 ? (i) : -(i) )
[...]
unsafe(x++); // x is incremented twice!
unsafe(f()); // f() is called twice!
[...]
使用内联函数,您可以确定参数会在函数实际执行之前进行评估。它们还将进行类型检查,并最终转换为与形式参数类型匹配。
- 在选择内联代码时必须做什么样的权衡?
通常,使用内联函数时程序执行应该更快,但二进制代码更大。更多信息,请阅读GoTW#33。
【讨论】:
inline int f() { ... },就优化而言,编译器会看到它等同于int f () { ... },然后运行自己的分析来确定是否内联。
内联代码本质上与宏类似,但它是实际的真实代码,可以进行优化。非常小的函数通常适合内联,因为与方法所做的少量实际工作相比,设置函数调用(将参数加载到适当的寄存器中)所需的工作成本很高。使用内联,无需设置函数调用,因为代码直接“粘贴”到任何使用它的方法中。
内联会增加代码大小,这是它的主要缺点。如果代码太大以至于无法放入 CPU 缓存中,则可能会严重降低速度。您只需要在极少数情况下担心这一点,因为您不太可能在很多地方使用方法,增加的代码会导致问题。
总之,内联非常适合加速被多次调用但不会在太多地方调用的小方法(不过,100 个地方仍然可以 - 您需要进入非常极端的示例才能获得任何显着的代码膨胀)。
编辑:正如其他人指出的那样,内联只是对编译器的建议。如果它认为你在发出愚蠢的请求,比如内联一个巨大的 25 行方法,它可以随意忽略你。
【讨论】:
是的 - 内联代码不涉及函数调用,并将寄存器变量保存到堆栈中。每次“调用”它都会使用程序空间。因此,总体而言,执行所需的时间更少,因为处理器中没有分支,也没有保存状态、清除缓存等。
宏和内联代码有相似之处。最大的区别是内联代码被专门格式化为一个函数,因此编译器和未来的维护者有更多的选择。具体来说,如果你告诉编译器优化代码空间,或者未来的维护者最终扩展它并在他们的代码中的许多地方使用它,它可以很容易地变成一个函数。
选择内联代码时必须做什么样的权衡?
需要注意的是,寄存器保存和跳转到函数确实会占用代码空间,所以对于非常小的函数,内联可以比函数占用更少的空间。 em>
-亚当
【讨论】:
inline 有时会比函数调用占用 很多 的空间。例如,给定inline double distsquared(double dx, double dy, double dz) { return sqrt(x*x+y*y+z*z); },代码d=distsquared(1.0, 2.0, z); 可以扩展为d=z*z+5;,这可能比推送三个double 参数然后调用函数的代码更紧凑。
这取决于编译器...
假设你有一个愚蠢的编译器。通过指示必须内联函数,它会在每次调用时放置函数内容的副本。
优点:没有函数调用开销(放参数、推送当前PC、跳转到函数等)。例如,在大循环的中心部分可能很重要。
不便之处:膨胀生成的二进制文件。
它是一个宏吗?不是真的,因为编译器仍然会检查参数的类型等。
智能编译器呢?如果他们“感觉”函数太复杂/太大,他们可以忽略 inline 指令。也许他们可以自动内联一些琐碎的函数,比如简单的 getter/setter。
【讨论】:
内联与宏的不同之处在于它是对编译器的提示(编译器可能决定不内联代码!)并且宏是在编译之前生成的源代码文本,因此被“强制”内联。
【讨论】:
将函数标记为内联意味着编译器具有选项以包含在调用它的“内联”中,如果编译器选择这样做的话;相比之下,宏将总是就地展开。内联函数将设置适当的调试符号,以允许符号调试器跟踪它的来源,而调试宏则令人困惑。内联函数必须是有效函数,而宏......好吧,不要。
决定将函数声明为内联在很大程度上是一种空间折衷——如果编译器决定内联它,您的程序将会更大(特别是如果它不是静态的,在这种情况下,至少需要一个非内联副本供任何外部对象使用);事实上,如果函数很大,这可能会导致性能下降,因为缓存中的代码较少。然而,一般性能提升只是您摆脱了函数调用本身的开销。对于作为内部循环的一部分调用的小函数,这是一个有意义的权衡。
如果您信任您的编译器,请随意标记内部循环中使用的小函数inline;编译器将负责在决定是否内联时做正确的事情。
【讨论】:
如果您在 f.e. 中将代码标记为内联C++ 你也告诉你的编译器代码应该内联执行,即。该代码块将“或多或少”插入到调用它的位置(从而消除堆栈上的推送、弹出和跳转)。所以,是的……如果函数适合这种行为,建议使用。
【讨论】:
“inline”类似于 2000 年的“register”。不用担心,编译器可以比您更好地决定要优化的内容。
【讨论】:
通过内联,编译器在调用点插入函数的实现。 您正在做的是消除函数调用开销。 但是,不能保证您的所有内联候选者实际上都会被编译器内联。但是,对于较小的函数,编译器总是内联的。 因此,如果您有一个被多次调用但只有有限数量的代码(几行代码)的函数,您可以从内联中受益,因为函数调用开销可能比函数本身的执行花费更长的时间。
一个很好的内联候选的典型例子是简单的具体类的getter。
CPoint
{
public:
inline int x() const { return m_x ; }
inline int y() const { return m_y ; }
private:
int m_x ;
int m_y ;
};
一些编译器(例如 VC2005)有一个积极内联的选项,使用该选项时您不需要指定 'inline' 关键字。
【讨论】:
我不会重复上面的内容,但值得注意的是,虚函数不会被内联,因为调用的函数是在运行时解析的。
【讨论】:
内联通常在优化级别 3 启用(在 GCC 的情况下为 -O3)。在某些情况下(如果可能),这可能会显着提高速度。
程序中的显式内联可以增加一些速度,但会增加代码大小。
您应该看看哪个是合适的:代码大小或速度,并决定是否应该将其包含在您的程序中。
您可以只打开第 3 级优化而忘记它,让编译器完成他的工作。
【讨论】:
是否应该内联的答案归结为速度。 如果你在一个紧密的循环中调用一个函数,而且它不是一个超级大的函数,而是一个在调用函数上浪费了很多时间的函数,那么让这个函数内联,你会得到很多好处你的钱。
【讨论】:
首先,内联是对编译器的内联函数的请求。所以编译器是否内联。
【讨论】:
内联是一种提高速度的技术。但是在你的情况下使用探查器来测试它。我发现 (MSVC) 内联并不总是可以交付,当然也不会以任何壮观的方式交付。运行时间有时会减少几个百分点,但在稍有不同的情况下会增加几个百分点。
如果代码运行缓慢,请使用分析器查找故障点并解决这些问题。
我已经停止在头文件中添加内联函数,它增加了耦合但回报很少。
【讨论】:
内联代码更快。无需执行函数调用(每个函数调用都需要一些时间)。缺点是您不能将指针传递给内联函数,因为该函数并不真正作为函数存在,因此没有指针。此外,该函数不能导出到公共(例如,库中的内联函数在链接到库的二进制文件中不可用)。另一个问题是,如果您从不同的地方调用函数,二进制文件中的代码部分将会增长(因为每次生成函数的副本而不是只有一个副本并总是跳到那里)
通常您不必手动决定是否内联函数。例如。 GCC 将根据优化级别 (-Ox) 和其他参数自动决定。它将考虑诸如“功能有多大?”之类的事情。 (指令数量),在代码中调用它的频率,通过内联二进制文件会变大多少,以及其他一些指标。例如。如果一个函数是静态的(因此无论如何都不会导出)并且只在您的代码中调用一次并且您从不使用指向该函数的指针,那么 GCC 很可能会决定自动内联它,因为它不会产生负面影响(二进制只内联一次不会变大)。
【讨论】: