我看到将我的类成员函数指定为内联实际上会增加执行时间,即使函数体非常小
I am seeing that specifying my class member function as inline actually increases the execution time, even though the function body is very small
我写了一个简单的类,并为类定义之外的成员函数定义了函数体。函数体尺寸非常小(大约一行)。当我测试性能时,当我将函数定义指定为内联时,性能似乎在下降。
这是我的类和成员函数定义。
#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测试中的一些弱点。
这两种测试应该在相同的代码中进行(以便在接近可比较的条件下进行测试)。
在测试CPU速度之前,建议进行"预热"。
现代编译器进行数据流分析。可以肯定的是,相关代码没有被优化掉,副作用必须小心地放入。另一方面,这些副作用可能会削弱测量。
现代编译器很聪明,可以在编译时尽可能多地计算。为防止出现这种情况,应使用 I/O 初始化相关参数。
一般应重复测量以平均测量误差。(这是我几十年前在物理课上学到的东西,当时我还是个小学生。
考虑到这些事情,我稍微修改了 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)告诉我
差异很可能仅仅是测量噪声。
我无法重现 OP 的语句,即内联代码的运行时间与非内联代码明显不同。
(除了评论和FalcoGer的回答中已经告诉的严重疑问之外。
- 简单C++"Hello World"程序的执行时间长
- 我使用 OpenMP 的线程越多,执行时间就越长,这是怎么回事?
- 为什么切换 for 循环的顺序会显著改变执行时间?
- cmd.exe与Powershell中C++程序的不同执行时间
- pthread执行时间比顺序执行时间差
- OpenCV 函数 cv::remap() 的执行时间更长,当程序在两者之间进入睡眠状态时
- 为什么 std::chrono 在测量循环和编译器优化的并行 OpenMP 的执行时间时不起作用?
- 我需要帮助来缩短检索 SSL 证书的执行时间
- 如何测量cudaMalloc执行时间
- c++中的执行时间和检查流状态
- 为什么for循环中的异步不能提高执行时间
- 为什么 C++ openMP 程序执行时间更长
- 测量任何 Windows 可执行文件的内存使用情况和执行时间
- 需要减少我的C++代码的执行时间
- 如何在 ubuntu 上的 php 脚本中获取程序(c,c++,java,python,php)的执行时间和内存使用量?
- 在Qt中设置pixmap时的执行时间很奇怪
- 我看到将我的类成员函数指定为内联实际上会增加执行时间,即使函数体非常小
- 使用OpenMP库,执行时间如何取决于线程数量的增加
- 为什么要在我的OpenMP代码中增加执行时间
- 即使cpu数量增加,执行时间也会增加,为什么?