web-dev-qa-db-fra.com

Comment puis-je vérifier si GCC effectue une optimisation de la récupération de la queue?

Comment puis-je dire si GCC (plus spécifiquement, g ++) optimise la récursion de la queue dans une fonction particulière? (Parce que c'est mixte plusieurs fois: je ne veux pas tester si GCC peut optimiser la récursion de la queue en général. Je veux savoir s'il optimise My Fonction récursive.)

Si votre réponse est "Regardez l'assembleur généré", j'aimerais savoir exactement ce que je cherche et si je pouvais ou non écrire un programme simple qui examine l'assembleur pour voir s'il y a une optimisation.

Ps. Je sais que cela apparaît dans le cadre de la question qui, le cas échéant, Compilateurs C++ effectue une optimisation de la récupération de la queue? À partir de 5 mois. Cependant, je ne pense pas cette partie de cette question a été répondu de manière satisfaisante. (La réponse Il y avait "le moyen le plus simple de vérifier si le compilateur a fait l'optimisation (que je sache) est d'effectuer un appel qui entraînerait un débordement d'une pile - ou de regarder la sortie de l'assemblage.")

59
A. Rex

Utilisez le code exemple de l'autre question . Compilez-le, mais dites à GCC de ne pas assembler:

[.____] GCC -STD = C99 -S -O2 Test.c [.____]

Voyons maintenant la fonction _atoi dans le fichier TEST.S (GCC 4.0.1 sur Mac OS 10.5):

        .text
        .align 4,0x90
_atoi:
        pushl   %ebp
        testl   %eax, %eax
        movl    %esp, %ebp
        movl    %eax, %ecx
        je      L3
        .align 4,0x90
L5:
        movzbl  (%ecx), %eax
        testb   %al, %al
        je      L3
        leal    (%edx,%edx,4), %edx
        movsbl  %al,%eax
        incl    %ecx
        leal    -48(%eax,%edx,2), %edx
        jne     L5
        .align 4,0x90
L3:
        leave
        movl    %edx, %eax
        ret

Le compilateur a effectué une optimisation des appels de queue sur cette fonction. Nous pouvons dire parce qu'il n'y a pas d'instruction call dans ce code, alors que le code C d'origine C avait clairement un appel de fonction. En outre, nous pouvons voir l'instruction jne L5, qui saute en arrière dans la fonction, indiquant une boucle lorsqu'il n'y avait clairement aucune boucle dans le code C. Si vous recompilez avec l'optimisation éteinte, vous verrez une ligne indiquant call _atoi, et vous ne verrez pas non plus de sauts en arrière.

Que vous puissiez l'automatiser, c'est une autre affaire. Les spécificités du code de l'assembleur dépendront du code que vous compilez.

Vous pourriez le découvrir par programme, je pense. Faire la fonction Imprimer la valeur actuelle du pointeur de pile (enregistrez ESP sur x86). Si la fonction imprime la même valeur pour le premier appel possible pour l'appel récursif, le compilateur a effectué l'optimisation de la queue. Cette idée nécessite de modifier la fonction que vous espérez observer, cependant, et cela pourrait affecter la manière dont le compilateur choisit d'optimiser la fonction. Si le test réussit (imprime la même valeur ESP fois), je pense qu'il est raisonnable de supposer que l'optimisation serait également effectuée sans votre instrumentation, mais si le test échoue, nous ne saurions pas si l'échec était dû à l'ajout du code d'instrumentation.

62
Rob Kennedy

ÉDITER Mon message original a également empêché GCC de faire des éliminations d'appel de la queue. J'ai ajouté des difficultés supplémentaires en dessous de ce imbécile GCC à faire une élimination de l'appel de la queue de toute façon.

Élargir sur la réponse de Steven, vous pouvez vérifier par programme à voir si vous avez le même cadre de pile:

#include <stdio.h>

// We need to get a reference to the stack without spooking GCC into turning
// off tail-call elimination
int Oracle2(void) { 
    char Oracle; int Oracle2 = (int)&Oracle; return Oracle2; 
}

void myCoolFunction(params, ..., int tailRecursionCheck) {
    int Oracle = Oracle2();
    if( tailRecursionCheck && tailRecursionCheck != Oracle ) {
        printf("GCC did not optimize this call.\n");
    }
    // ... more code ...
    // The return is significant... GCC won't eliminate the call otherwise
    return myCoolFunction( ..., Oracle);
}

int main(int argc, char *argv[]) {
    myCoolFunction(..., 0);
    return 0;
}

Lorsque vous appelez la fonction non récursive, passez dans 0 le paramètre de contrôle. Sinon, passez à Oracle. Si un appel récursif de queue qui aurait dû être éliminé n'était pas, alors vous serez informé au moment de l'exécution.

Lorsque vous testez cela, il semble que ma version de GCC n'oblise pas l'appel de la première queue, mais les appels de queue restants sont optimisés. Intéressant.

11
Paul

Regardez le code de montage généré et voyez s'il utilise un call ou jmp instructions pour l'appel récursif sur x86 (pour d'autres architectures, recherchez les instructions correspondantes). Vous pouvez utiliser nm et objdump pour obtenir uniquement l'assemblage correspondant à votre fonction. Considérez la fonction suivante:

int fact(int n)
{
  return n <= 1 ? 1 : n * fact(n-1);
}

Compiler comme

gcc fact.c -c -o fact.o -O2

Ensuite, pour tester s'il utilise la récursion de la queue:

# get starting address and size of function fact from nm
ADDR=$(nm --print-size --radix=d fact.o | grep ' fact$' | cut -d ' ' -f 1,2)
# strip leading 0's to avoid being interpreted by objdump as octal addresses
STARTADDR=$(echo $ADDR | cut -d ' ' -f 1 | sed 's/^0*\(.\)/\1/')
SIZE=$(echo $ADDR | cut -d ' ' -f 2 | sed 's/^0*//')
STOPADDR=$(( $STARTADDR + $SIZE ))

# now disassemble the function and look for an instruction of the form
# call addr <fact+offset>
if objdump --disassemble fact.o --start-address=$STARTADDR --stop-address=$STOPADDR | \
    grep -qE 'call +[0-9a-f]+ <fact\+'
then
    echo "fact is NOT tail recursive"
else
    echo "fact is tail recursive"
fi

Lorsque vous avez dirigé sur la fonction ci-dessus, ce script imprime "Le fait est récursif de la queue". Lorsque vous avez compilé avec -O3 Au lieu de -O2, Cela imprime curieusement "le fait n'est pas récursif de queue".

Notez que cela pourrait générer de faux négatifs, comme le soulignait Ehemient dans son commentaire. Ce script ne donnera que la bonne réponse si la fonction ne contient aucun appels récursifs sur lui-même, et il ne détecte pas non plus la récursion de la frère de soeur (par exemple, où _ A() appelle B() qui appelle A()). Je ne peux pas penser à une méthode plus robuste pour le moment qui n'implique pas avoir un regard humain sur l'assemblage généré, mais au moins vous pouvez utiliser ce script pour accroître facilement l'ensemble correspondant à une fonction particulière dans un fichier d'objet.

7
Adam Rosenfield

Développer sur la réponse de PolyThinker, voici un exemple concret.

int foo(int a, int b) {
    if (a && b)
        return foo(a - 1, b - 1);
    return a + b;
}

i686-pc-linux-gnu-gcc-4.3.2 -Os -fno-optimize-sibling-calls sortir:

00000000 <FOO>: 
 0: 55 Push% EBP [.____] 1: 89 E5 MOV% ESP,% EBP [.____] 3: 8B 55 08 MOV 0x8 (% EBP),% EDX [.____] 6: 8B 45 0C MOV 0xc (% EBP),% EAX [.____] 9: 85 D2 Test% EDX,% EDX [.____] B: 74 16 JE 23 <FOO + 0X23> [.____] D: 85 C0 Test% EAX,% EAX [.____] F: 74 12 JE 23 <FOO + 0x23> 
 11: 51 Poussez% ecx 
 12: 48 Dec% EAx [.____] 13: 51 Poussez% ecx [.____] 14: 50 Poussez% EAX [.____] 15: 8D 42 FF LEA -0X1 (% EDX),% EAX [.____] 18: 50 Poussez% EAX [.____] 19: E8 FC FF FF FF Appelez 1A <FOO + 0x1a> [.____] 1e: 83 C4 10 Ajouter $ 0x10,% ESP [.____] 21: EB 02 JMP 25 <FOO + 0X25> 
 23: 01 D0 Ajouter% EDX,% EAX [.____] 25: C9 quitter [.____] 26: C3 RET [.____]

i686-pc-linux-gnu-gcc-4.3.2 -Os sortir:

00000000 <FOO>: 
 0: 55 Push% EBP [.____] 1: 89 E5 MOV% ESP,% EBP [.____] 3: 8B 55 08 MOV 0x8 (% EBP),% EDX [.____] 6: 8b 45 0C MOV 0xc (% EBP),% EAX [.____] 9: 85 D2 Test% EDX,% EDX [.____] B: 74 08 JE 15 <FOO + 0X15> [.____] D: 85 C0 Test% EAX,% EAX [.____] F: 74 04 JE 15 <FOO + 0X15> 
 11: 48 Dec% EAX 
 12: 4a Dec% EDX 
 13: EB F4 JMP 9 <FOO + 0X9> [.____] 15: 5D Pop% EBP [.____] 16: 01 D0 Add% EDX,% EAX [.____] 18: C3 RET 

Dans le premier cas, <foo+0x11>-<foo+0x1d> pousse les arguments d'un appel de fonction, tandis que dans le second cas, <foo+0x11>-<foo+0x14> modifie les variables et jmps à la même fonction, quelque part après le préambule. C'est ce que vous voulez rechercher.

Je ne pense pas que vous puissiez le faire de manière programmable; Il y a trop de variation possible. La "viande" de la fonction peut être plus proche ou plus loin du début, et vous ne pouvez pas distinguer que jmp d'une boucle ou d'une conditionnelle sans le regarder. Ce pourrait être un saut conditionnel au lieu d'un jmp. gcc peut laisser un call dans certains cas, mais appliquer une optimisation des appels de frère de sœur dans d'autres cas.

Les "appels de sodiblon" de la GCC sont légèrement plus généraux que les appels récursifs de la queue - efficacement, tout appel de fonction où réutiliser le même cadre de pile est correct, c'est d'accord potentiellement un appel de fromage.

[Éditer]

Comme exemple de quand je cherche juste une auto-récursive call vous trompera en erreur,

int bar(int n) {
    if (n == 0)
        return bar(bar(1));
    if (n % 2)
        return n;
    return bar(n / 2);
}

GCC appliquera une optimisation des appels de soeur à deux appels de trois bar. J'appellerais toujours l'optimisation de l'appel de la queue, puisque cet appel unique non optimisé ne va jamais plus loin qu'un seul niveau, même si vous trouverez un call <bar+..> dans l'assemblage généré.

6
ephemient

je suis beaucoup trop paresseux pour regarder un démontage. Essaye ça:

void so(long l)
{
    ++l;
    so(l);
}
int main(int argc, char ** argv)
{
    so(0);
    return 0;
}

compiler et exécuter ce programme. Si cela fonctionne pour toujours, la récursion de la queue a été optimisée. Si ça souffle la pile, ce n'était pas.

EDIT: Désolé, lisez trop vite, l'OP veut savoir si sa fonction particulière a une récursion de la queue optimisée. D'ACCORD...

... Le principe est toujours le même - si la récursion de la queue est optimisée, alors le cadre de pile restera le même. Vous devriez être capable d'utiliser la fonction de backtrace pour capturer les cadres de pile de votre fonction et déterminer si elles augmentent ou non. Si la récursion de la queue est optimisée, vous aurez un seul pointeur de retour dans le tampon .

3
Steven A. Lowe

Une autre façon que j'ai vérifié c'est:

  1. Compilez votre code avec 'GCC -O2'
  2. démarrer 'gdb'
  3. Placez un point d'arrêt dans la fonction que vous attendez d'être une récursion de la queue optimisée/éliminée
  4. exécutez votre code
  5. Si l'appel de la queue a été éliminé, le point d'arrêt ne sera frappé qu'une seule fois. Pour plus de choses sur ce point de vue this
2
user59634

Une méthode simple: construire un programme de récursions de queue simple, le compilez et le dissimuler pour voir s'il est optimisé.

Je viens de réaliser que vous aviez déjà cela dans votre question. Si vous savez comment lire l'assemblage, c'est assez facile à dire. Les fonctions récursives s'appelleront eux-mêmes (avec "l'étiquette d'appel") de l'organe de fonction et une boucle sera juste "étiquette JMP".

1
PolyThinker

Vous pouvez créer des données d'entrée qui entraîneraient un débordement de pile en raison de la récursion trop profonde de cette fonction d'appels s'il n'y avait aucune optimisation et voyez si cela se produit. Bien sûr, ce n'est pas trivial et parfois d'autres entrées suffisantes rendront la fonction courir pendant intolérablement longue période.

0
sharptooth