Kompiler C ++ mana, yang melakukan optimasi rekursi ekor?

149

Tampaknya bagi saya itu akan bekerja dengan sangat baik untuk melakukan optimasi rekursi ekor di C dan C ++, namun saat debugging saya sepertinya tidak pernah melihat frame stack yang menunjukkan optimasi ini. Itu agak bagus, karena tumpukan memberi tahu saya seberapa dalam rekursi itu. Namun, pengoptimalan juga akan menyenangkan.

Apakah ada kompiler C ++ yang melakukan optimasi ini? Mengapa? Kenapa tidak?

Bagaimana cara saya memberitahu kompiler untuk melakukannya?

  • Untuk MSVC: /O2atau/Ox
  • Untuk GCC: -O2atau-O3

Bagaimana kalau memeriksa apakah kompiler telah melakukan ini dalam kasus tertentu?

  • Untuk MSVC, aktifkan output PDB untuk dapat melacak kode, lalu periksa kodenya
  • Untuk GCC ..?

Saya masih akan mengambil saran untuk bagaimana menentukan apakah fungsi tertentu dioptimalkan seperti ini oleh kompiler (meskipun saya merasa meyakinkan bahwa Konrad menyuruh saya untuk menganggapnya)

Selalu mungkin untuk memeriksa apakah kompiler melakukan ini sama sekali dengan membuat rekursi tak terbatas dan memeriksa apakah itu menghasilkan infinite loop atau stack overflow (saya melakukan ini dengan GCC dan menemukan itu -O2sudah cukup), tapi saya ingin menjadi mampu memeriksa fungsi tertentu yang saya tahu akan berakhir pula. Saya ingin memiliki cara mudah untuk memeriksa ini :)


Setelah beberapa pengujian, saya menemukan bahwa destruktor merusak kemungkinan melakukan optimasi ini. Kadang-kadang layak untuk mengubah cakupan variabel dan temporal tertentu untuk memastikan mereka keluar dari ruang lingkup sebelum pernyataan pengembalian dimulai.

Jika ada destructor yang perlu dijalankan setelah tail-call, optimasi tail-call tidak dapat dilakukan.

Magnus Hoff
sumber

Jawaban:

128

Semua kompiler arus utama saat ini melakukan optimasi panggilan ekor dengan cukup baik (dan telah dilakukan selama lebih dari satu dekade), bahkan untuk panggilan rekursif bersama seperti:

int bar(int, int);

int foo(int n, int acc) {
    return (n == 0) ? acc : bar(n - 1, acc + 2);
}

int bar(int n, int acc) {
    return (n == 0) ? acc : foo(n - 1, acc + 1);
}

Membiarkan kompiler melakukan optimasi secara langsung: Aktifkan optimasi untuk kecepatan:

  • Untuk MSVC, gunakan /O2atau /Ox.
  • Untuk GCC, Dentang dan ICC, gunakan -O3

Cara mudah untuk memeriksa apakah kompiler melakukan optimasi adalah dengan melakukan panggilan yang jika tidak akan menghasilkan stack overflow - atau melihat output perakitan.

Sebagai catatan sejarah yang menarik, optimisasi panggilan ekor untuk C ditambahkan ke GCC dalam rangka tesis diploma oleh Mark Probst. Tesis ini menggambarkan beberapa peringatan yang menarik dalam implementasi. Itu layak dibaca.

Konrad Rudolph
sumber
ICC akan melakukannya, saya percaya. Sepengetahuan saya, ICC menghasilkan kode tercepat di pasar.
Paul Nathan
35
@ Paul Pertanyaannya adalah seberapa besar kecepatan kode ICC disebabkan oleh optimisasi algoritmik seperti optimisasi panggilan ekor dan seberapa banyak yang disebabkan oleh optimisasi cache dan microinstruction yang hanya dapat dilakukan oleh Intel, dengan pengetahuan mendalam tentang prosesor mereka sendiri, dapat melakukannya.
Imagist
6
gccmemiliki opsi yang lebih sempit -foptimize-sibling-callsuntuk "mengoptimalkan panggilan saudara dan panggilan rekursif". Pilihan ini (menurut gcc(1)halaman manual untuk versi 4.4, 4.7 dan 4.8 menargetkan berbagai platform) diaktifkan pada tingkat -O2, -O3, -Os.
FooF
Juga, berjalan dalam mode DEBUG tanpa secara eksplisit meminta optimasi TIDAK akan melakukan optimasi sama sekali. Anda dapat mengaktifkan PDB untuk mode rilis benar EXE dan mencoba melangkah melalui itu, tetapi perhatikan bahwa debugging dalam mode Rilis memiliki komplikasinya - variabel tidak terlihat / dilucuti, variabel gabungan, variabel menjadi di luar ruang lingkup dalam ruang lingkup tidak diketahui / tidak terduga, variabel yang tidak pernah masuk ruang lingkup dan menjadi konstanta sejati dengan alamat tingkat-tumpukan, dan - baik - Gabung atau tidak ada bingkai tumpukan. Biasanya frame stack yang digabung berarti callee disejajarkan, dan frame yang hilang / backmerged mungkin tail tail.
Петър Петров
21

gcc 4.3.2 sepenuhnya menguraikan fungsi ini ( atoi()implementasi jelek / sepele ) ke main(). Level optimisasi adalah -O1. Saya perhatikan jika saya bermain-main dengannya (bahkan mengubahnya dari staticmenjadi extern, rekursi ekor hilang cukup cepat, jadi saya tidak akan bergantung padanya untuk kebenaran program.

#include <stdio.h>
static int atoi(const char *str, int n)
{
    if (str == 0 || *str == 0)
        return n;
    return atoi(str+1, n*10 + *str-'0');
}
int main(int argc, char **argv)
{
    for (int i = 1; i != argc; ++i)
        printf("%s -> %d\n", argv[i], atoi(argv[i], 0));
    return 0;
}
Tom Barta
sumber
1
Anda dapat mengaktifkan optimasi tautan-waktu dan saya kira itu bahkan sebuah extern metode pun dapat diuraikan.
Konrad Rudolph
5
Aneh. Saya baru saja menguji gcc 4.2.3 (x86, Slackware 12.1) dan gcc 4.6.2 (AMD64, Debian wheezy) dan dengan-O1 tidak ada inlining dan tidak ada optimasi rekursi ekor . Anda harus menggunakannya -O2untuk itu (well, dalam 4.2.x, yang sudah agak kuno sekarang, masih tidak akan diuraikan). BTW Perlu juga ditambahkan bahwa gcc dapat mengoptimalkan rekursi bahkan ketika itu tidak sepenuhnya ekor (seperti faktorial tanpa akumulator).
przemoc
16

Seperti halnya yang jelas (kompiler tidak melakukan optimasi semacam ini kecuali Anda memintanya), ada kerumitan tentang optimasi panggilan ekor di C ++: destructors.

Diberikan sesuatu seperti:

   int fn(int j, int i)
   {
      if (i <= 0) return j;
      Funky cls(j,i);
      return fn(j, i-1);
   }

Compiler tidak dapat (secara umum) mengoptimalkan panggilan karena ini perlu memanggil destruktor cls setelah panggilan rekursif kembali.

Terkadang kompilator dapat melihat bahwa destruktor tidak memiliki efek samping yang terlihat secara eksternal (sehingga dapat dilakukan lebih awal), tetapi seringkali tidak.

Bentuk yang sangat umum dari ini adalah di mana Funkysebenarnya astd::vector atau serupa.

Martin Bonner mendukung Monica
sumber
Tidak bekerja untuk saya. Sistem memberi tahu saya bahwa suara saya dikunci sampai jawabannya diedit.
hmuelner
Baru saja mengedit jawabannya (menghilangkan paranthes) dan sekarang saya bisa membatalkan downvote saya.
hmuelner
11

Sebagian besar kompiler tidak melakukan optimasi dalam bentuk debug.

Jika menggunakan VC, coba versi rilis dengan dinyalakan info PDB - ini akan membuat Anda menelusuri aplikasi yang dioptimalkan dan semoga Anda akan melihat apa yang Anda inginkan. Perhatikan, bagaimanapun, bahwa debugging dan penelusuran build yang dioptimalkan akan membuat Anda berputar-putar di mana-mana, dan sering kali Anda tidak dapat memeriksa variabel secara langsung karena mereka hanya berakhir di register atau dioptimalkan sepenuhnya sepenuhnya. Ini pengalaman yang "menarik" ...

Greg Whitfield
sumber
2
coba gcc mengapa -g -O3 dan untuk mendapatkan opimizations dalam membangun debug. xlC memiliki perilaku yang sama.
g24l
Ketika Anda mengatakan "kebanyakan kompiler": koleksi kompiler apa yang Anda pertimbangkan? Seperti yang ditunjukkan ada setidaknya dua kompiler yang melakukan optimasi selama debug build - dan sejauh yang saya tahu VC juga melakukannya (kecuali jika Anda mengaktifkan modifikasi-dan-lanjutkan mungkin).
meroket
7

Seperti yang disebutkan Greg, kompiler tidak akan melakukannya dalam mode debug. Tidak apa-apa untuk debug build menjadi lebih lambat daripada build prod, tetapi mereka seharusnya tidak crash lebih sering: dan jika Anda bergantung pada optimisasi panggilan ekor, mereka mungkin melakukan hal itu. Karena itu, seringkali yang terbaik adalah menulis ulang panggilan ekor sebagai loop normal. :-(

0124816
sumber