Почему рекурсивная версия функции будет быстрее, чем итеративная в 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. Обратный порядок вы запускаете тесты, и результаты также будут обратными.

Если вы хотите получить значительную измерений, вы должны убедиться, что что

  1. только код, который вы хотите измерить в измерения, или, по крайней мере, дополнительный код очень минимален по сравнению с тем, что вы измерение,
  2. вы что-то делаете с результатами, так что компилятор не может оптимизируйте весь код (не проблема в ваших тестах),
  3. вы выполняете код для измерения большого количества раз, беря среднее,
  4. вы измеряете время процессора, а не время настенных часов, и
  5. вы перед запуском программы убедитесь, что все готово измерения.

во многих случаях на современном оборудовании кэша являются ограничивающим фактором производительности для малых циклических конструкций. Рекурсивная реализация с меньшей вероятностью создает пропуски кэша на пути к инструкции.