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

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

假设您调用了一个计算值的方法并且returns它:

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 的情况下,它的性能最差,而在 __forceinline 的情况下,它似乎要好得多。

我考虑过使用两个重写来计算一个虚拟方法,但我读到虚拟方法不是优化代码的好方法,因为我们仍然必须在 运行 时间找到正确的方法来调用。不过我没试过。

但是,无论我怎么修改,我似乎都无法恢复到原来的性能(也许这是不可能的?)。是否有设计模式以最佳方式处理此问题(可能 cleaner/easier 以保持更好)?


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 到 运行,而调用时我平均得到 15,00s 到 运行 calculate0 (with __forceinline, 这似乎使差距更小).

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

最后,如果你的情况和我一样,我的建议如下:

  • 如果正确的预测很少改变,请不要担心分支预测错误。

尽管我无法提供确切的数字来支持它,但成本似乎微不足道。

  • 新中间方法的开销成本可以通过 __force 内联 VC++
  • 来减轻

我能够注意到差异,这最终是避免降低性能的最佳方法。仅当您内联的方法很小时才采用这种方式,例如简单的 getters 等。我不知道为什么我的编译器不会选择自己内联这些方法,但 __force 内联实际上起到了作用(即使你不能确定编译器是否会将方法内联为 __force inline 只是对编译器的一个建议。