一个优化器很可能会对真实的代码和这个虚假代码进行不同的处理,无论
foo()
和
bar()
做什么,在任何情况下都可能占主导地位。
正如你所说,“从理论角度来看”,问题在于
specialCase
是循环不变量,因此避免在该值上进行条件评估和分支将带来好处。然而,在实践中,编译器可能会发现它是循环不变量,并为您消除这个问题,因此每个解决方案之间的差异可能不在于循环不变量的评估。
确定最快的解决方案或是否存在足够显著的差异来证明更丑陋、更难以跟踪或维护的代码是否合理的唯一现实手段是对其进行分析;这项活动可能会占用您的生活时间,比任何解决方案节省的时间都要多——编译器优化器可能会产生更大的影响,而您的生产力可能会因不必担心这种微观优化而提高——这很可能是一种虚假的经济效益。
另一个要考虑的选择——给定一个成员函数的指针:
void (MyClass::*foobar)() ;
,则:
void ifInLoopD( bool specialCase, MyClass& acc )
{
acc.foobar = specialCase ? &MyClass::foo : &MyClass::bar ;
for( auto i = 0; i < n; ++i )
{
for( auto j = 0; j < n; ++j )
{
(acc.*acc.foobar)() ;
}
}
}
查看
C++调用成员函数指针,了解如何使用存储成员函数指针的本地变量。但请记住,此答案中的基准数据来自此版本,这可能会阻止一些编译器意识到函数指针在调用之间没有更改,因此可以进行内联。(直到编译器尝试内联指向的成员函数,它才会意识到函数不会更改类的指针成员。)
编辑注:版本D的基准数字可能不代表大多数循环体中的使用情况。
展示该成员函数指针具有类似于其他方法的性能的基准测试是基于一个瓶颈在递增
static volatile int
的延迟上的函数体。
在生成的asm中,这会创建一个包括存储转发延迟在内的循环承载依赖链。首先,这可以隐藏很多循环开销。在像x86这样的现代乱序执行CPU上,成本不仅仅是累加的。事情可以重叠:许多循环开销可以在那个延迟瓶颈的阴影下运行。
更糟糕的是,存储转发延迟不是恒定的,并且在存储和重新加载之间有更多开销,特别是不相关的存储时,它可以变得更快。请参阅
带有函数调用的循环比空循环快和
在没有优化编译的情况下添加冗余赋值会加速代码(其中调试构建将其循环计数器保留在内存中以创建此瓶颈)。即使在经过优化的构建中,使用
volatile
也会强制生成这样的asm。
在Intel Sandybridge系列中,使用volatile
递增可以随着更多的循环开销而变得更快。因此,如果您尝试推广到其他更典型的情况,则此循环体的选择会创建极具误导性的基准数据。正如我(Peter)在我的答案中所说,微基准测试很难。有关详细信息,请参见评论中的讨论。
此问题中的基准数字是针对此代码的,但您应该期望其他循环体在质量上有所不同。
请注意,此答案小心地不从任何角度得出可能在实际代码中更快的结论。
但我要补充的是,在内部循环中调用非内联函数几乎总是比内部循环中易于预测的分支更昂贵。非内联函数调用强制编译器更新所有仅在寄存器中临时存在的内存中的值,因此内存的状态与C++抽象机器相匹配。至少对于全局变量和静态变量以及通过函数参数指向/可访问的任何内容(包括成员函数的
this
),它还会破坏所有被调用的寄存器。
因此,就性能而言,我期望在循环外初始化的成员函数指针与选项A(内部的
if()
)类似,但几乎总是更差。如果它们都从常量传播中优化掉,则相等。
编辑者注:结束
对于每个实现A、B和我的实现D(我将其称为D,因为我无法想象您如何在实际实现中使用C),并给定:
class MyClass
{
public:
void foo(){ volatile static int a = 0 ; a++ ; }
void bar(){ volatile static int a = 0 ; a++ ; }
void (MyClass::*foobar)() ;
} acc ;
static const int n = 10000 ;
我得到了以下结果:
VC++ 2019 默认调试模式:(注意:不要计时调试模式,那几乎总是没有用的。)
Original Answer翻译成"最初的回答"
ifInLoopA( true, acc ) : 3.146 seconds
ifInLoopA( false, acc ) : 2.918 seconds
ifInLoopB( true, acc ) : 2.892 seconds
ifInLoopB( false, acc ) : 2.872 seconds
ifInLoopD( true, acc ) : 3.078 seconds
ifInLoopD( false, acc ) : 3.035 seconds
VC++ 2019 默认发布版本:
最初的回答
ifInLoopA( true, acc ) : 0.247 seconds
ifInLoopA( false, acc ) : 0.242 seconds
ifInLoopB( true, acc ) : 0.234 seconds
ifInLoopB( false, acc ) : 0.242 seconds
ifInLoopD( true, acc ) : 0.219 seconds
ifInLoopD( false, acc ) : 0.205 seconds
最初的回答:
你可以看到,在调试解决方案中,D明显变慢了,而在优化构建中,它明显变快了。此外,选择
specialCase
值对性能影响很小——尽管我不完全确定原因。
我增加了
n
到30000以获得更好的分辨率:
VC++ 2019 默认发布 n = 30000:
ifInLoopA( true, acc ) : 2.198 seconds
ifInLoopA( false, acc ) : 1.989 seconds
ifInLoopB( true, acc ) : 1.934 seconds
ifInLoopB( false, acc ) : 1.979 seconds
ifInLoopD( true, acc ) : 1.721 seconds
ifInLoopD( false, acc ) : 1.732 seconds
很明显,解决方案A对 specialCase
最为敏感,如果需要确定行为,则可能应避免使用该方案,但是在实际的 foo()
和 bar()
实现中存在差异,这种差异可能会被淹没。
您的结果可能因使用的编译器、目标和编译器选项而异,并且这些差异可能不太显著,以至于您无法为所有编译器得出任何结论。
例如,在 https://www.onlinegdb.com/上使用g ++ 5.4.1编译时,未优化和优化代码之间的差异远不那么显着(可能是由于VC ++调试器中远大于功能的重大开销),并且对于优化代码,解决方案之间的差异要少得多。
(编辑说明: MSVC调试模式包括函数调用中的间接寻址,以允许增量重新编译,这可以解释调试模式下额外开销的巨大量。这是不计时调试模式的另一个原因。)
volatile
增量限制了性能,使其与调试模式(将循环计数器保存在内存中)大致相同;两个单独的存储转发延迟链可以重叠。
https://www.onlinegdb.com/ C++14 默认选项,n = 30000
ifInLoopA( true, acc ) : 3.29026 seconds
ifInLoopA( false, acc ) : 3.08304 seconds
ifInLoopB( true, acc ) : 3.21342 seconds
ifInLoopB( false, acc ) : 3.26737 seconds
ifInLoopD( true, acc ) : 3.74404 seconds
ifInLoopD( false, acc ) : 3.72961 seconds
https://www.onlinegdb.com/ C++14默认 -O3优化,n=30000
最初的回答:
ifInLoopA( true, acc ) : 3.07913 seconds
ifInLoopA( false, acc ) : 3.09762 seconds
ifInLoopB( true, acc ) : 3.13735 seconds
ifInLoopB( false, acc ) : 3.05647 seconds
ifInLoopD( true, acc ) : 3.09078 seconds
ifInLoopD( false, acc ) : 3.04051 seconds
我认为你唯一能得出的结论就是,你必须测试每个解决方案,以确定它们在编译器、目标实现和你的真实代码(而不是虚构的循环体)中的表现如何。
如果所有的解决方案都满足你的性能要求,我建议你使用最易读/易于维护的解决方案,并且只有在性能成为问题时才考虑优化,这样你就能够准确地确定整个代码中哪部分会给你带来最大的影响,而又不需要付出太多的努力。
为了完整起见并让你自己进行评估,以下是我的测试代码:
class MyClass
{
public:
void foo(){ volatile static int a = 0 ; a++ ; }
void bar(){ volatile static int a = 0 ; a++ ; }
void (MyClass::*foobar)() ;
} acc ;
static const int n = 30000 ;
void ifInLoopA( bool specialCase, MyClass& acc ) {
for( auto i = 0; i < n; ++i ) {
for( auto j = 0; j < n; ++j ) {
if( specialCase ) {
acc.foo();
}
else {
acc.bar();
}
}
}
}
void ifInLoopB( bool specialCase, MyClass& acc ) {
if( specialCase ) {
for( auto i = 0; i < n; ++i ) {
for( auto j = 0; j < n; ++j ) {
acc.foo();
}
}
}
else {
for( auto i = 0; i < n; ++i ) {
for( auto j = 0; j < n; ++j ) {
acc.bar();
}
}
}
}
void ifInLoopD( bool specialCase, MyClass& acc )
{
acc.foobar = specialCase ? &MyClass::foo : &MyClass::bar ;
for( auto i = 0; i < n; ++i )
{
for( auto j = 0; j < n; ++j )
{
(acc.*acc.foobar)() ;
}
}
}
#include <ctime>
#include <iostream>
int main()
{
std::clock_t start = std::clock() ;
ifInLoopA( true, acc ) ;
std::cout << "ifInLoopA( true, acc ) : " << static_cast<double>((clock() - start)) / CLOCKS_PER_SEC << " seconds\n" ;
start = std::clock() ;
ifInLoopA( false, acc ) ;
std::cout << "ifInLoopA( false, acc ) : " << static_cast<double>((clock() - start)) / CLOCKS_PER_SEC << " seconds\n" ;
start = std::clock() ;
ifInLoopB( true, acc ) ;
std::cout << "ifInLoopB( true, acc ) : " << static_cast<double>((clock() - start)) / CLOCKS_PER_SEC << " seconds\n" ;
start = std::clock() ;
ifInLoopB( false, acc ) ;
std::cout << "ifInLoopB( false, acc ) : " << static_cast<double>((clock() - start)) / CLOCKS_PER_SEC << " seconds\n" ;
start = std::clock() ;
ifInLoopD( true, acc ) ;
std::cout << "ifInLoopD( true, acc ) : " << static_cast<double>((clock() - start)) / CLOCKS_PER_SEC << " seconds\n" ;
start = std::clock() ;
ifInLoopD( false, acc ) ;
std::cout << "ifInLoopD( false, acc ) : " << static_cast<double>((clock() - start)) / CLOCKS_PER_SEC << " seconds\n" ;
}
template<Fnc> apply(Fnc fnc)
中,并像apply([&](){acc.foo();})
或apply([&](){acc.bar();})
这样使用它,而无需重复编写代码来使用 B 选项。 - Quimby