web-dev-qa-db-fra.com

Pourquoi .NET / C # n'optimise-t-il pas pour la récursivité des appels de queue?

J'ai trouvé cette question sur quelles langues optimisent la récursivité de la queue. Pourquoi C # n'optimise pas la récursivité de la queue, dans la mesure du possible?

Pour un cas concret, pourquoi cette méthode n'est-elle pas optimisée dans une boucle ( Visual Studio 2008 32 bits, si cela importe)?:

private static void Foo(int i)
{
    if (i == 1000000)
        return;

    if (i % 100 == 0)
        Console.WriteLine(i);

    Foo(i+1);
}
100
ripper234

La compilation JIT est un exercice d'équilibre délicat entre ne pas passer trop de temps à faire la phase de compilation (ralentissant ainsi considérablement les applications de courte durée) et ne pas faire assez d'analyses pour maintenir l'application compétitive à long terme avec une compilation standard à l'avance .

Fait intéressant, les étapes de compilation NGen ne visent pas à être plus agressives dans leurs optimisations. Je soupçonne que c'est parce qu'ils ne veulent tout simplement pas avoir de bogues dont le comportement dépend du fait que JIT ou NGen était responsable du code machine.

Le CLR lui-même prend en charge l'optimisation des appels de queue, mais le compilateur spécifique au langage doit savoir comment générer le opcode et le JIT pertinents doit être disposé à le respecter. F # fsc générera les opcodes appropriés (bien que pour une récursivité simple, il peut simplement convertir le tout en une boucle while directement). Le csc de C # ne fonctionne pas.

Voir cet article de blog pour certains détails (très probablement maintenant obsolète compte tenu des récents changements JIT). Notez que le CLR change pour 4.0 les x86, x64 et ia64 le respecteront .

79
ShuggyCoUk

Cette soumission de commentaires Microsoft Connect devrait répondre à votre question. Il contient une réponse officielle de Microsoft, je vous recommande donc de vous en tenir à cela.

Merci pour la suggestion. Nous avons envisagé d'émettre des instructions d'appel de queue à un certain nombre de points dans le développement du compilateur C #. Cependant, il y a quelques problèmes subtils qui nous ont poussés à éviter cela jusqu'à présent: 1) Il y a en fait un coût indirect non négligeable à l'utilisation de l'instruction .tail dans le CLR (ce n'est pas seulement une instruction de saut car les appels de queue deviennent finalement dans de nombreux environnements moins stricts tels que les environnements d'exécution de langage fonctionnel où les appels de queue sont fortement optimisés). 2) Il existe peu de vraies méthodes C # où il serait légal d'émettre des appels de queue (d'autres langages encouragent les modèles de codage qui ont plus de récursivité de queue, et beaucoup qui dépendent fortement de l'optimisation des appels de queue font en fait une réécriture globale (comme les transformations de passage de continuation) ) pour augmenter le nombre de récursions de queue). 3) En partie à cause de 2), les cas où les méthodes C # dépassent la pile en raison d'une récursion profonde qui aurait dû réussir sont assez rares.

Cela dit, nous continuons à regarder cela, et nous pourrons peut-être dans une future version du compilateur trouver des modèles où il est logique d'émettre des instructions .tail.

Par ailleurs, comme cela a été souligné, il convient de noter que la récursivité de la queue est optimisée sur x64.

72
Noldorin

C # n'optimise pas la récursivité des appels de queue car c'est à cela que sert F #!

Pour plus d'informations sur les conditions qui empêchent le compilateur C # d'effectuer des optimisations d'appel de fin, consultez cet article: conditions d'appel de fin JIT CLR .

Interopérabilité entre C # et F #

C # et F # interagissent très bien, et parce que le Common Language Runtime .NET (CLR) est conçu avec cette interopérabilité à l'esprit, chaque langue est conçue avec des optimisations spécifiques à son intention et à ses objectifs. Pour un exemple qui montre à quel point il est facile d'appeler du code F # à partir du code C #, voir Appel du code F # à partir du code C # ; pour un exemple d'appel de fonctions C # à partir de code F #, voir Appel de fonctions C # à partir de F # .

Pour l'interopérabilité des délégués, consultez cet article: Interopérabilité des délégués entre F #, C # et Visual Basic .

Différences théoriques et pratiques entre C # et F #

Voici un article qui couvre certaines des différences et explique les différences de conception de la récursivité des appels de queue entre C # et F #: Génération de l'opcode Tail-Call en C # et F # .

Voici un article avec quelques exemples en C #, F # et C++\CLI: Adventures in Tail Recursion en C #, F # et C++\CLI

La principale différence théorique est que C # est conçu avec des boucles tandis que F # est conçu selon les principes du calcul Lambda. Pour un très bon livre sur les principes du calcul lambda, voir ce livre gratuit: Structure et interprétation des programmes informatiques, par Abelson, Sussman et Sussman .

Pour un très bon article d'introduction sur les appels de queue en F #, consultez cet article: Introduction détaillée aux appels de queue en F # . Enfin, voici un article qui couvre la différence entre la récursion non-queue et la récursivité appel-queue (en F #): récursion-queue vs récursion non-queue en F Sharp .

15
devinbost

On m'a récemment dit que le compilateur C # pour 64 bits optimise la récursivité de la queue.

C # implémente également cela. La raison pour laquelle elle n'est pas toujours appliquée, c'est que les règles utilisées pour appliquer la récursivité de la queue sont très strictes.

8
Alexandre Brisebois

Vous pouvez utiliser la technique du trampoline pour les fonctions récursives de queue en C # (ou Java). Cependant, la meilleure solution (si vous vous souciez seulement de l'utilisation de la pile) est d'utiliser la méthode ce petit assistant pour encapsuler des parties de la même fonction récursive et la rendre itérative tout en gardant la fonction lisible.

3
naiem