2017-10-25 72 views
6

Eksperymentowanie z optymalizacji połączeń ogon (TCO), natknąłem się na następujący ciekawy przykład:Dlaczego gcc przeprowadza optymalizację połączeń końcowych dla jednej wersji, ale nie dla drugiej?

unsigned long long int fac1(unsigned long long int n){ 
    if (n==0) 
    return 1; 
    return n*fac1(n-1); 
} 

faktycznie, byłem pod wrażeniem, że gcc was able wykonać TCO tutaj (z -O2 flagą), ponieważ nie jest to, że prosta:

fac1(unsigned long long): 
     testq %rdi, %rdi 
     movl $1, %eax 
     je  .L4 
.L3: 
     imulq %rdi, %rax 
     subq $1, %rdi 
     jne  .L3 
     rep ret 
.L4: 
     rep ret 

jednak po zmianie rodzaju powrotnej z unsigned long long int do unsigned int gcc nie był w stanie wykonać tlo:

unsigned int fac2(unsigned long long int n){ 
    if (n==0) 
    return 1; 
    return n*fac2(n-1); 
} 

możemy jasno zobaczyć wywołanie rekurencyjne w resulting assembly:

fac2(unsigned long long): 
     testq %rdi, %rdi 
     jne  .L16 
     movl $1, %eax 
     ret 
.L16: 
     pushq %rbx 
     movq %rdi, %rbx 
     leaq -1(%rdi), %rdi 
     call fac2(unsigned long long) 
     imull %ebx, %eax 
     popq %rbx 
     ret 

W pierwszej chwili odrzucił to jako straconą optymalizacji, ale teraz nie jestem pewien, bo clang isn't able aby wykonać tę optymalizację, a także . Może więc są subtelności języka, którego nie jestem świadomy, które uniemożliwiają tę optymalizację.

Dlaczego gcc nie przeprowadza optymalizacji wywołań końcowych dla funkcji fac2, ale tylko dla fac1?

Jest dla mnie jasne, że w drugiej wersji wynik musi być zaniżony. Oczywiście to jedyna różnica. Ale dlaczego miałby to być problem i zapobiegać Tlo?

Na przykład, jeśli mogę pomóc kompilator i przepisać mojej funkcji jako klasyczny ogon-rekursji (który powinien produkować identyczne wyniki do wersji fac2):

unsigned int tlo_fac(unsigned long long int n, unsigned long long int cur){ 
    if (n==0) 
    return cur; 
    return tlo_fac(n-1, n*cur); 
} 

unsigned int fac(unsigned long long int n){ 
    return tlo_fac(n,1); 
} 

dostaję tlo zoptymalizowanej wersji, która jest identical to fac1 (wysoka 32bit są allowed to contain garbage, więc imulq mogą być stosowane po inline):

fac(unsigned long long): 
     testq %rdi, %rdi 
     movl $1, %eax 
     je  .L10 
.L11: 
     imulq %rdi, %rax 
     subq $1, %rdi 
     jne  .L11 
.L10: 
     rep ret 
+2

może być fakt, że odlewa się konieczne w 'powrotu n * fac2 (n-1);' po wywołaniu 'fact2 (n-1) ' –

+0

@GauravSehgal, ale dlaczego ten rzutowałby zapobiec Tlo? – ead

+0

Ponieważ rzut zostanie wykonany po przeanalizowaniu głębokości rekursji. Jeśli zmienisz parametr na "fact2 (unisnged int n)", otrzymasz optymalizację. –

Odpowiedz

2

W fact2() po zakończeniu rekurencji odlew będą potrzebne fr OM unsigned long long int do unsigned int

unsigned int fac2(unsigned int n) wytwarza poniżej montażu

fac2(unsigned int): 
     testl %edi, %edi 
     movl $1, %eax 
     je  .L10 
.L9: 
     imull %edi, %eax 
     subl $1, %edi 
     jne  .L9 
     rep ret 
.L10: 
     rep ret 
+2

To może zmylić GCC, ale nie * zasadniczo * zapobiega przepisywaniu go do iteracji – harold

+0

Jestem świadomy, że wymagana jest obsada, a także, że zmiana typu parametru wejściowego na typ wyjściowy doprowadziłaby do wersji uproszczonej. Ale nie jest dla mnie jasne, dlaczego to zapobiegnie TLO. – ead

+1

@ead Aby przeprowadzić optymalizację wywołań końcowych, wywołanie rekursywne powinno być ostatnią rzeczą do zrobienia z funkcji, 'return n * fac1 (n-1);', ostatnią rzeczą nie jest wywołanie funkcji 'fact (n- 1) ', ale rzucanie wyniku. –