Saya tertarik pada kompleksitas waktu dari kompiler. Jelas ini adalah pertanyaan yang sangat rumit karena ada banyak kompiler, opsi kompiler dan variabel yang perlu dipertimbangkan. Secara khusus, saya tertarik pada LLVM tetapi akan tertarik pada pemikiran orang atau tempat untuk memulai penelitian. Google yang cukup tampaknya membawa sedikit ke cahaya.
Dugaan saya adalah bahwa ada beberapa langkah optimasi yang eksponensial, tetapi hanya berdampak kecil pada waktu aktual. Misalnya, eksponensial berdasarkan angka adalah argumen dari suatu fungsi.
Dari atas kepala saya, saya akan mengatakan bahwa menghasilkan pohon AST akan linier. Generasi IR akan membutuhkan melangkah melalui pohon sambil mencari nilai dalam tabel yang terus tumbuh, jadi atau . Pembuatan dan penautan kode akan menjadi jenis operasi yang serupa. Karena itu, tebakan saya adalah, jika kita menghapus variabel eksponensial yang tidak tumbuh secara realistis.
Tapi aku bisa salah total. Apakah ada yang punya pemikiran tentang itu?
Jawaban:
Buku terbaik untuk menjawab pertanyaan Anda mungkin adalah: Cooper dan Torczon, "Engineering a Compiler," 2003. Jika Anda memiliki akses ke perpustakaan universitas, Anda harus dapat meminjam salinannya.
Dalam kompiler produksi seperti llvm atau gcc, para desainer berusaha keras untuk menjaga semua algoritma di bawah iniO ( n2) dimana n adalah ukuran input. Untuk beberapa analisis untuk fase "optimisasi" ini berarti Anda perlu menggunakan heuristik daripada menghasilkan kode yang benar-benar optimal.
Lexer adalah mesin negara yang terbatas, jadiO ( n ) dalam ukuran input (dalam karakter) dan menghasilkan aliran O ( n ) token yang diteruskan ke parser.
Untuk banyak kompiler untuk banyak bahasa parser adalah LALR (1) dan dengan demikian memproses token stream dalam waktuO ( n ) dalam jumlah token input. Selama parsing Anda biasanya harus melacak tabel simbol, tetapi, untuk banyak bahasa, itu dapat ditangani dengan tumpukan tabel hash ("kamus"). Setiap akses kamus adalahO ( 1 ) , tetapi Anda kadang-kadang harus berjalan di tumpukan untuk mencari simbol. Kedalaman tumpukan adalahO ( s ) dimana s adalah kedalaman ruang lingkup. (Jadi dalam bahasa mirip-C, berapa banyak lapisan kurung kurawal yang ada di dalam Anda.)
Kemudian pohon parse biasanya "diratakan" ke dalam grafik aliran kontrol. Node dari grafik aliran kontrol mungkin berupa instruksi 3-alamat (mirip dengan bahasa rakitan RISC), dan ukuran grafik aliran kontrol biasanya akan linier dalam ukuran pohon parse.
Kemudian serangkaian langkah-langkah eliminasi redundansi biasanya diterapkan (eliminasi subekspresi umum, gerakan kode invarian loop, propagasi konstan, ...). (Ini sering disebut "optimisasi" walaupun jarang ada yang optimal tentang hasilnya, tujuan sebenarnya adalah untuk meningkatkan kode sebanyak mungkin dalam batasan waktu dan ruang yang telah kita tempatkan pada kompiler.) Setiap langkah eliminasi redundansi akan biasanya memerlukan bukti dari beberapa fakta tentang grafik aliran kontrol. Bukti-bukti ini biasanya dilakukan dengan menggunakan analisis aliran data . Sebagian besar analisis aliran data dirancang sehingga akan menyatuO ( d) melewati grafik aliran di mana d adalah (secara kasar) kedalaman sarang loop dan melewati grafik aliran membutuhkan waktu O ( n ) dimana n adalah jumlah instruksi 3-alamat.
Untuk optimasi yang lebih canggih, Anda mungkin ingin melakukan analisis yang lebih canggih. Pada titik ini Anda mulai mengalami pengorbanan. Anda ingin algoritma analisis Anda mengambil kurang dariO ( n2) waktu dalam ukuran diagram alir seluruh program, tetapi ini berarti Anda perlu melakukannya tanpa informasi (dan program meningkatkan transformasi) yang mungkin mahal untuk dibuktikan. Contoh klasik dari hal ini adalah analisis alias, di mana untuk pasangan memori menulis Anda ingin membuktikan bahwa keduanya menulis tidak pernah dapat menargetkan lokasi memori yang sama. (Anda mungkin ingin melakukan analisis alias untuk melihat apakah Anda dapat memindahkan satu instruksi di atas yang lain.) Tetapi untuk mendapatkan informasi yang akurat tentang alias, Anda mungkin perlu menganalisis setiap jalur kontrol yang mungkin melalui program, yang eksponensial dalam jumlah cabang. dalam program (dan dengan demikian eksponensial dalam jumlah node dalam grafik aliran kontrol.)
Selanjutnya Anda masuk ke alokasi register. Alokasi register dapat dirumuskan sebagai masalah pewarnaan grafik, dan pewarnaan grafik dengan jumlah warna minimal dikenal sebagai NP-Hard. Jadi kebanyakan kompiler menggunakan semacam heuristik serakah dikombinasikan dengan tumpahan register dengan tujuan mengurangi jumlah tumpahan register sebaik mungkin dalam batas waktu yang wajar.
Akhirnya Anda masuk ke pembuatan kode. Pembuatan kode biasanya dilakukan maksimal-blok dasar pada saat di mana blok dasar adalah seperangkat node grafik aliran kontrol terhubung linear dengan entri tunggal dan keluar tunggal. Ini dapat dirumuskan ulang sebagai grafik yang mencakup masalah di mana grafik yang Anda coba untuk tutupi adalah grafik ketergantungan dari set instruksi 3-alamat di blok dasar, dan Anda mencoba untuk menutup dengan satu set grafik yang mewakili mesin yang tersedia instruksi. Masalah ini bersifat eksponensial dalam ukuran blok dasar terbesar (yang pada prinsipnya dapat menjadi urutan yang sama dengan ukuran seluruh program), jadi ini lagi biasanya dilakukan dengan heuristik di mana hanya sebagian kecil dari penutup yang mungkin. diperiksa.
sumber
Sebenarnya, beberapa bahasa (seperti C ++, Lisp, dan D) adalah Turing-complete pada waktu kompilasi, jadi kompilasi mereka tidak dapat diputuskan secara umum. Untuk C ++, ini karena instantiasi templat rekursif. Untuk Lisp dan D, Anda dapat mengeksekusi hampir semua kode pada waktu kompilasi, sehingga Anda dapat membuang kompilator ke dalam infinite loop jika Anda mau.
sumber
Dari pengalaman saya yang sebenarnya dengan kompiler C #, saya dapat mengatakan bahwa untuk program tertentu ukuran biner keluaran tumbuh secara eksponensial sehubungan dengan ukuran sumber input (ini sebenarnya diperlukan oleh spesifikasi C # dan tidak dapat dikurangi), jadi kompleksitas waktu harus setidaknya eksponensial juga.
Tugas resolusi kelebihan beban umum di C # dikenal sebagai NP-hard (dan kompleksitas implementasi aktual setidaknya eksponensial).
Pemrosesan komentar dokumentasi XML dalam sumber C # juga memerlukan evaluasi ekspresi XPath 1.0 yang sewenang-wenang pada waktu kompilasi, yang juga eksponensial, AFAIK.
sumber
class X<A,B,C,D,E> { class Y : X<Y,Y,Y,Y,Y> { Y.Y.Y.Y.Y.Y.Y.Y.Y y; } }
Ukurlah dengan basis kode yang realistis, seperti seperangkat proyek sumber terbuka. Jika Anda memplot hasilnya sebagai (codeSize, finishTime), maka Anda dapat memplot grafik tersebut. Jika data Anda f (x) = y adalah O (n), maka memplot g = f (x) / x akan memberi Anda garis lurus setelah data mulai menjadi besar.
Plot f (x) / x, f (x) / lg (x), f (x) / (x * lg (x)), f (x) / (x * x), dll. Grafik akan menyelam off ke nol, naikkan tanpa batas, atau ratakan. Gagasan ini berguna untuk situasi seperti mengukur waktu memasukkan mulai dari database kosong (yaitu: untuk mencari 'kebocoran kinerja' dalam jangka waktu yang lama.).
sumber