假设您有一个对计算值并返回值的方法的调用:
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逻辑所需的额外代码,性能会有所下降。它还取决于编译器是否内联calculate
、calculate1
或calculate2
。理想情况下,它们都应该内联,尽管与直接调用calculate1
或calculate2
相比,这种情况不太可能发生,因为代码大小更大。请注意,我没有试图重现您的结果,但对于3%的性能下降没有什么特别可疑的。如果可以使useFirstVersion
从不动态更改,则可以将其转换为宏。否则,通过函数指针调用calculate
的想法将消除大部分性能开销。顺便说一句,我不认为MSVC可以通过函数指针内联调用,但这些函数是内联的好候选者。
最后,如果你和我处于同样的情况,我会建议如下:
- 如果正确的预测很少改变,那么就不用担心分支预测错误
虽然我无法提供确切的数字来支持,但成本似乎很低。
- 新中介方法的开销成本可以通过VC中的__force内联来降低++
我能够注意到差异,这最终是避免性能下降的最佳方式。只有当内联的方法很小,比如简单的getters&这样的我不知道为什么我的编译器不选择自己内联方法,但__force内联实际上做到了(尽管你不能确定编译器会内联方法,因为__force inline只是对编译器的一个建议(。