Почему рекурсивная версия функции будет быстрее, чем итеративная в C?
Я проверяю разницу между двумя реализациями градиентного спуска, я предполагаю, что после оптимизации компилятора обе версии алгоритма будут эквивалентны.
к моему удивлению, рекурсивная версия была значительно быстрее. Я не отбросил фактический дефект ни в одной из версий или даже в том, как я измеряю время. Ребята, вы не могли бы мне кое-что рассказать?
Это мой код:
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#include <stdint.h>
double f(double x)
{
return 2*x;
}
double descgrad(double xo, double xnew, double eps, double precision)
{
// printf("step ... x:%f Xp:%f, delta:%fn",xo,xnew,fabs(xnew - xo));
if (fabs(xnew - xo) < precision)
{
return xnew;
}
else
{
descgrad(xnew, xnew - eps*f(xnew), eps, precision);
}
}
double descgraditer(double xo, double xnew, double eps, double precision)
{
double Xo = xo;
double Xn = xnew;
while(fabs(Xn-Xo) > precision)
{
//printf("step ... x:%f Xp:%f, delta:%fn",Xo,Xn,fabs(Xn - Xo));
Xo = Xn;
Xn = Xo - eps * f(Xo);
}
return Xn;
}
int64_t timespecDiff(struct timespec *timeA_p, struct timespec *timeB_p)
{
return ((timeA_p->tv_sec * 1000000000) + timeA_p->tv_nsec) -
((timeB_p->tv_sec * 1000000000) + timeB_p->tv_nsec);
}
int main()
{
struct timespec s1, e1, s2, e2;
clock_gettime(CLOCK_MONOTONIC, &s1);
printf("Minimum : %fn",descgraditer(100,99,0.01,0.00001));
clock_gettime(CLOCK_MONOTONIC, &e1);
clock_gettime(CLOCK_MONOTONIC, &s2);
printf("Minimum : %fn",descgrad(100,99,0.01,0.00001));
clock_gettime(CLOCK_MONOTONIC, &e2);
uint64_t dif1 = timespecDiff(&e1,&s1) / 1000;
uint64_t dif2 = timespecDiff(&e2,&s2) / 1000;
printf("time_iter:%llu ms, time_rec:%llu ms, ratio (dif1/dif2) :%gn", dif1,dif2, ((double) ((double)dif1/(double)dif2)));
printf("End. n");
}
Я собираю с gcc 4.5.2 на Ubuntu 11.04 со следующими параметрами: град ССЗ.c-O3-lrt-o DG
вывод моего кода:
Minimum : 0.000487
Minimum : 0.000487
time_iter:127 ms, time_rec:19 ms, ratio (dif1/dif2) :6.68421
End.
Я прочитал поток,который также спрашивает о рекурсивной версии алгоритма быстрее, чем итеративный. Объяснение было в том, что будучи рекурсивной версией, использующей стек, а другая версия, использующая некоторые векторы, доступ к куче замедлял итеративную версию. Но в этом случае (в лучшем из моих понимание) я просто использую стек в обоих случаях.
Я что-то пропустила? Что-нибудь очевидное, чего я не вижу? Мой способ измерения времени? Какие-либо выводы?
изменить: Тайна решена в комментарии. Как сказал @TonyK, инициализация printf замедляла первое выполнение. Жаль, что я пропустил эту очевидную вещь.
кстати, код компилируется правильно без предупреждений. Я не думаю, что " возвращение descgrad(.."необходимо, так как условие остановки происходит раньше.
7 ответов
я скомпилировал и запустил ваш код локально. Перемещение printf
вне синхронизированного блока делает обе версии выполняться в ~5 мс каждый раз.
таким образом, центральной ошибкой в вашем времени является то, что вы измеряете сложный зверь printf
и его время выполнения затмевает код, который вы фактически пытаетесь измерить.
мой main()
-функция теперь выглядит так:
int main() {
struct timespec s1, e1, s2, e2;
double d = 0.0;
clock_gettime(CLOCK_MONOTONIC, &s1);
d = descgraditer(100,99,0.01,0.00001);
clock_gettime(CLOCK_MONOTONIC, &e1);
printf("Minimum : %f\n", d);
clock_gettime(CLOCK_MONOTONIC, &s2);
d = descgrad(100,99,0.01,0.00001);
clock_gettime(CLOCK_MONOTONIC, &e2);
printf("Minimum : %f\n",d);
uint64_t dif1 = timespecDiff(&e1,&s1) / 1000;
uint64_t dif2 = timespecDiff(&e2,&s2) / 1000;
printf("time_iter:%llu ms, time_rec:%llu ms, ratio (dif1/dif2) :%g\n", dif1,dif2, ((double) ((double)dif1/(double)dif2)));
printf("End. \n");
}
- Это мой способ измерения времени?
да. В короткие промежутки времени, которые вы измеряете, планировщик может оказать огромное влияние на вашу программу. Вам нужно либо сделать свой тест намного дольше, чтобы усреднить такие различия, либо использовать CLOCK_PROCESS_CPUTIME_ID
вместо этого для измерения времени процессора, используемого вашим процессом.
во-первых, ваш рекурсивный шаг пропускает return
:
double descgrad(double xo, double xnew, double eps, double precision)
{
if (fabs(xnew - xo) < precision)
return xnew;
else
descgrad(xnew, xnew - eps*f(xnew), eps, precision);
}
должно быть:
double descgrad(double xo, double xnew, double eps, double precision)
{
if (fabs(xnew - xo) < precision)
return xnew;
else
return descgrad(xnew, xnew - eps*f(xnew), eps, precision);
}
этот надзор вызывает возвращаемое значение descgrad
чтобы быть неопределенным, поэтому компилятор даже не генерировать код для него на все ;)
для начала, вы включали printf в то время, когда вы пытались измерить. Это всегда гигантский нет-нет, потому что он может и, скорее всего, приостановит ваш процесс при выполнении вывода консоли. Фактически выполнение любого системного вызова может полностью сбросить измерения времени, подобные этим.
а во-вторых, как кто-то еще упоминал, на этом коротком периоде выборки прерывания планировщика могут иметь огромное влияние.
это не идеально, но попробуйте это для вашего main и вы увидите, что на самом деле очень мало разницы. По мере увеличения количества циклов соотношение приближается к 1,0.
#define LOOPCOUNT 100000
int main()
{
struct timespec s1, e1, s2, e2;
int i;
clock_gettime(CLOCK_MONOTONIC, &s1);
for(i=0; i<LOOPCOUNT; i++)
{
descgraditer(100,99,0.01,0.00001);
}
clock_gettime(CLOCK_MONOTONIC, &e1);
clock_gettime(CLOCK_MONOTONIC, &s2);
for(i=0; i<LOOPCOUNT; i++)
{
descgrad(100,99,0.01,0.00001);
}
clock_gettime(CLOCK_MONOTONIC, &e2);
uint64_t dif1 = timespecDiff(&e1,&s1) / 1000;
uint64_t dif2 = timespecDiff(&e2,&s2) / 1000;
printf("time_iter:%llu ms, time_rec:%llu ms, ratio (dif1/dif2) :%g\n", dif1,dif2, ((double) ((double)dif1/(double)dif2)));
printf("End. \n");
}
EDIT: после просмотра разобранного вывода с помощью objdump -dS
Я заметил несколько вещей:
С оптимизацией-O3 вышеуказанный код полностью оптимизирует вызов функции. Однако он по-прежнему создает код для двух функций, и ни одна из них не является рекурсивной.
во-вторых, с -О0, так что результирующий код фактически рекурсивен, рекурсивная версия буквально триллион раз медленнее. Я предполагаю, что стек вызовов заставляет переменные заканчиваться в памяти, где итеративная версия исчерпывает регистры и / или кэш.
принятый ответ неверен.
существует разница во времени выполнения итеративной функции и рекурсивной функции, и причиной является оптимизация компилятора -foptimize-sibling-calls
добавил -O3
.
во-первых, код:
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#include <stdint.h>
double descgrad(double xo, double xnew, double eps, double precision){
if (fabs(xnew - xo) <= precision) {
return xnew;
} else {
return descgrad(xnew, xnew - eps*2*xnew, eps, precision);
}
}
double descgraditer(double xo, double xnew, double eps, double precision){
double Xo = xo;
double Xn = xnew;
while(fabs(Xn-Xo) > precision){
Xo = Xn;
Xn = Xo - eps * 2*Xo;
}
return Xn;
}
int main() {
time_t s1, e1, d1, s2, e2, d2;
int i, iter = 10000000;
double a1, a2;
s1 = time(NULL);
for( i = 0; i < iter; i++ ){
a1 = descgraditer(100,99,0.01,0.00001);
}
e1 = time(NULL);
d1 = difftime( e1, s1 );
s2 = time(NULL);
for( i = 0; i < iter; i++ ){
a2 = descgrad(100,99,0.01,0.00001);
}
e2 = time(NULL);
d2 = difftime( e2, s2 );
printf( "time_iter: %d s, time_rec: %d s, ratio (iter/rec): %f\n", d1, d2, (double)d1 / d2 ) ;
printf( "return values: %f, %f\n", a1, a2 );
}
предыдущие сообщения были правильными, указывая, что вам нужно повторить много раз, чтобы усреднить помехи окружающей среды. Учитывая это, я отказался от вашей функции дифференцирования в пользу time.h
' s difftime
функция on time_t
данные, так как на протяжении многих итераций все, что тоньше секунды, бессмысленно. Кроме того, я удалил printfs в бенчмарке.
я также исправил ошибку в рекурсивной реализации. If-оператор исходного кода проверен на fabs(xnew-xo) < precision
, что неверно (или, по крайней мере, отличается от итеративной реализации). Итеративные циклы в то время как fabs() > точность, поэтому рекурсивная функция не должна рекурсировать, когда fabs точности. Добавление счетчиков "итерации" к обеим функциям подтверждает, что это исправление делает функцию логически эквивалентной.
компиляция и запуск с -O3
:
$ gcc test.c -O3 -lrt -o dg
$ ./dg
time_iter: 34 s, time_rec: 0 s, ratio (iter/rec): inf
return values: 0.000487, 0.000487
сборка и запуск без -O3
$ gcc test.c -lrt -o dg
$ ./dg
time_iter: 54 s, time_rec: 90 s, ratio (iter/rec): 0.600000
return values: 0.000487, 0.000487
при отсутствии оптимизации итерация выполняется лучше, чем рекурсия.
под -O3
оптимизация, однако, рекурсия выполняет десять миллионов итераций менее чем за секунду. Причина что это добавляет -foptimize-sibling-calls
, который оптимизирует сестринские и хвостовые рекурсивные вызовы,что именно то, что ваша рекурсивная функция использует.
конечно, я побежал, все будет -O3
оптимизаций, кроме -foptimize-sibling-calls
:
$ gcc test.c -lrt -o dg -fcprop-registers -fdefer-pop -fdelayed-branch -fguess-branch-probability -fif-conversion2 -fif-conversion -fipa-pure-const -fipa-reference -fmerge-constants -ftree-ccp -ftree-ch -ftree-copyrename -ftree-dce -ftree-dominator-opts -ftree-dse -ftree-fre -ftree-sra -ftree-ter -funit-at-a-time -fthread-jumps -falign-functions -falign-jumps -falign-loops -falign-labels -fcaller-saves -fcrossjumping -fcse-follow-jumps -fcse-skip-blocks -fdelete-null-pointer-checks -fexpensive-optimizations -fgcse -fgcse-lm -fpeephole2 -fregmove -freorder-blocks -freorder-functions -frerun-cse-after-loop -fsched-interblock -fsched-spec -fschedule-insns -fschedule-insns2 -fstrict-aliasing -ftree-pre -ftree-vrp -finline-functions -funswitch-loops -fgcse-after-reload -ftree-vectorize
$ ./dg
time_iter: 55 s, time_rec: 89 s, ratio (iter/rec): 0.617978
return values: 0.000487, 0.000487
рекурсия без оптимизации хвостового вызова выполняет хуже, чем итерация, так же, как и при компиляции без оптимизации. читайте об оптимизации компилятора вот!--44-->.
EDIT:
в качестве проверки правильности я обновил свой код, включая возвращаемые значения. Кроме того, я установил две статические переменные в 0 и увеличил каждую на рекурсии и итерации, чтобы проверить правильный вывод:
int a = 0;
int b = 0;
double descgrad(double xo, double xnew, double eps, double precision){
if (fabs(xnew - xo) <= precision) {
return xnew;
} else {
a++;
return descgrad(xnew, xnew - eps*2*xnew, eps, precision);
}
}
double descgraditer(double xo, double xnew, double eps, double precision){
double Xo = xo;
double Xn = xnew;
while(fabs(Xn-Xo) > precision){
b++;
Xo = Xn;
Xn = Xo - eps * 2*Xo;
}
return Xn;
}
int main() {
time_t s1, e1, d1, s2, e2, d2;
int i, iter = 10000000;
double a1, a2;
s1 = time(NULL);
for( i = 0; i < iter; i++ ){
a1 = descgraditer(100,99,0.01,0.00001);
}
e1 = time(NULL);
d1 = difftime( e1, s1 );
s2 = time(NULL);
for( i = 0; i < iter; i++ ){
a2 = descgrad(100,99,0.01,0.00001);
}
e2 = time(NULL);
d2 = difftime( e2, s2 );
printf( "time_iter: %d s, time_rec: %d s, ratio (iter/rec): %f\n", d1, d2, (double)d1 / d2 ) ;
printf( "return values: %f, %f\n", a1, a2 );
printf( "number of recurs/iters: %d, %d\n", a, b );
}
вывод:
$ gcc optimization.c -O3 -lrt -o dg
$ ./dg
time_iter: 41 s, time_rec: 24 s, ratio (iter/rec): 1.708333
return values: 0.000487, 0.000487
number of recurs/iters: 1755032704, 1755032704
ответы те же, и повторение то же самое.
также интересно отметить, что статическая переменная fetching / incrementing оказывает значительное влияние на оптимизация хвостового вызова, однако рекурсия все еще выполняет итерацию.
во-первых,clock_gettime
кажется, измеряет время настенных часов, а не
время выполнения. Во-вторых, фактическое время, которое вы измеряете
время выполнения printf
, а не время выполнения вашей функции.
И в-третьих, первый раз вы звоните printf
, это не в памяти, так это
должен быть вызван, включая значительный диск IO. Обратный порядок
вы запускаете тесты, и результаты также будут обратными.
Если вы хотите получить значительную измерений, вы должны убедиться, что что
- только код, который вы хотите измерить в измерения, или, по крайней мере, дополнительный код очень минимален по сравнению с тем, что вы измерение,
- вы что-то делаете с результатами, так что компилятор не может оптимизируйте весь код (не проблема в ваших тестах),
- вы выполняете код для измерения большого количества раз, беря среднее,
- вы измеряете время процессора, а не время настенных часов, и
- вы перед запуском программы убедитесь, что все готово измерения.
во многих случаях на современном оборудовании кэша являются ограничивающим фактором производительности для малых циклических конструкций. Рекурсивная реализация с меньшей вероятностью создает пропуски кэша на пути к инструкции.