Сравнение эффективности: указатель на функцию, объект функции и разветвленный код. Почему функциональный объект хуже всего работает?

Я надеялся улучшить производительность, передав указатель функции или объект функции вызову функции внутри вложенного цикла, чтобы избежать ветвления цикла. Ниже приведены три кода: один с объектом функции, с указателем функции и с ветвлением. Для любого варианта оптимизации компилятора или для любого размера задачи и указатель на функцию, и версия объекта работают меньше всего. Это удивительно для меня; почему накладные расходы из-за указателя функции или масштаба объекта с размером проблемы? Второй вопрос. Почему объект функции работает хуже, чем указатель на функцию?

Обновлять

В конце я также добавляю версию лямбда-выражения того же кода. Опять побеждает грубая сила. Версия лямбда-выражения занимает более чем в два раза больше времени с оптимизацией или без нее по сравнению с соответствующим кодом грубой силы и для другого размера задачи.

Коды ниже. Выполнить с помощью ./a.out [SIZE] [function choice]

Объект функции:

    #include <iostream>
    #include <chrono>
    class Interpolator
    {
    public:
      Interpolator(){};
      virtual double operator()(double left, double right) = 0;
    };
    class FirstOrder : public Interpolator
    {
    public:
      FirstOrder(){};
      virtual double operator()(double left, double right) { return 2.0 * left * left * left + 3.0 * right; }
    };
    class SecondOrder : public Interpolator
    {
    public:
      SecondOrder(){};
      virtual double operator()(double left, double right) { return 2.0 * left * left + 3.0 * right * right; }
    };
    
    double kernel(double left, double right, Interpolator *int_func) { return (*int_func)(left, right); }
    
    int main(int argc, char *argv[])
    {
      double *a;
      int SIZE = atoi(argv[1]);
      int it = atoi(argv[2]);
      //initialize
      a = new double[SIZE];
      for (int i = 0; i < SIZE; i++)
        a[i] = (double)i;
      std::cout << "Initialized" << std::endl;
      Interpolator *first;
      switch (it)
      {
      case 1:
        first = new FirstOrder();
        break;
      case 2:
        first = new SecondOrder();
        break;
      }
      std::cout << "function" << std::endl;
      auto start = std::chrono::high_resolution_clock::now();
      //loop
      double g;
      for (int i = 0; i < SIZE; i++)
      {
        g = 0.0;
        for (int j = 0; j < SIZE; j++)
        {
          g += kernel(a[i], a[j], first);
        }
        a[i] += g;
      }
      auto stop = std::chrono::high_resolution_clock::now();
      auto duration = std::chrono::duration_cast<std::chrono::microseconds>(stop - start);
      std::cout << "Finalized in " << duration.count() << " ms" << std::endl;
      return 0;
    }

Указатель функции:

#include <iostream>
#include <chrono>

double firstOrder(double left, double right) { return 2.0 * left * left * left + 3.0 * right; }
double secondOrder(double left, double right) { return 2.0 * left * left + 3.0 * right * right; }
double kernel(double left, double right, double (*f)(double, double))
{
    return (*f)(left, right);
}
int main(int argc, char *argv[])
{
    double *a;
    int SIZE = atoi(argv[1]);
    int it = atoi(argv[2]);
    a = new double[SIZE];
    for (int i = 0; i < SIZE; i++)
        a[i] = (double)i; // initialization
    std::cout << "Initialized" << std::endl;

    //Func func(it);
    double (*func)(double, double);
    switch (it)
    {
    case 1:
        func = &firstOrder;
        break;
    case 2:
        func = &secondOrder;
        break;
    }
    std::cout << "function" << std::endl;
    auto start = std::chrono::high_resolution_clock::now();
    //loop
    double g;
    for (int i = 0; i < SIZE; i++)
    {
        g = 0.0;
        for (int j = 0; j < SIZE; j++)
        {
            g += kernel(a[i], a[j], func);
        }
        a[i] += g;
    }
    auto stop = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::microseconds>(stop - start);
    std::cout << "Finalized in " << duration.count() << " ms" << std::endl;
    return 0;
}

Ветвление:

#include <iostream>
#include <chrono>

double firstOrder(double left, double right) { return 2.0 * left * left * left + 3.0 * right; }
double secondOrder(double left, double right) { return 2.0 * left * left + 3.0 * right * right; }
int main(int argc, char *argv[])
{
    double *a;
    int SIZE = atoi(argv[1]); // array size
    int it = atoi(argv[2]);   // function choice
    //initialize
    a = new double[SIZE];
    double g;
    for (int i = 0; i < SIZE; i++)
        a[i] = (double)i; // initialization
    std::cout << "Initialized" << std::endl;

    auto start = std::chrono::high_resolution_clock::now();
    //loop
    for (int i = 0; i < SIZE; i++)
    {
        g = 0.0;
        for (int j = 0; j < SIZE; j++)
        {
            if (it == 1)
            {
                g += firstOrder(a[i], a[j]);
            }
            else if (it == 2)
            {
                g += secondOrder(a[i], a[j]);
            }
        }
        a[i] += g;
    }
    auto stop = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::microseconds>(stop - start);
    std::cout << "Finalized in " << duration.count() << " ms" << std::endl;
    return 0;
}

Лямбда-выражение

#include <iostream>                                                             
#include <chrono>                                                               
#include<functional>                                                            
                                                                                
std::function<double(double, double)> makeLambda(int kind){                     
  return [kind] (double left, double right){                                    
    if(kind == 0) return 2.0 * left * left * left + 3.0 * right;                
    else if (kind ==1) return 2.0 * left * left + 3.0 * right * right;          
  };                                                                            
}                                                                               
                                                                                
int main(int argc, char *argv[])                                                
{                                                                               
  double *a;                                                                    
  int SIZE = atoi(argv[1]);                                                     
  int it = atoi(argv[2]);                                                       
  //initialize                                                                  
  a = new double[SIZE];                                                         
  for (int i = 0; i < SIZE; i++)                                                
    a[i] = (double)i;                                                           
  std::cout << "Initialized" << std::endl;                                      
  std::function<double(double,double)> interp ;                                 
  switch (it)                                                                   
  {                                                                             
  case 1:                                                                       
    interp = makeLambda(0);                                                     
    break;                                                                      
  case 2:                                                                       
    interp = makeLambda(1);                                                     
    break;                                                                      
  }                                                                             
  std::cout << "function" << std::endl;                                         
  auto start = std::chrono::high_resolution_clock::now();                       
  //loop                                                                        
  double g;                                                                     
  for (int i = 0; i < SIZE; i++)                                                
  {                                                                             
    g = 0.0;                                                                    
    for (int j = 0; j < SIZE; j++)                                              
    {                                                                           
      g += interp(a[i], a[j]);                                                  
    }                                                                           
    a[i] += g;                                                                  
  }                                                                             
  auto stop = std::chrono::high_resolution_clock::now();                        
  auto duration = std::chrono::duration_cast<std::chrono::microseconds>(stop - start);
  std::cout << "Finalized in " << duration.count() << " ms" << std::endl;       
  return 0;                                                                     
}

person P. Nair    schedule 01.11.2020    source источник
comment
Сравните с четвертым вариантом: поместите if вне цикла (хотя разницы не будет, так как компилятор сделает это за вас).   -  person bipll    schedule 01.11.2020
comment
Объект функции, вероятно, имеет больше ссылок на память, потому что вы использовали виртуальные функции, поэтому он должен искать правильную функцию на основе типа объекта.   -  person John3136    schedule 01.11.2020
comment
@bipll Я пробовал и с -O0, и результат тот же. Будет ли компилятор делать это и в этом случае?   -  person P. Nair    schedule 01.11.2020
comment
@John3136 John3136 Я сделаю еще один пример без виртуальной функции.   -  person P. Nair    schedule 01.11.2020
comment
@P.Nair Когда вы компилируете без оптимизации, компилятор не тот, кто ускоряет случай ветвления. Скорее, этот случай, вероятно, выиграет от предсказание ветвления и получение преимущества, аналогичного оптимизации компилятора.   -  person JaMiT    schedule 01.11.2020
comment
Я также проверил с clang как с -O0, так и с -O3, и третья версия работает быстрее. Третья версия генерирует меньший ассемблерный код. Однако я не понимаю, почему.   -  person Ali    schedule 01.11.2020
comment
Одна из основных причин, по которой я бы использовал функтор или лямбду, — предоставить функцию в качестве аргумента функции, внутри которой происходят вычисления со сложными вложенными циклами. Так что внутренние ядра не должны иметь условия if. Означает ли приведенное выше сравнение, что мои рассуждения неверны для такого варианта использования?   -  person P. Nair    schedule 12.11.2020
comment
@bipll Я попробовал это, и ты прав. Я не публикую код здесь. Это будет раздутый код, чего мы пытаемся избежать. Станут ли лямбда-выражения или версии функций более эффективными, чем грубая сила, за пределами некоторой пороговой сложности?   -  person P. Nair    schedule 12.11.2020


Ответы (1)


Хотя этот вопрос касается производительности, у меня есть несколько замечаний по улучшению кода:

Ветвление может быть потенциально более подверженным ошибкам

Представьте, что вы хотите добавить еще одну функцию интерполяции. Затем вам нужно определить новую функцию и добавить новый случай (для переключателя) или новый if/else. Решением может быть создание вектора лямбд:

std::vector<std::function<double(double, double)>> Interpolate {
  [](double left, double right) {return 2.0*left*left*left + 3.0*right;}, //first order
  [](double left, double right) {return 2.0*left*left + 3.0*right*right;} //second order
};

или альтернативно:

double firstOrder(double left, double right) {return 2.0*left*left*left + 3.0*right;}
double secondOrder(double left, double right) {return 2.0*left*left + 3.0*right*right;}
std::array<double(*)(double, double), 2> Interpolate {firstOrder, secondOrder};

С этим изменением вам не нужен оператор if или switch. Вы просто пишете:

g += Interpolate[it-1] (x, y);

вместо

if (it == 1)
    g += firstOrder(a[i], a[j]);
else if (it == 2)
    g += secondOrder(a[i], a[j]);

Следовательно, требуется меньше обслуживания, и меньше вероятность пропустить оператор if/else.

Лучше избегать голых новых

Вместо double *a = new double[SIZE]; люди предлагают использовать std::vector<double> a (SIZE);. Таким образом, нам не нужно освобождать какой-либо ресурс, и мы избегаем потенциальной утечки памяти в коде.

Возвращаясь к вопросу, я не вижу причин, по которым лямбда-выражения должны обеспечивать лучшую производительность. Особенно в этом случае, когда мы не получаем преимуществ от constexpr природы лямбда-выражений.

person Ali    schedule 12.11.2020