我看到将我的类成员函数指定为内联实际上会增加执行时间,即使函数体非常小

I am seeing that specifying my class member function as inline actually increases the execution time, even though the function body is very small

本文关键字:执行时间 增加 实际上 非常 函数体 我的 成员 函数 我看      更新时间:2023-10-16

我写了一个简单的类,并为类定义之外的成员函数定义了函数体。函数体尺寸非常小(大约一行)。当我测试性能时,当我将函数定义指定为内联时,性能似乎在下降。

这是我的类和成员函数定义。

#include<iostream>
#include<sys/time.h>
class number {
protected:
long _val;
public:
number(long n): _val(n) {}
operator long() const;
number operator ++(int);
number& operator ++();
bool operator < (long n);
};
number :: operator long() const { return _val; }
number number :: operator ++(int) { return number(_val++); }
number& number :: operator ++() { _val ++; return *this; }
bool number :: operator < (long n) { return _val < n; } 
#define microsec(t) (t.tv_sec * 1000000 + t.tv_usec)
int main() {
struct timeval t1, t2;
gettimeofday(&t1, NULL);
for(number n = 0; n < 999999999L; ++n);
gettimeofday(&t2, NULL);
std::cout << (microsec(t2) - microsec(t1)) << std::endl;
}

当我运行上面的代码时,大约需要 3.3 秒才能完成。

当我在成员函数定义之前添加inline时,大约需要 4.6 秒。

我可以理解,如果函数体的大小很大,内联可能会影响性能。但是,就我而言,它们非常小。因此,它应该产生更好的性能或至少相同的性能。但是,执行时间会随着内联而增加。

有人可以帮助我理解这种行为吗?

[编辑1] 问题不完全在于优化。但是,要了解有关inline的更多信息.我知道由编译器决定是否遵循inline关键字并选择优化其认为合适的代码。 但是,我的问题是为什么它会对性能产生不利影响(没有明确的优化)。我能够始终如一地重现这种行为。

正如此线程中的一些人所建议的那样,我使用 https://gcc.godbolt.org 来查看编译器生成的 ASM 命令,无论是否使用inline。我看到为 main 生成的 ASM,因为两种情况是相同的。我看到的唯一区别是,使用inline关键字,ASM代码不是为未使用的方法生成的。但是,生成的有效代码对于两者是相同的,因此它不应该在运行时持续时间上产生任何差异。

只是,以防万一,这很重要,我正在使用gettimeofday(&_t2, NULL);来获取当前系统时间以找到时差。

我正在使用 g++ 编译器,带有 -std=c++11 标准选项。我没有使用优化标志,因为这不是我问题的重点。

[编辑 2] 修改了代码片段以包含重现问题的完整代码。

启用了优化的编译器将最了解何时内联函数。事实上,将函数声明为内联可能不会将其内联,不这样做可能会这样做。没有什么理由。
我不知道它是什么,但是内联这样的循环不应该影响执行时间 1.3 秒,因为它只会提高函数调用的速度,而不是函数内部的代码。

有关内联的更多信息,请参见此处。

性能测试应该(必须)始终通过完整的优化来完成,并且没有调试代码生成。

出于多种原因(除了要测量/比较调试代码的性能),在调试模式下进行性能比较是不可信的,因为编译器将生成额外的代码,以确保源代码中执行的代码的正确可跟踪性。两个例子来说明这一点:

1. 调试代码中没有内联

来自 cpp偏好 关于inline

inline关键字的初衷是作为优化器的指示,即函数的内联替换优先于函数调用,也就是说,不是执行函数调用 CPU 指令将控制权转移到函数体,而是执行函数体的副本而不生成调用。这避免了函数调用(传递参数和检索结果)产生的开销,但它可能会导致更大的可执行文件,因为函数的代码必须重复多次。

由于关键字inline的这种含义不具有约束力,因此编译器可以自由地对任何未标记为内联的函数使用内联替换,并且可以自由生成对任何标记为内联的函数的函数调用。这些优化选择不会更改上面列出的有关多个定义和共享静态数据的规则。

(我在VS2013中肯定知道这一点,并且也怀疑其他编译器也是如此。摆弄神电告诉我,g++似乎也表现得很好。

为了允许调试器命令进行单步执行和单步执行,似乎有必要抑制内联函数。否则,可能很难在源代码中为机器代码地址分配行号。

(几年前,我不小心调试了C++代码(用gcc编译),其中函数被内联(由于可能是错误的编译器设置)。调试器(gdb)在多个嵌套的内联函数调用中跳转单步命令,在到达大多数"内部"函数的代码之前没有停止。这真的很痛苦

例:

#include <iostream>
int add(int a, int b) { return a + b; }
inline int sub(int a, int b) { return a - b; }
int main()
{
int a, b; std::cin >> a >> b;
std::cout << add(a, b);
std::cout << sub(a, b);
return 0;
}

摘自生成的代码g++ -std=c++17 -g...:

; 10: std::cout << add(a, b);
mov     edx, DWORD PTR [rbp-8]
mov     eax, DWORD PTR [rbp-4]
mov     esi, edx
mov     edi, eax
call    add(int, int)
mov     esi, eax
mov     edi, OFFSET FLAT:_ZSt4cout
call    std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
; 11: std::cout << sub(a, b);
mov     edx, DWORD PTR [rbp-8]
mov     eax, DWORD PTR [rbp-4]
mov     esi, edx
mov     edi, eax
call    sub(int, int)
mov     esi, eax
mov     edi, OFFSET FLAT:_ZSt4cout
call    std::basic_ostream<char, std::char_traits<char> >::operator<<(int)

即使对ASM知之甚少,call add(int, int)call sub(int, int)也很容易识别。

摘自生成的代码g++ -std=c++17 -O2...:

; 10: std::cout << add(a, b);
mov     esi, DWORD PTR [rsp+8]
mov     edi, OFFSET FLAT:_ZSt4cout
add     esi, DWORD PTR [rsp+12]
call    std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
; 11: std::cout << sub(a, b);
mov     esi, DWORD PTR [rsp+8]
mov     edi, OFFSET FLAT:_ZSt4cout
sub     esi, DWORD PTR [rsp+12]
call    std::basic_ostream<char, std::char_traits<char> >::operator<<(int)

编译器不是调用函数add()sub(),而是内联两者。命令是add esi, DWORD PTR [rsp+12]sub esi, DWORD PTR [rsp+12]

关于神螺栓的完整示例

2.assert()

使用assert()来丰富代码是很常见的(也在 std 库类和函数中),这将有助于在调试模式下找到实现错误,但在发布模式下被排除以获得额外的性能。

可能的实现(从 cppreference.com):

#ifdef NDEBUG
#define assert(condition) ((void)0)
#else
#define assert(condition) /*implementation defined*/
#endif

因此,当代码在调试模式下编译时,性能度量也将测量所有这些assert表达式。通常,这被认为是扭曲的结果。

例:

#include <cassert>
#include <iostream>
int main()
{
int n; std::cin >> n;
assert(n > 0); // (stupid idea to assert user input)
std::cout << n;
return 0;
}

摘自生成的代码g++ -std=c++17 -D_DEBUG...:

; 6: int n; std::cin >> n;
lea     rax, [rbp-4]
mov     rsi, rax
mov     edi, OFFSET FLAT:_ZSt3cin
call    std::basic_istream<char, std::char_traits<char> >::operator>>(int&)
; 7: assert(n > 0); // (stupid idea to assert user input)
mov     eax, DWORD PTR [rbp-4]
test    eax, eax
jg      .L2
mov     ecx, OFFSET FLAT:main::__PRETTY_FUNCTION__
mov     edx, 7
mov     esi, OFFSET FLAT:.LC0
mov     edi, OFFSET FLAT:.LC1
call    __assert_fail
.L2:
; 8: std::cout << n;
mov     eax, DWORD PTR [rbp-4]
mov     esi, eax
mov     edi, OFFSET FLAT:_ZSt4cout
call    std::basic_ostream<char, std::char_traits<char> >::operator<<(int)

摘自生成的代码g++ -std=c++17 -DNDEBUG...:

; 6: int n; std::cin >> n;
lea     rax, [rbp-4]
mov     rsi, rax
mov     edi, OFFSET FLAT:_ZSt3cin
call    std::basic_istream<char, std::char_traits<char> >::operator>>(int&)
; 8: std::cout << n;
mov     eax, DWORD PTR [rbp-4]
mov     esi, eax
mov     edi, OFFSET FLAT:_ZSt4cout
call    std::basic_ostream<char, std::char_traits<char> >::operator<<(int)

关于神螺栓的完整示例


试图重现OP的主张...

免责声明:我不是基准测试方面的专业人士。如果你想得到另一个意见,这可能很有趣:CppCon 2015:钱德勒·卡鲁斯 "调优C++:基准测试、CPU 和编译器!天呐!

尽管如此,我还是看到了OP测试中的一些弱点。

  1. 这两种测试应该在相同的代码中进行(以便在接近可比较的条件下进行测试)。

  2. 在测试CPU速度之前,建议进行"预热"。

  3. 现代编译器进行数据流分析。可以肯定的是,相关代码没有被优化掉,副作用必须小心地放入。另一方面,这些副作用可能会削弱测量。

  4. 现代编译器很聪明,可以在编译时尽可能多地计算。为防止出现这种情况,应使用 I/O 初始化相关参数。

  5. 一般应重复测量以平均测量误差。(这是我几十年前在物理课上学到的东西,当时我还是个小学生。

考虑到这些事情,我稍微修改了 OP 的公开示例代码:

#include <iostream>
#include <iomanip>
#include <vector>
#include <sys/time.h>
class Number {
protected:
long _val;
public:
Number(long n): _val(n) { }
operator long() const;
Number operator ++(int);
Number& operator ++();
bool operator < (long n) const;
};
Number :: operator long() const { return _val; }
Number Number :: operator ++(int) { return Number(_val++); }
Number& Number :: operator ++() { ++_val; return *this; }
bool Number :: operator < (long n) const { return _val < n; }
class NumberInline {
protected:
long _val;
public:
NumberInline(long n): _val(n) { }
operator long() const { return _val; }
NumberInline operator ++(int) { return NumberInline(_val++); }
NumberInline& operator ++() { ++_val; return *this; }
bool operator < (long n) const { return _val < n; }
};
long microsec(timeval &t) { return t.tv_sec * 1000000L + t.tv_usec; }
int main()
{
timeval t1, t2, t3;
// heat-up of CPU
std::cout << "Heating up...n";
gettimeofday(&t1, nullptr);
Number n(0), nMax(0);
do {
++n; ++nMax;
gettimeofday(&t2, nullptr);
} while (microsec(t2) < microsec(t1) + 3000000L /* 3s */);
// do experiment
std::cout << "Starting experiment...n";
int nExp; if (!(std::cin >> nExp)) return 1;
long max; if (!(std::cin >> max)) return 1;
std::vector<std::pair<long, long>> t;
for (int i = 0; i < nExp; ++i) {
Number n = 0; NumberInline nI = 0;
gettimeofday(&t1, nullptr);
for (n = 0; n < max; ++n);
gettimeofday(&t2, nullptr);
for (nI = 0; nI < max; ++nI);
gettimeofday(&t3, nullptr);
std::cout << "n: " << n << ", nI: " << nI << 'n';
t.push_back(std::make_pair(
microsec(t2) - microsec(t1),
microsec(t3) - microsec(t2)));
std::cout << "t[" << i << "]: { "
<< std::setw(10) << t[i].first << "us, "
<< std::setw(10) << t[i].second << "us }n";
}
double tAvg0 = 0.0, tAvg1 = 0.0;
for (const std::pair<long, long> &tI : t) {
tAvg0 += tI.first; tAvg1 += tI.second;
}
tAvg0 /= nExp; tAvg1 /= nExp;
std::cout << "Average times: " << std::fixed
<< tAvg0 << "us, " << tAvg1 << "usn";
std::cout << "Ratio: " << tAvg0 / tAvg1 << "n";
return 0;
}

我在 Windows 64(64 位)上的cygwin10中编译并测试了它:

$ g++ --version
g++ (GCC) 7.3.0
$ echo "10 1000000000" | (g++ -std=c++11 -O0 testInline.cc -o testInline && ./testInline)
Heating up...
Starting experiment...
n: 1000000000, nI: 1000000000
t[0]: {    4811515us,    4579710us }
n: 1000000000, nI: 1000000000
t[1]: {    4703022us,    4649293us }
n: 1000000000, nI: 1000000000
t[2]: {    4725413us,    4724408us }
n: 1000000000, nI: 1000000000
t[3]: {    4777736us,    4744561us }
n: 1000000000, nI: 1000000000
t[4]: {    4807298us,    4831872us }
n: 1000000000, nI: 1000000000
t[5]: {    4853159us,    4616783us }
n: 1000000000, nI: 1000000000
t[6]: {    4818285us,    4769500us }
n: 1000000000, nI: 1000000000
t[7]: {    4753801us,    4693287us }
n: 1000000000, nI: 1000000000
t[8]: {    4781828us,    4439588us }
n: 1000000000, nI: 1000000000
t[9]: {    4125942us,    4090368us }
Average times: 4715799.900000us, 4613937.000000us
Ratio: 1.022077
$ echo "10 1000000000" | (g++ -std=c++11 -O1 testInline.cc -o testInline && ./testInline)
Heating up...
Starting experiment...
n: 1000000000, nI: 1000000000
t[0]: {     395756us,     381372us }
n: 1000000000, nI: 1000000000
t[1]: {     410973us,     395130us }
n: 1000000000, nI: 1000000000
t[2]: {     383708us,     376009us }
n: 1000000000, nI: 1000000000
t[3]: {     399632us,     373718us }
n: 1000000000, nI: 1000000000
t[4]: {     362056us,     398840us }
n: 1000000000, nI: 1000000000
t[5]: {     370812us,     397596us }
n: 1000000000, nI: 1000000000
t[6]: {     381679us,     392219us }
n: 1000000000, nI: 1000000000
t[7]: {     371318us,     396928us }
n: 1000000000, nI: 1000000000
t[8]: {     404398us,     433730us }
n: 1000000000, nI: 1000000000
t[9]: {     370402us,     356458us }
Average times: 385073.400000us, 390200.000000us
Ratio: 0.986862
$ echo "10 1000000000" | (g++ -std=c++11 -O2 testInline.cc -o testInline && ./testInline)
Heating up...
Starting experiment...
n: 1000000000, nI: 1000000000
t[0]: {          1us,          0us }
n: 1000000000, nI: 1000000000
t[1]: {          0us,          0us }
n: 1000000000, nI: 1000000000
t[2]: {          0us,          0us }
n: 1000000000, nI: 1000000000
t[3]: {          0us,          0us }
n: 1000000000, nI: 1000000000
t[4]: {          0us,          0us }
n: 1000000000, nI: 1000000000
t[5]: {          0us,          0us }
n: 1000000000, nI: 1000000000
t[6]: {          0us,          0us }
n: 1000000000, nI: 1000000000
t[7]: {          0us,          0us }
n: 1000000000, nI: 1000000000
t[8]: {          0us,          0us }
n: 1000000000, nI: 1000000000
t[9]: {          0us,          1us }
Average times: 0.100000us, 0.100000us
Ratio: 1.000000
$ 

关于最后一个测试(使用-O2),我严重怀疑 quest 中的代码是否在运行时执行。我将文件加载到 godbolt(相同的 g++ 版本,相同的选项)以获得线索。我在解释生成的代码时遇到了严重的问题,但这里是:编译器资源管理器中的示例

最后,前两个实验的比率(接近 1)告诉我

  1. 差异很可能仅仅是测量噪声。

  2. 我无法重现 OP 的语句,即内联代码的运行时间与非内联代码明显不同。

(除了评论和FalcoGer的回答中已经告诉的严重疑问之外。