Как убедиться, что во время вызова метода с использованием одного из двух методов, основанных на логическом значении, не допускается неправильное предсказание ветвлений - PullRequest
0 голосов
/ 31 октября 2018

Допустим, у вас есть вызов метода, который вычисляет значение и возвращает его:

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, который, кажется, делает щель меньше).

Ответы [ 2 ]

0 голосов
/ 26 ноября 2018

В конце концов, если вы находитесь в той же ситуации, что и я, я бы посоветовал следующее:

  • Не беспокойтесь о неправильном предсказании ветвлений, если правильное предсказание редко когда-либо меняется.

Стоимость кажется незначительной, хотя я не могу предоставить точные цифры для ее подтверждения.

  • Стоимость накладных расходов на новые промежуточные методы может быть уменьшена с помощью __force inline в VC ++

Я могу заметить разницу, и в конце концов это был лучший способ избежать унизительных выступлений. Идите по этому пути, только если методы, которые вы встраиваете, малы, как простые методы получения и так далее. Я не знаю, почему мой компилятор не решил встроить методы сам по себе, но встроенная функция __force фактически сделала свое дело (даже если вы не можете быть уверены, что компилятор встроит методы, поскольку встроенная функция __force является лишь предложением компилятору ).

0 голосов
/ 01 ноября 2018

Поскольку useFirstVersion редко изменяется, путь выполнения calculate очень легко прогнозируется большинством методов прогнозирования ветвлений. Производительность немного снижается из-за дополнительного кода, необходимого для реализации логики if / else. Это также зависит от того, встроен ли компилятор calculate, calculate1 или calculate2. В идеале все они должны быть встроены, хотя это менее вероятно по сравнению с вызовом calculate1 или calculate2 напрямую, поскольку размер кода больше. Обратите внимание, что я не пытался воспроизвести ваши результаты, но нет ничего особенно подозрительного в снижении производительности на 3%. Если вы можете сделать useFirstVersion, чтобы он никогда не изменялся динамически, то вы можете превратить его в макрос. В противном случае идея вызова calculate через указатель на функцию устранит большую часть накладных расходов. Кстати, я не думаю, что MSVC может встраивать вызовы через указатели функций, но эти функции являются хорошими кандидатами для встраивания.

...