优化器可能会以不同于此假代码的方式处理任何真实代码,foo()
并且bar()
在任何情况下都可能占主导地位。
正如您所说,“从理论的角度来看”specialCase
,问题在于循环不变,因此避免条件评估和对该值的分支将带来好处。然而,在实践中,编译器可能会发现它是循环不变的,并为您消除该问题,因此每个解决方案之间的差异可能不取决于循环不变的评估。
确定最快解决方案或差异是否足以证明更丑陋、更难遵循或维护代码的唯一现实方法是对其进行分析;一项活动可能会比任何一种解决方案都节省更多时间——编译器优化器可能会产生更大的影响,并且你的生产力可能会因为不担心这种微优化而提高——这很可能是一种虚假的经济。
另一个要考虑的选项 - 给定一个指向成员函数成员的指针:void (MyClass::*foobar)() ;
然后:
void ifInLoopD( bool specialCase, MyClass& acc )
{
// FIXME: use a local, not class member, for the pointer-to-member-function
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 的基准数字可能并不代表大多数循环体使用它。
显示这个指向成员函数的指针与其他方法具有相似性能的基准测试是基于一个函数体,该函数体在增加 a 的延迟上存在瓶颈static volatile int
。
在生成的 asm 中,这会创建一个循环携带的依赖链,其中包括存储转发延迟。首先,这可以隐藏很多循环开销。在像任何 x86 一样的现代乱序执行 CPU 上,成本不只是累加。事情可能会重叠:大量循环开销可能会在延迟瓶颈的阴影下运行。
更糟糕的是,存储转发延迟不是恒定的,当在存储和重新加载之间存在更多开销时,尤其是不相关的存储,它会变得更快。请参阅函数调用比空循环更快的循环和添加冗余分配在没有优化的情况下编译时加速代码(其中调试构建将循环计数器保留在内存中以创建此瓶颈)。volatile
即使在优化的构建中也使用这样的 force asm。
在英特尔 Sandybridge 系列上,volatile
增量可以通过更多循环开销变得更快。 因此,如果您尝试将其推广到其他更典型的情况,这种循环体的选择会产生极具误导性的基准数字。正如我(彼得)在我的回答中所说,微基准测试很难。有关更多详细信息,请参阅评论中的讨论。
此问题中的基准数字是针对此代码的,但您应该期望其他循环体在质量上有所不同。
请注意,此答案小心不要得出任何关于在实际代码中可能更快的结论
但我要补充一点,内循环内的非内联函数调用几乎总是比内循环内易于预测的分支更昂贵。非内联函数调用强制编译器更新内存中暂时仅在寄存器中的所有值,因此内存状态与 C++ 抽象机匹配。至少对于全局变量和静态变量,以及通过函数 args 指向/可访问的任何内容(包括this
成员函数)。它还破坏了所有调用破坏的寄存器。
所以在性能方面,我希望在循环外部初始化的指向成员函数的指针类似于选项 A(if()
内部),但几乎总是更糟。或者如果它们都优化远离恒定传播,则相等。
编者注结束
对于我将称为 D 的每个实现 A、B 和我的(我省略了 C,因为我无法弄清楚您打算如何在实际实现中使用它),并给出:
class MyClass
{
public:
void foo(){ volatile static int a = 0 ; a++ ; }
void bar(){ volatile static int a = 0 ; a++ ; }
// FIXME: don't put a tmp var inside the class object!
// but keep in mind the benchmark results below *are* done with this
void (MyClass::*foobar)() ;
} acc ;
static const int n = 10000 ;
我得到以下结果:
VC++ 2019 默认调试:(注意:不要计时调试模式,这几乎总是没用的。)
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 对 最敏感,如果需要确定性行为,则可以避免这种情况,但实际bar()` 实现specialCase
中的差异可能会掩盖这种差异。foo() and
您的结果可能很大程度上取决于您使用的编译器、目标和编译器选项,并且差异可能不是那么显着,以至于您可以对所有编译器得出任何结论。
例如,在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" ;
}