7
votes

Pourquoi une version récursive d'une fonction serait plus rapide qu'un itératif en C?

Je vérifie la différence entre deux implémentations de descente de gradient, ma supposition était qu'avec après l'optimisation du compilateur, les deux versions de l'algorithme seraient équivalentes.

Pour ma surprise, la version récursive était nettement plus rapide. Je n'ai pas rejeté un défaut réel sur aucune des versions ni même dans la façon dont je mesure le temps. Pouvez-vous me donner des idées s'il vous plaît? P>

Ceci est mon code: P>

Minimum : 0.000487
Minimum : 0.000487
time_iter:127 ms, time_rec:19 ms, ratio (dif1/dif2) :6.68421
End.


7 commentaires

Tout "pourquoi est-ce plus rapide?" Les questions doivent être accompagnées de la sortie du code de montage de votre compilateur.


Où est la déclaration de retour pour decgrad pour le cas où le si instruction est faux? Ce code ne devrait pas compiler.


@ user112358132134: Nah, il ne devrait que compiler avec un avertissement, non?


Je ne suis pas convaincu que c'est plus rapide. Vous devez parcourir les deux appels à plusieurs reprises à la moyenne sur la gigue. Vous devez supprimer Printf de la section référencée.


Que obtenez-vous si vous exécutez les tests de synchronisation dans l'autre ordre (premier Descgrad , puis Descgraditer )? Il pourrait être printf initialisation qui prend tout ce temps, comme l'implique Totowtwo.


Avec le renvoi manquant corrigé, un nombre décent d'itérations et printf retiré de la référence, les deux fois sont Très similaire .


@Tonyk a juste raison! Merci! Cela résout le mystère, la distorsion a été occasionnée par le printf. Maintenant, les deux implémentations sont à peu près la même qui a du sens! Merci!


7 Réponses :


3
votes

Pour une chose, votre étape récursive manque un retour : xxx

devrait être: xxx

Cette supervision entraîne la valeur de retour du Descgrad indéfini, de sorte que le compilateur doit à peine générer du code pour celui-ci;)


7 commentaires

Je viens de tester cette théorie et cela ne change pas l'observation de l'OP.


@ user112358132134: assez juste. Après avoir testé localement, je peux reproduire vos conclusions.


Je ne pense pas que ce soit nécessaire car la condition d'arrêt est "retour xnew" et les deux fonctions ont renvoyé la bonne valeur. De plus, j'ai à nouveau essayé avec le code que vous avez proposé et il n'y avait pas de changement sur la performance.


@Pedrom: Il est absolument essentiel d'avoir le là pour obtenir un programme correct. Cependant, il ne semble pas affecter le temps d'exécution, comme je l'ai déjà dit. S'il produit la sortie correcte, ce n'est que par hasard.


@Magnushoff Désolé pour le désaccord Magnus, mais le compilateur le résout pour moi ou que ce n'est pas nécessaire. Ce que je m'attend à ce que l'environnement de pile soit réutilisé sur chaque appel puis de retourner la valeur pour arrêter la récursivité puisque je ne pars pas d'opérations en attente. Cela pourrait être faux mais s'il vous plaît si ce n'est pas le cas pourquoi il fonctionne?


@Pedrom: Cela fonctionne probablement exactement comme si vous l'envisagez. Cependant, cela n'est pas mandaté par la norme linguistique. Selon la norme, le rendement manquant provoque "comportement indéfini": la norme ne se soucie pas de ce que le compilateur fait dans ce cas. Donc, il pourrait compiler pour rendre le programme que vous avez destiné (qu'il semble faire), ou il pourrait ne pas . Si vous souhaitez vous garantir que le programme fonctionne correctement à l'avenir, sur différents ordinateurs, sur différents compilateurs, mettez le retour de là. En tout cas, il ne devrait pas ralentir les choses;)


@Magnushoff merci pour la perspicacité :) et je pense que je pense que c'est une violation de la norme, c'était juste un code éloigné que je faisais et j'ai eu cette question au milieu ... aussi merci pour la réponse. :)



0
votes

Dans de nombreux cas, sur les misses de cache de matériel moderne sont le facteur limitant des performances des constructions de petite boucle. Une implémentation récursive est moins susceptible de créer des manque de cache sur le chemin d'instructions.


1 commentaires

Alors qu'une boucle serrée ne sera pas? Je n'ai pas acheté ça.



5
votes

est ma façon de mesurer le temps faux?

oui. Dans les épisodes courtes, vous mesurez, le planificateur peut avoir un impact massif sur votre programme. Vous devez soit rendre votre test beaucoup plus longtemps à la moyenne de telles différences, ou d'utiliser clock_process_cuTime_id pour mesurer le temps de la CPU utilisé par votre processus.


3 commentaires

Cela semble juste mais cela n'explique pas la différence significative entre eux? Les deux fois ne sont même pas proches


@Pedrom: Oui, mais un processus non invitionné n'obtienne aucun temps de processeur pendant 0,1 seconde est tout à fait possible. De plus, comme Totowtwo a noté, la déclaration printf introduit davantage de retard comme des tampons potentiellement non engagés, qui introduisent des ralentissements plus imprévisibles. Votre algorithme ne peut prendre que 10 ms pour les deux variantes et le reste est le bruit, il n'y a donc aucune signification dans la différence.


Yeap tu es ... Le printf faisait le bruit.



10
votes

J'ai compilé et exécuté votre code localement. Déplacer le printf code> à l'extérieur du bloc chronométré permet aux deux versions exécutées dans ~ 5ms à chaque fois.

Une erreur centrale dans votre timing est que vous mesurez la bête complexe de printf code > Et son runtime narfs le code que vous essayez de mesurer. p>

mon Main () code> -function ressemble maintenant à ceci: p>

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");
}


4 commentaires

Merci .. cela fait de la tonne de sens!


Cette réponse est incorrecte. Je ne peux pas reproduire ses résultats, en particulier sur plusieurs itérations de fonction.


@ user112358132134 Qu'est-ce que vous ne pouviez pas reproduire exactement? Je pourrais en effet confirmer que ces imprimés ralentissaient l'exécution de la première fonction. Si vous commencez à la fois que le temps d'exécution de Printf doit être proche pour les deux fonctions.


J'ai posté une réponse. En bref, la différence d'exécution est le résultat de l'optimisation des appels queue de -O3.



2
votes

Pour commencer, vous incluiez une Printf à l'époque que vous essayiez de mesurer. C'est toujours un non-nul géant, car il peut, et très probablement, suspendre votre processus en effectuant la sortie de la console. En fait, tout appel système peut complètement lancer des mesures de temps comme celles-ci.

Et deuxièmement, comme quelqu'un d'autre l'a mentionné, à ce court-circonscription, des interruptions de planificateur peuvent avoir un impact énorme.

Ceci est Pas parfait, mais essayez ceci pour votre principal et vous verrez qu'il y a vraiment très peu de différence. Lorsque vous augmentez le nombre de boucles, le rapport s'approche 1.0. xxx

}

EDIT: Après avoir examiné la sortie démontée en utilisant objdump -ds j'ai remarqué quelques choses:

Avec l'optimisation -O3, le code ci-dessus optimise complètement l'appel de la fonction. Cependant, il produit toujours du code pour les deux fonctions et n'est pas non plus récursif.

Deuxièmement, avec -O0, de telle sorte que le code résultant est réellement récursif, la version récursive est littéralement un billion de billion < / fort> fois plus lentement. Je suppose que la pile d'appel force les variables pour se retrouver dans la mémoire où la version itérative s'étend de registres et / ou de cache.


1 commentaires

Merci pour la perspicacité. J'ai essayé avec -O0 auparavant, mais GCC ne génère pas le code que je m'attendais. Je m'attendais à ce qu'il fonctionnait comme un système où si vous n'avez pas d'opérations en attente, cela réutiliserait l'environnement. En fait, avec -O0 il n'y a pas de moyen d'avoir une fonction de récursivité qui compte pour toujours, elle se terminera toujours par un débordement de pile (défaut de segmentation dans l'affaire CGC).



1
votes

Premier, clock_gettime semble être la mesure de l'horloge murale, pas temps d'exécution. Deuxièmement, le temps réel que vous mesurez est le Heure d'exécution de printf , pas l'heure d'exécution de votre fonction. Et troisièmement, la première fois que vous appelez printf , ce n'est pas en mémoire, alors il doit être paginé, impliquant un disque important IO. Inverse de l'ordre Vous exécutez les tests et les résultats seront également inverse.

Si vous voulez obtenir des mesures significatives, vous devez vous assurer que que

  1. Seul le code que vous souhaitez mesurer est dans les boucles de mesure, ou au moins, le code supplémentaire est très minimum par rapport à ce que vous êtes mesurer,
  2. Vous faites quelque chose avec les résultats, de sorte que le compilateur ne puisse pas Optimiser tout le code (pas un problème dans vos tests),
  3. Vous exécutez le code à mesurer un grand nombre de fois, prenant la moyenne,
  4. Vous mesurez l'heure du processeur, et pas l'heure de l'horloge murale et
  5. Vous vous assurez que tout est paginé avant de commencer la mesures.

0 commentaires

2
votes

La réponse acceptée est incorrecte forte>.

Il y a une différence dans les courses de la fonction itérative et la fonction récursive et la raison est l'optimisation du compilateur -Foptimize-appels de sodibling code> ajouté par -O3 code>. p>

Tout d'abord, le code: p> xxx pré>

Les messages précédents étaient corrects que vous devez itération à plusieurs reprises afin d'éviter les interférences d'environnement à l'extérieur. Étant donné que j'ai rejeté votre fonction de différenciation en faveur de Time.h code> La fonction code> la fonction code> sur TIME_T CODE> Data, depuis sur de nombreuses itérations, quelque chose de plus fin. qu'une seconde n'a pas de sens. De plus, j'ai supprimé les Printfs dans la référence. P>

J'ai également corrigé un bogue dans la mise en œuvre récursive. Votre déclaration IF-FABS FABS (XNew-XO) de votre code d'origine , qui est incorrecte (ou au moins différente de la mise en œuvre itérative). Les boucles itératives tandis que FABS ()> la précision, la fonction récursive ne doit donc pas recueillir lorsque FABS précision. Ajouter des compteurs de «itération» aux deux fonctions confirme que ce correctif rend la fonction logiquement équivalente. P>

compilation et exécution avec -O3 code>: p>

$ 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


5 commentaires

Merci! C'est une analyse très complète du code .. Réellement plus détaillé que prévu. Cependant, je ne comprends pas ce résultat: Time_iter: 34 s, Time_Rec: 0 s, Ratio (ITER / REC): INF
Il semble que la fonction ne donne pas la bonne réponse car elle semble vraiment suspecte qu'après 10000000 Itérations Cela ne durerait pas au moins 1s tandis que l'autre est 34. Je ne sais pas seulement sonne bizarre ...


Et je ne pouvais pas le reproduire ... Je copie et collé votre code et j'ai compilé avec -O3 et j'ai toujours reçu le ratio 1.


C'est peut-être aussi une question d'environnement. Qu'avez-vous testé? J'ai testé sur Rhel5 avec 5 CPU et 34 Go de mémoire. Cela a peut-être eu un impact sur mes performances améliorées, bien que tout ce qui devrait faire est d'exagérer les avantages d'optimisation. Je me méfiais aussi, alors je définissais deux variables statiques à 0 et ++ 'd eux sur chaque recur / interface et les comptes résultants étaient les mêmes (1755032704, si vous souhaitez vérifier; assurez-vous de vérifier; assurez-vous de vérifier corrigez le <= bug). L'incrémentation de la variable statique détruit le gain d'optimisation de la queue: Time_iter: 34 s, Time_Rec: 24 s, ratio (ITER / REC): 1.416667


Il semble certainement une question d'environnement. J'ai testé sur 2 pc à la fois I5 Core et les résultats n'ont pas changé. De vos résultats ressemblent à votre code est en quelque sorte exécuté en parallèle. Est-ce que c'est possible?


Pourquoi exploiterait la récursion de la queue rendre la fonction récursive plus rapidement que celle itérative? La version itérative réutilise les variables comme l'appel récursif de la queue. Pour référence, je reçois également des résultats identiques pour les deux fonctions à l'aide de GCC 4.6.1 sur Ubuntu 11.10 Compilation avec -O3 .