Указатель на функцию работает быстрее, чем встроенная функция. Зачем?

Я запустил свой тест на своем компьютере (Intel i3-3220 @ 3.3 ГГц, Fedora 18) и получил очень неожиданные результаты. Указатель на функцию был немного быстрее встроенной функции.

Код:

#include <iostream>
#include <chrono>
inline short toBigEndian(short i)
{
return (i<<8)|(i>>8);
}
short (*toBigEndianPtr)(short i)=toBigEndian;
int main()
{
std::chrono::duration<double> t;
int total=0;
for(int i=0;i<10000000;i++)
{
auto begin=std::chrono::high_resolution_clock::now();
short a=toBigEndian((short)i);//toBigEndianPtr((short)i);
total+=a;
auto end=std::chrono::high_resolution_clock::now();
t+=std::chrono::duration_cast<std::chrono::duration<double>>(end-begin);
}
std::cout<<t.count()<<", "<<total<<std::endl;
return 0;
}

составлено с

g++ test.cpp -std=c++0x -O0

Цикл «toBigEndian» всегда заканчивается примерно в пределах 0,26–0,27 секунды, тогда как цикл «toBigEndianPtr» занимает 0,21–0,22 секунды.

Что делает это еще более странным, так это то, что когда я удаляю «total», указатель функции становится медленнее на 0,35–0,37 секунды, а встроенная функция — на 0,27–0,28 секунды.

Мой вопрос:

Почему указатель функции быстрее, чем встроенная функция, когда существует значение total?

5

Решение

Краткий ответ: это не так.

  • Вы компилируете с -O0, который не оптимизирует (сильно). Без оптимизации вы не сможете сказать «быстро», потому что неоптимизированный код не так быстр, как может быть.
  • Вы берете адрес toBigEndian, который предотвращает встраивание. inline В любом случае ключевое слово является подсказкой для компилятора, за которым оно может следовать, а может и не следовать. Вы сделали все возможное, чтобы не заставь это следовать этому совету.

Итак, чтобы дать вашим измерениям какой-либо смысл,

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

Другие решения

Распространенной ошибкой в ​​измерении производительности (помимо забвения оптимизации) является использование неправильного инструмента для измерения. Использование std :: chrono было бы хорошо, если бы вы измеряли производительность всей вашей, 10000000 или 500000000 итераций. Вместо этого вы просите его измерить вызов / inline toBigEndian. Функция, которая состоит из 6 инструкций. Поэтому я переключился на rdtsc (считывание счетчика меток времени, то есть тактов).

Позволяя компилятору действительно оптимизировать все в цикле, не загромождая его записью времени на каждой крошечной итерации, мы получаем другую последовательность кода. Теперь, после компиляции с g++ -O3 fp_test.cpp -o fp_test -std=c++11Я наблюдаю желаемый эффект. Встроенная версия в среднем составляет около 2,15 циклов за итерацию, а указатель на функцию занимает около 7,0 циклов за итерацию.

Даже без использования rdtsc, разница все еще довольно заметна. Время настенных часов составляло 360 мс для встроенного кода и 1,17 с для указателя функции. Таким образом, можно использовать std :: chrono вместо rdtsc в этом коде.

Модифицированный код следует:

#include <iostream>
static inline uint64_t rdtsc(void)
{
uint32_t hi, lo;
asm volatile ("rdtsc" : "=a"(lo), "=d"(hi));
return ( (uint64_t)lo)|( ((uint64_t)hi)<<32 );
}
inline short toBigEndian(short i)
{
return (i<<8)|(i>>8);
}
short (*toBigEndianPtr)(short i)=toBigEndian;
#define LOOP_COUNT 500000000
int main()
{
uint64_t t = 0, begin=0, end=0;
int total=0;
begin=rdtsc();
for(int i=0;i<LOOP_COUNT;i++)
{
short a=0;
a=toBigEndianPtr((short)i);
//a=toBigEndian((short)i);
total+=a;
}
end=rdtsc();
t+=(end-begin);
std::cout<<((double)t/LOOP_COUNT)<<", "<<total<<std::endl;
return 0;
}
3

О боже (нужно ли подвергать цензуре ругань здесь?), Я это выяснил. Это было как-то связано с выбором времени в цикле. Когда я вытащил его наружу, как показано ниже,

#include <iostream>
#include <chrono>
inline short toBigEndian(short i)
{
return (i<<8)|(i>>8);
}

short (*toBigEndianPtr)(short i)=toBigEndian;
int main()
{
int total=0;
auto begin=std::chrono::high_resolution_clock::now();
for(int i=0;i<100000000;i++)
{
short a=toBigEndianPtr((short)i);
total+=a;
}
auto end=std::chrono::high_resolution_clock::now();
std::cout<<std::chrono::duration_cast<std::chrono::duration<double>>(end-begin).count()<<", "<<total<<std::endl;
return 0;
}

результаты такие, какими они должны быть. 0,08 секунды для встроенного, 0,20 секунды для указателя. Извините, что беспокою вас, ребята.

2

Во-первых, с -O0 вы не запускаете оптимизатор, что означает, что компилятор игнорирует ваш запрос на встроенную функцию, как это можно сделать бесплатно. Стоимость двух разных звонков должна быть почти одинаковой. Попробуйте с -O2.

Во-вторых, если вы работаете только в течение 0,22 секунды, странные переменные затраты, связанные с запуском вашей программы, полностью влияют на стоимость выполнения тестовой функции. Этот вызов функции — всего лишь несколько инструкций. Если ваш процессор работает на частоте 2 ГГц, он должен выполнить этот вызов функции примерно за 20 наносекунд, чтобы вы могли видеть, что независимо от того, что вы измеряете, это не стоимость запуска этой функции.

Попробуйте вызвать тестовую функцию в цикле, скажем, 1 000 000 раз. Увеличьте количество циклов в 10 раз, пока тест не займет> 10 секунд. Затем разделите результат на количество циклов для приблизительной оценки стоимости операции.

0

Во многих / наиболее уважающих себя современных компиляторах код, который вы разместили, будет по-прежнему встроен в вызов функции, даже когда он вызывается через указатель. (Предполагая, что компилятор прилагает разумные усилия для оптимизации кода). Ситуация просто слишком легко увидеть. Другими словами, сгенерированный код может легко оказаться практически одинаковым в обоих случаях, что означает, что ваш тест не очень полезен для измерения того, что вы пытаетесь измерить.

Если вы действительно хотите убедиться, что вызов физически выполняется через указатель, вы должны приложить усилия, чтобы «запутать» компилятор до такой степени, что он не сможет определить значение указателя во время компиляции. Например, сделайте значение указателя зависимым от времени выполнения, как в

toBigEndianPtr = rand() % 1000 != 0 ? toBigEndian : NULL;

или что-то в этом роде. Вы также можете объявить свой указатель на функцию как volatile, что обычно будет вызывать подлинный вызов через указатель каждый раз, а также заставит компилятор перечитывать значение указателя из памяти на каждой итерации.

-1
По вопросам рекламы [email protected]