如何确保在使用基于布尔值的两个方法之一调用方法时避免分支预测错误

How to make sure to avoid branch misprediction when calling a method using one of two methods based on a boolean

本文关键字:方法 两个 调用 错误 分支预测 确保 何确保 于布尔      更新时间:2023-10-16

假设您有一个对计算值并返回值的方法的调用:

double calculate(const double& someArg);

您实现了另一种计算方法,该方法与第一种方法具有相同的配置文件,但工作方式不同:

double calculate2(const double& someArg);

你希望能够根据布尔设置从一个切换到另一个,所以你最终会得到这样的东西:

double calculate(const double& someArg)
{
if (useFirstVersion) // <-- this is a boolean
return calculate1(someArg); // actual first implementation
else
return calculate2(someArg); // second implementation
}

布尔值可能在运行时发生变化,但这种情况非常罕见。

我注意到一个小但明显的性能打击,我认为这是由于分支预测失误或缓存不友好的代码造成的。

如何优化它以获得最佳运行时性能?


我对这个问题的想法和尝试:

我尝试使用指针指向函数来确保避免分支预测错误:

当时的想法是,当布尔值发生变化时,我会更新指向函数的指针。这样,就没有if/else,我们直接使用指针:

指针定义如下:

double (ClassWeAreIn::*pCalculate)(const double& someArg) const;

新的计算方法变成这样:

double calculate(const double& someArg)
{
(this->*(pCalculate))(someArg);
}

我试着将它与__forceinline结合使用,它确实产生了影响(我不确定这是否应该是预期的,因为编译器应该已经做到了?(。没有__forceline是性能最差的,有了__forceline似乎好多了。

我曾想过用两个重写来计算一个虚拟方法,但我读到虚拟方法不是优化代码的好方法,因为我们仍然需要找到在运行时调用的正确方法。不过我没有尝试。

然而,无论我做了什么修改,我似乎都无法恢复原来的表演(也许这是不可能的?(。是否有一种设计模式可以以最优化的方式处理这一问题(可能越干净/越容易维护越好(?


VS的完整示例:

main.cpp

#include "stdafx.h"
#include "SomeClass.h"
#include <time.h>
#include <stdlib.h>
#include <chrono>
#include <iostream>
int main()
{
srand(time(NULL));
auto start = std::chrono::steady_clock::now();
SomeClass someClass;
double result;
for (long long i = 0; i < 1000000000; ++i)
result = someClass.calculate(0.784542);
auto end = std::chrono::steady_clock::now();
std::chrono::duration<double> diff = end - start;
std::cout << diff.count() << std::endl;
return 0;
}

SomeClass.cpp

#include "stdafx.h"
#include "SomeClass.h"
#include <math.h>
#include <stdlib.h>
double SomeClass::calculate(const double& someArg)
{
if (useFirstVersion)
return calculate1(someArg);
else
return calculate2(someArg);
}
double SomeClass::calculate1(const double& someArg)
{
return asinf((rand() % 10 + someArg)/10);
}
double SomeClass::calculate2(const double& someArg)
{    
return acosf((rand() % 10 + someArg) / 10);
}

SomeClass.h

#pragma once
class SomeClass
{
public:
bool useFirstVersion = true;
double calculate(const double& someArg);
double calculate1(const double& someArg);
double calculate2(const double& someArg);
};

(我没有在示例中包含ptr函数,因为它似乎只会让事情变得更糟(。


使用上面的例子,当在main中直接调用calculate1时,我平均要运行14,61s,而当调用calculate 0时,我的平均要运行15,00s(使用__forceinline,这似乎使差距更小(。

由于useFirstVersion很少更改,因此大多数分支预测技术都很容易预测calculate的执行路径。由于实现if/else逻辑所需的额外代码,性能会有所下降。它还取决于编译器是否内联calculatecalculate1calculate2。理想情况下,它们都应该内联,尽管与直接调用calculate1calculate2相比,这种情况不太可能发生,因为代码大小更大。请注意,我没有试图重现您的结果,但对于3%的性能下降没有什么特别可疑的。如果可以使useFirstVersion从不动态更改,则可以将其转换为宏。否则,通过函数指针调用calculate的想法将消除大部分性能开销。顺便说一句,我不认为MSVC可以通过函数指针内联调用,但这些函数是内联的好候选者。

最后,如果你和我处于同样的情况,我会建议如下:

  • 如果正确的预测很少改变,那么就不用担心分支预测错误

虽然我无法提供确切的数字来支持,但成本似乎很低。

  • 新中介方法的开销成本可以通过VC中的__force内联来降低++

我能够注意到差异,这最终是避免性能下降的最佳方式。只有当内联的方法很小,比如简单的getters&这样的我不知道为什么我的编译器不选择自己内联方法,但__force内联实际上做到了(尽管你不能确定编译器会内联方法,因为__force inline只是对编译器的一个建议(。