Допустим, у вас есть вызов метода, который вычисляет значение и возвращает его:
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, казалось бы, намного лучше.
Я думал о том, чтобы вычислить виртуальный метод с двумя переопределениями, но я читал, что виртуальные методы не являются хорошим способом оптимизации кода, поскольку нам все еще нужно найти правильный метод для вызова во время выполнения. Я не пробовал, хотя.
Однако, какие бы модификации я ни делал, мне никогда не удавалось восстановить первоначальные исполнения (может быть, это невозможно?). Существует ли схема проектирования, позволяющая справиться с этим наиболее оптимальным образом (и, возможно, чем чище / легче поддерживать, тем лучше)?
Полный пример для 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 для работы в примере, так как это только ухудшает ситуацию).
Используя приведенный выше пример, я получаю в среднем 14,61 с, чтобы запустить его при вызове прямого метода calc1 в основном, тогда как у меня получается в среднем 15,00 с при вызове Calculate 0 (с __forceinline, который, кажется, делает щель меньше).