Membandingkan proyek yang sama di Rust, Haskell, C ++, Python, Scala, dan OCaml

Pada semester terakhir universitas, saya memilih kursus kompiler CS444 . Di sana, setiap kelompok yang terdiri dari 1-3 orang harus menulis kompiler dari subset substansial Jawa pada x86. Bahasa untuk memilih grup. Ini adalah kesempatan langka untuk membandingkan implementasi program besar dengan fungsi yang sama, ditulis oleh programmer yang sangat kompeten dalam berbagai bahasa, dan untuk membandingkan perbedaan dalam desain dan pilihan bahasa. Perbandingan semacam itu memunculkan banyak pemikiran menarik. Perbandingan bahasa yang dikontrol seperti itu jarang terlihat. Ini tidak sempurna, tetapi jauh lebih baik daripada kebanyakan cerita subjektif yang menjadi dasar opini orang tentang bahasa pemrograman.

Kami membuat kompiler Rust kami, dan pertama saya membandingkannya dengan proyek tim Haskell. Saya berharap program mereka jauh lebih pendek, tetapi ternyata ukurannya sama atau lebih besar. Hal yang sama berlaku untuk OCaml. Kemudian saya membandingkannya dengan kompiler C ++, dan di sana cukup diharapkan bahwa kompiler sekitar 30% lebih besar, terutama karena header, kurangnya jumlah jumlah dan pencocokan pola. Perbandingan berikut adalah dengan pacar saya, yang membuat kompiler sendiri di Python dan menggunakan kurang dari setengah kode dibandingkan dengan kami, karena kekuatan metaprogramming dan tipe dinamis. Teman lain memiliki program Scala yang lebih kecil. Yang paling mengejutkan saya adalah perbandingan dengan tim lain yang juga menggunakan Rust, tetapi mereka ternyata memiliki kode tiga kali lebih banyak karena keputusan desain yang berbeda. Pada akhirnya, perbedaan terbesar dalam jumlah kode adalah dalam bahasa yang sama!

Saya akan menjelaskan mengapa saya menganggap ini perbandingan yang bagus, memberikan beberapa informasi tentang setiap proyek, dan menjelaskan beberapa alasan perbedaan ukuran kompiler. Saya juga akan menarik kesimpulan dari setiap perbandingan. Jangan ragu untuk menggunakan tautan ini untuk pergi ke bagian yang menarik:

Isi


  • Mengapa saya menganggapnya bermakna
  • Karat (dasar untuk perbandingan)
  • Haskell : ukuran 1,0-1,6, tergantung pada bagaimana Anda menghitung, untuk alasan menarik
  • C ++ : 1,4 ukuran untuk alasan yang jelas
  • Python : 0,5 ukuran karena metaprogramming mewah!
  • Karat (grup lain) : tiga kali ukuran karena desain yang berbeda!
  • Scala : 0,7 ukuran
  • OCaml : ukuran 1,0-1,6 tergantung pada bagaimana Anda menghitung, mirip dengan Haskell

Mengapa saya menganggapnya bermakna


Sebelum Anda mengatakan bahwa jumlah kode (saya membandingkan string dan byte) adalah metrik yang mengerikan, saya ingin mencatat bahwa dalam hal ini dapat memberikan pemahaman yang baik. Setidaknya ini adalah contoh paling terkontrol di mana tim yang berbeda menulis program besar yang sama yang telah saya dengar atau baca.

  • Tidak seorang pun (termasuk saya) tahu bahwa saya akan mengukur parameter ini, jadi tidak ada yang mencoba memainkan metrik, semua orang hanya mencoba menyelesaikan proyek dengan cepat dan benar.
  • Semua (dengan pengecualian proyek Python, yang akan saya bahas nanti) mengimplementasikan program untuk tujuan tunggal lulus rangkaian tes otomatis yang sama pada saat yang sama, sehingga hasilnya tidak dapat sangat terdistorsi oleh kelompok yang memecahkan masalah yang berbeda.
  • Proyek ini selesai dalam beberapa bulan, dengan tim, dan seharusnya secara bertahap berkembang dan lulus tes yang dikenal dan tidak diketahui. Ini berarti berguna untuk menulis kode yang bersih dan jelas.
  • Selain lulus tes kursus, kode tidak akan digunakan untuk hal lain, tidak ada yang akan membacanya dan, sebagai kompiler untuk subset Java yang terbatas ke dalam assembler teks, itu tidak akan berguna.
  • Tidak ada perpustakaan selain dari perpustakaan standar yang diizinkan, dan tidak ada pembantu untuk penguraian, bahkan jika mereka ada di perpustakaan standar. Ini berarti bahwa perbandingan tidak dapat didistorsi oleh pustaka kompiler yang kuat yang hanya dimiliki oleh beberapa perintah.
  • Tidak hanya publik, tetapi juga tes rahasia. Mereka mulai sekali setelah pengiriman akhir. Ini berarti bahwa ada insentif untuk menulis kode tes Anda sendiri dan memastikan bahwa kompilator dapat diandalkan, memperbaiki dan menangani situasi perbatasan yang kompleks.
  • Meskipun semua peserta adalah mahasiswa, saya menganggap mereka sebagai programmer yang cukup kompeten. Masing-masing dari mereka mengambil magang selama setidaknya dua tahun, terutama di perusahaan teknologi tinggi, kadang-kadang bahkan mengerjakan kompiler. Hampir semua dari mereka telah pemrograman selama 7-13 tahun dan adalah penggemar yang banyak membaca di Internet di luar kursus mereka.
  • Kode yang dihasilkan tidak diperhitungkan, tetapi file tata bahasa dan kode yang menghasilkan kode lain diperhitungkan.

Jadi, saya berpikir bahwa jumlah kode memberikan pemahaman yang layak tentang berapa banyak upaya yang akan diperlukan untuk mendukung setiap proyek, jika itu jangka panjang. Saya pikir tidak terlalu banyak perbedaan antara proyek juga memungkinkan Anda untuk menyangkal beberapa pernyataan luar biasa yang saya baca, misalnya, bahwa kompiler Haskell akan lebih dari setengah ukuran C ++ karena bahasa.

Karat (dasar untuk perbandingan)


Saya dan salah satu rekan saya masing-masing menulis lebih dari 10 ribu baris di Rust sebelumnya, dan kolega ketiga menulis, mungkin, 500 baris di beberapa hackathon. Kompiler kami keluar dalam 6806 baris wc -l , 5900 baris sumber (tanpa spasi dan komentar), dan 220 KB wc -c .

Saya menemukan bahwa dalam proyek-proyek lain proporsi ini kira-kira dihormati, dengan beberapa pengecualian, yang akan saya perhatikan. Untuk sisa artikel, ketika saya merujuk pada string atau jumlah, maksud saya wc -l , tapi itu tidak masalah (kecuali saya perhatikan perbedaannya), dan Anda dapat mengonversi dengan koefisien.

Saya menulis artikel lain yang menggambarkan desain kami , yang lulus semua tes publik dan rahasia. Ini juga berisi beberapa fitur tambahan yang kami buat untuk bersenang-senang, bukan untuk lulus tes, yang mungkin menambahkan sekitar 400 baris. Ini juga memiliki sekitar 500 baris unit test kami.

Haskell


Tim Haskell termasuk dua teman saya yang menulis mungkin beberapa ribu baris Haskell masing-masing, ditambah membaca banyak konten online tentang Haskell dan bahasa fungsional serupa lainnya seperti OCaml dan Lean. Mereka memiliki rekan satu tim lain yang tidak saya kenal dengan baik, tetapi tampaknya seorang programmer yang kuat menggunakan Haskell sebelumnya.

Kompiler mereka berjumlah 9.750 baris wc -l , 357 KB, dan 7777 baris kode (SLOC). Tim ini juga memiliki satu-satunya perbedaan signifikan antara rasio-rasio ini: kompiler mereka 1,4 kali lebih besar dari kita di baris, 1,3 kali dalam SLOC dan 1,6 kali dalam byte. Mereka tidak menerapkan fungsi tambahan apa pun, lulus 100% dari tes publik dan rahasia.

Penting untuk dicatat bahwa dimasukkannya tes paling mempengaruhi tim ini. Karena mereka dengan hati-hati mendekati kebenaran kode, mereka memasukkan 1.600 baris tes. Mereka menangkap beberapa situasi batas yang tidak ditangkap tim kami, tetapi kasus-kasus ini sama sekali tidak diperiksa oleh tes lapangan. Jadi tanpa tes di kedua sisi (6,3 ribu baris versus 8,1 ribu baris) kompiler mereka hanya 30% lebih tinggi dari kita.

Di sini saya cenderung byte sebagai ukuran perbandingan volume yang lebih masuk akal, karena dalam proyek Haskell, rata-rata, ada garis yang lebih panjang, karena tidak memiliki sejumlah besar garis dari satu kurung penutup, dan rustfmt tidak memecah rantai fungsi garis tunggal menjadi beberapa garis.

Setelah mencari-cari salah satu rekan tim saya, kami menemukan penjelasan berikut untuk perbedaan ini:

  • Kami menggunakan analisa leksikal tulisan tangan dan metode keturunan rekursif, dan mereka menggunakan generator NFA dan DFA dan parser LR , dan kemudian sebuah pass untuk mengubah pohon parsing menjadi AST ( pohon sintaksis abstrak , representasi kode yang lebih nyaman). Ini memberi mereka lebih banyak kode: 2677 baris dibandingkan dengan 1705 kami, sekitar 1000 baris lebih.
  • Mereka menggunakan AST generik fantastis, yang pindah ke berbagai jenis parameter karena lebih banyak informasi ditambahkan di setiap pass. Fungsi pembantu ini dan lebih banyak lagi untuk menulis ulang mungkin menjelaskan mengapa kode AST mereka sekitar 500 baris lebih lama dari implementasi kami, tempat kami mengumpulkan literal struct dan memutasi bidang Option<_> untuk menambahkan informasi saat kami melanjutkan.
  • Mereka masih memiliki sekitar 400 baris kode selama generasi, yang terutama terkait dengan abstraksi yang lebih besar yang diperlukan untuk menghasilkan dan menggabungkan kode dengan cara yang murni fungsional, di mana kita hanya menggunakan mutasi dan penulisan garis.

Perbedaan ini ditambah tes menjelaskan semua perbedaan volume. Bahkan, file kami untuk konstanta lipat dan resolusi konteks sangat dekat ukurannya. Tapi tetap saja, ada beberapa perbedaan dalam byte karena garis yang lebih panjang: mungkin karena lebih banyak kode diperlukan untuk menulis ulang seluruh pohon di setiap pass.

Akibatnya, mengesampingkan keputusan desain, menurut saya, Rust dan Haskell sama-sama ekspresif, mungkin dengan sedikit keuntungan Rust karena kemampuan untuk dengan mudah menggunakan mutasi ketika itu nyaman. Menarik juga untuk mengetahui bahwa pilihan saya tentang metode keturunan rekursif dan analisis leksikal tulisan tangan terbayar: itu adalah risiko yang bertentangan dengan rekomendasi dan instruksi profesor, tetapi saya memutuskan bahwa itu lebih mudah dan itu benar.

Penggemar Haskell akan berpendapat bahwa tim itu mungkin tidak mengambil keuntungan penuh dari fitur Haskell, dan jika mereka tahu bahasa lebih baik, mereka bisa membuat proyek dengan kode lebih sedikit. Saya setuju, seseorang seperti Edward Kmett dapat menulis kompiler yang sama dalam jumlah yang jauh lebih kecil. Memang, tim teman saya tidak menggunakan banyak abstraksi canggih super canggih dan perpustakaan kombinator mewah seperti lensa . Namun, semua ini mempengaruhi keterbacaan kode. Semua orang di tim adalah programmer berpengalaman, mereka tahu bahwa Haskell mampu melakukan hal-hal yang sangat aneh, tetapi memutuskan untuk tidak menggunakannya karena mereka memutuskan bahwa memahami mereka akan memakan waktu lebih banyak daripada yang akan mereka hemat, dan membuat kode lebih sulit untuk dipahami orang lain. Ini tampaknya seperti kompromi nyata, dan klaim bahwa Haskell secara ajaib cocok untuk kompiler masuk ke dalam sesuatu seperti "Haskell membutuhkan keterampilan yang sangat tinggi dalam menulis kompiler jika Anda tidak peduli tentang dukungan kode untuk orang-orang yang juga tidak terlalu mahir di Haskell."

Menarik juga untuk dicatat bahwa pada awal setiap proyek, profesor mengatakan bahwa siswa dapat menggunakan bahasa apa pun yang berjalan di server universitas, tetapi memperingatkan bahwa tim di Haskell berbeda dari yang lain: mereka memiliki sebaran terbesar di kelas. Banyak orang melebih-lebihkan kemampuan mereka dan tim Haskell memiliki nilai paling buruk, meskipun yang lain baik-baik saja seperti teman-teman saya.

C ++


Kemudian saya berbicara dengan teman saya dari tim C ++. Saya hanya mengenal satu orang di tim ini, tetapi C ++ digunakan di beberapa program di universitas kami, jadi mungkin semua orang di tim memiliki pengalaman C ++.

Proyek mereka terdiri dari 8733 baris dan 280 KB, tidak termasuk kode uji, tetapi termasuk sekitar 500 baris fungsi tambahan. Yang membuatnya 1,4 kali lebih besar dari kode kami tanpa tes, yang juga memiliki sekitar 500 baris fungsi tambahan. Mereka lulus 100% dari tes publik, tetapi hanya 90% dari tes rahasia. Agaknya karena mereka tidak mengimplementasikan array vtables mewah yang diperlukan oleh spesifikasi, yang mungkin memakan 50-100 baris kode.

Saya tidak menggali terlalu dalam tentang perbedaan ukuran ini. Saya kira ini terutama disebabkan oleh:

  • Mereka menggunakan LR parser dan tree rewriter alih-alih metode turunan rekursif.
  • Kurangnya jumlah jumlah dan perbandingan pola dalam C ++, yang telah kami gunakan secara luas dan yang sangat berguna.
  • Kebutuhan untuk menduplikasi semua tanda tangan di file header, yang tidak terjadi di Rust.

Kami juga membandingkan waktu kompilasi. Di laptop saya, build debug bersih dari compiler kami membutuhkan 9,7 detik, rilis bersih 12,5 detik, dan build debug tambahan 3,5 detik. Teman saya tidak memiliki timing untuk C ++ build (menggunakan parallel parallel), tetapi ia mengatakan bahwa jumlahnya sama, dengan peringatan bahwa mereka meletakkan implementasi dari banyak fungsi kecil di file header untuk mengurangi duplikasi tanda tangan dengan biaya waktu yang lebih lama (yaitu oleh karena itu, saya tidak dapat mengukur overhead baris net di file header).

Python


Teman saya, seorang programmer yang sangat baik, memutuskan untuk membuat proyek sendiri dengan Python. Dia juga menerapkan fitur yang lebih canggih (untuk bersenang-senang) daripada tim lain, termasuk tampilan SSA menengah dengan alokasi register dan optimisasi lainnya. Di sisi lain, karena ia bekerja sendiri dan menerapkan banyak fungsi tambahan, ia memberikan sedikit perhatian pada kualitas kode, misalnya, melempar pengecualian yang tidak terdiferensiasi untuk semua kesalahan (mengandalkan backtraces untuk debugging) alih-alih menerapkan jenis kesalahan dan pesan yang sesuai, seperti kita

Kompilernya terdiri dari 4.581 baris dan lulus semua tes publik dan rahasia. Dia juga menerapkan fungsi yang lebih maju daripada perintah lain, tetapi sulit untuk menentukan berapa banyak kode tambahan yang diperlukan, karena banyak fungsi tambahan adalah versi yang lebih kuat dari hal-hal sederhana yang diperlukan semua orang untuk mengimplementasikan, seperti konstanta lipat dan menghasilkan kode. Fungsi tambahan mungkin 1000-2000 baris, setidaknya, jadi saya yakin bahwa kodenya setidaknya dua kali lebih ekspresif dari kita.

Satu bagian besar dari perbedaan ini mungkin adalah pengetikan dinamis. Hanya di ast.rs kami ast.rs 500 baris definisi tipe, dan banyak lagi tipe lain yang didefinisikan di kompiler. Kami juga selalu terbatas pada sistem tipe itu sendiri. Sebagai contoh, kita memerlukan infrastruktur untuk menambahkan informasi baru ke AST secara ergonomis ketika kita melewatinya dan mengaksesnya nanti. Sementara di Python Anda bisa mengatur bidang baru pada node AST.

Metaprogramming yang kuat juga menjelaskan bagian dari perbedaan. Sebagai contoh, meskipun dia menggunakan parser LR daripada metode keturunan rekursif, dalam kasus saya, saya pikir itu mengambil kode kurang karena alih-alih melalui menulis ulang pohon, tata bahasa LR-nya termasuk potongan-potongan kode Python untuk membangun AST, yang generator dapat berubah menjadi fungsi Python menggunakan eval . Salah satu alasan kami tidak menggunakan parser LR adalah bahwa membangun AST tanpa menulis ulang pohon akan membutuhkan banyak upacara (membuat file Rust atau makro prosedural) untuk mengaitkan tata bahasa dengan fragmen kode Rust.

Contoh lain dari kekuatan metaprogramming dan pengetikan dinamis adalah file 400-line visit.rs , yang pada dasarnya adalah kode boilerplate berulang yang mengimplementasikan pengunjung pada sekelompok struktur AST. Dalam Python, ini bisa menjadi fungsi pendek dari sekitar 10 baris yang secara rekursif mengintrospeksi bidang-bidang simpul AST dan mengunjungi mereka (menggunakan atribut __dict__ ).

Sebagai penggemar Rust dan bahasa yang diketik secara statis pada umumnya, saya cenderung mencatat bahwa sistem tipe sangat berguna untuk mencegah kesalahan dan untuk kinerja. Metaprogramming yang tidak biasa juga dapat membuatnya sulit untuk memahami cara kerja kode. Namun, perbandingan ini mengejutkan saya dengan fakta bahwa saya tidak berharap bahwa perbedaan dalam jumlah kode akan sangat besar. Jika perbedaan secara keseluruhan sangat dekat dengan keharusan untuk menulis kode dua kali lebih banyak, saya masih berpikir Rust adalah kompromi yang cocok, tetapi masih setengah dari kode adalah argumen, dan di masa depan saya cenderung melakukan sesuatu dalam Ruby / Python jika Anda hanya perlu dengan cepat membangun sesuatu sendiri, dan kemudian membuangnya.

Karat (grup lain)


Perbandingan yang paling menarik bagi saya adalah dengan teman saya, yang juga melakukan proyek di Rust dengan satu teman satu tim (yang saya tidak tahu). Teman saya memiliki pengalaman Karat yang baik. Dia berkontribusi pada pengembangan compiler Rust dan banyak membaca. Saya tidak tahu apa-apa tentang kawannya.

Proyek mereka terdiri dari 17.211 jalur mentah, jalur sumber 15k dan 637 KB, tidak termasuk kode uji dan kode yang dihasilkan. Itu tidak memiliki fungsi tambahan, dan hanya melewati 4 dari 10 tes rahasia dan 90% dari tes publik untuk pembuatan kode, karena mereka tidak punya cukup waktu sebelum batas waktu untuk mengimplementasikan bagian-bagian spesifikasi yang lebih aneh. Program mereka tiga kali lebih besar dari program kami, ditulis dalam bahasa yang sama, dan dengan fungsionalitas yang lebih sedikit!

Hasil ini benar-benar luar biasa bagi saya dan menaungi semua perbedaan antara bahasa yang telah saya pelajari sejauh ini. Oleh karena itu, kami membandingkan daftar ukuran file wc -l , dan juga memeriksa bagaimana masing-masing dari kami menerapkan beberapa hal spesifik yang menghasilkan ukuran kode yang berbeda.

Tampaknya semuanya bermuara pada pengadopsian berbagai keputusan desain secara konsisten. Sebagai contoh, frontend mereka (analisis leksikal, penguraian, membangun AST) mengambil 7597 baris terhadap 2164 kami. Mereka menggunakan penganalisa leksikal DFA dan pengurai LALR (1), tetapi kelompok lain melakukan hal serupa tanpa begitu banyak kode. Melihat file weeder mereka, saya perhatikan sejumlah keputusan desain yang berbeda dari kita:

  • Mereka memutuskan untuk menggunakan pohon parsing yang diketik sepenuhnya daripada pohon parsing standar, seragam, berbasis string. Ini mungkin memerlukan lebih banyak definisi tipe dan kode konversi tambahan pada tahap penguraian atau pengurai yang lebih kompleks.
  • Mereka menggunakan implementasi tryfrom tryfrom untuk mengkonversi antara tipe pohon parsing dan tipe AST untuk memvalidasinya. Ini mengarah ke banyak blok impl 10-20-line. Untuk melakukan ini, kami menggunakan fungsi yang mengembalikan tipe Result , yang menghasilkan lebih sedikit garis, dan juga membebaskan kami sedikit dari struktur tipe, menyederhanakan parameterisasi dan menggunakan kembali. Beberapa hal yang, bagi kami, adalah cabang match satu baris, mereka memiliki blok impl 10-baris.
  • Tipe kami disusun untuk mengurangi salin-tempel. Sebagai contoh, mereka menggunakan bidang terpisah is_abstract , is_native dan is_static , di mana kode periksa kendala harus disalin dua kali: sekali untuk metode yang diketik batal dan satu kali untuk metode dengan tipe kembali, dengan perubahan kecil. Sementara void kami hanyalah tipe khusus, kami menghasilkan taksonomi pengubah dengan mode dan visibility yang menerapkan batasan level-type, dan kesalahan kendala dihasilkan secara default untuk operator pertandingan, yang menerjemahkan set pengubah ke mode dan visibility .

Saya tidak melihat kode lintasan analisis kompiler mereka, tetapi mereka juga hebat. Saya berbicara dengan teman saya, dan tampaknya mereka tidak menerapkan sesuatu yang mirip dengan infrastruktur pengunjung, seperti kita. Saya kira itu, bersama dengan beberapa perbedaan desain yang lebih kecil, menjelaskan perbedaan ukuran bagian ini. Pengunjung mengijinkan analisis kami untuk fokus hanya pada bagian-bagian AST yang mereka butuhkan, daripada mencocokkan pola di seluruh struktur AST. Ini menghemat banyak kode.

Bagian mereka untuk pembuatan kode terdiri dari 3594 baris, dan kita - 1560. Saya melihat kode mereka dan tampaknya hampir semua perbedaannya adalah bahwa mereka memilih struktur data menengah untuk instruksi assembler, di mana kami hanya menggunakan pemformatan string untuk output assembler langsung . Mereka harus mendefinisikan tipe dan fungsi output untuk semua instruksi dan tipe operan yang digunakan. Itu juga berarti bahwa instruksi pembangunan gedung mengambil lebih banyak kode. Di mana kami memiliki operator format dengan instruksi pendek, seperti mov ecx, [edx] , mereka membutuhkan operator rustfmt raksasa, dipecah menjadi 6 baris, yang membangun instruksi dengan sekelompok tipe bersarang menengah untuk operan yang mencakup hingga 6 tingkat tanda kurung bersarang. Kami juga bisa menampilkan blok instruksi terkait, seperti pembukaan fungsi, dalam pernyataan format tunggal, di mana mereka harus menentukan konstruksi lengkap untuk setiap instruksi.

Tim kami sedang mempertimbangkan untuk menggunakan abstraksi seperti milik mereka. Lebih mudah untuk bisa mengeluarkan rakitan teks atau langsung mengeluarkan kode mesin, namun ini bukan persyaratan kursus. Hal yang sama dapat dilakukan dengan kode yang lebih sedikit dan kinerja yang lebih baik menggunakan X86Writer X86Writer dengan metode seperti push(reg: Register) . Kami juga memperhitungkan bahwa ini dapat menyederhanakan debugging dan pengujian, tetapi kami menyadari bahwa melihat assembler teks yang dihasilkan sebenarnya lebih mudah dibaca dan diuji menggunakan pengujian snapshot jika Anda memasukkan komentar secara bebas. Tetapi kami (tampaknya benar) memperkirakan bahwa itu akan membutuhkan banyak kode tambahan, dan tidak ada manfaat nyata, mengingat kebutuhan nyata kami, jadi kami tidak khawatir.

Adalah baik untuk membandingkan ini dengan representasi perantara yang digunakan tim C ++ sebagai fungsi tambahan, yang hanya membutuhkan 500 baris tambahan. Mereka menggunakan struktur yang sangat sederhana (untuk definisi tipe sederhana dan membangun kode) yang menggunakan operasi yang dekat dengan apa yang diperlukan Java. Ini berarti bahwa perwakilan perantara mereka jauh lebih kecil (dan karena itu memerlukan lebih sedikit kode build) daripada assembler yang dihasilkan, karena banyak operasi bahasa, seperti panggilan dan gips, diperluas ke banyak instruksi assembler. Mereka juga mengatakan bahwa itu sangat membantu debugging, karena memotong banyak sampah dan meningkatkan keterbacaan. Presentasi tingkat yang lebih tinggi juga memungkinkan beberapa optimasi sederhana dilakukan pada representasi perantara mereka. Tim C ++ menghasilkan desain yang sangat bagus yang membuat mereka jauh lebih baik dengan kode yang lebih sedikit.

Secara umum, tampaknya alasan umum untuk perbedaan tiga kali lipat dalam volume adalah karena adopsi yang konsisten dari berbagai keputusan desain, baik besar maupun kecil, ke arah kode yang lebih banyak. Mereka menerapkan sejumlah abstraksi yang tidak kami lakukan - mereka menambahkan lebih banyak kode, dan melewatkan beberapa abstraksi kami, yang mengurangi jumlah kode.

Hasil ini benar-benar mengejutkan saya. Saya tahu bahwa keputusan desain penting, tetapi saya tidak akan menduga sebelumnya bahwa mereka akan menyebabkan perbedaan ukuran ini, mengingat bahwa saya hanya memeriksa orang-orang yang saya anggap sebagai programmer yang kompeten dan kuat. Dari semua hasil perbandingan, ini yang paling signifikan bagi saya.Mungkin membantu saya bahwa saya banyak membaca tentang cara menulis kompiler sebelum saya mengambil kursus ini, sehingga saya bisa menggunakan proyek pintar yang orang lain buat dan ternyata bekerja dengan baik, seperti pengunjung AST dan metode penurunan rekursif, walaupun mereka tidak mengajar pada kursus kami.

Yang benar-benar membuat saya berpikir adalah biaya abstraksi. Abstraksi dapat memfasilitasi ekspansi di masa depan atau melindungi dari beberapa jenis kesalahan, tetapi harus diperhitungkan, mengingat Anda bisa mendapatkan kode tiga kali lebih banyak untuk memahami dan melakukan refactoring, tiga kali lebih banyak tempat yang memungkinkan untuk kesalahan dan lebih sedikit waktu untuk pengujian dan selanjutnya pengembangan. Kursus pelatihan kami berbeda dari dunia nyata: kami tahu pasti bahwa kami tidak akan pernah menyentuh kode setelah pengembangan, ini menghilangkan manfaat abstraksi proaktif. Namun, jika saya harus memilih kompiler mana yang akan diperluas dengan fungsi sewenang-wenang yang akan Anda katakan nanti, saya akan memilih kompiler kami, tanpa mempertimbangkan keakraban saya dengannya. Hanya karena memiliki kode yang jauh lebih sedikit untuk dipahami, dan saya berpotensi memilih abstraksi terbaik untuk persyaratan (mis.representasi perantara dari perintah C ++) ketika saya mengetahui persyaratan spesifik.

Juga, dalam pandangan saya, taksonomi abstraksi diperkuat: ada yang mengurangi kode, hanya memperhitungkan persyaratan saat ini, seperti templat pengunjung kami, dan ada abstraksi yang menambahkan kode, tetapi memberikan manfaat ekstensibilitas, debugging, atau kebenaran.

Scala


Saya juga berbicara dengan seorang teman yang melakukan proyek di Scala pada semester sebelumnya, tetapi proyek dan tesnya persis sama. Kompiler mereka terdiri dari 4141 baris dan ~ 160 KB kode, tidak termasuk tes. Mereka lulus 8 dari 10 tes rahasia dan 100% tes terbuka dan tidak menerapkan fungsi tambahan. Jadi, dibandingkan dengan lini 5906 kami tanpa fungsi dan tes tambahan, kompilernya adalah 30% lebih sedikit.

Salah satu faktor desain kecil adalah pendekatan parsing yang berbeda. Kursus ini memungkinkan penggunaan alat baris perintah untuk generator tabel LR. Tidak ada yang menggunakannya kecuali tim ini. Ini menyelamatkan mereka dari keharusan mengimplementasikan generator tabel LR. Mereka juga berhasil menghindari menulis tata bahasa LR dengan skrip Python 150-baris yang mengikis halaman web tata bahasa Java yang mereka temukan di Internet dan menerjemahkannya ke dalam format input generator. Mereka masih perlu membuat semacam pohon di Scala, tetapi secara umum tahap parsing berjumlah 1073 baris dibandingkan dengan 1443 kami, meskipun metode gradient descent kami di sini memberikan keuntungan dalam volume dibandingkan dengan semua tim lain.

Sisa dari kompiler mereka juga lebih kecil dari kita, tanpa perbedaan desain yang jelas, meskipun saya tidak menggali kode. Saya menduga bahwa ini disebabkan oleh perbedaan dalam ekspresifitas Scala dan Rust. Scala dan Rust memiliki fitur pemrograman serupa yang berguna untuk kompiler, seperti pencocokan pola, tetapi memori yang dikelola Scala menyimpan kode yang diperlukan untuk pemeriksa pinjaman agar bekerja di Rust. Selain itu, Scala memiliki gula sintaksis yang lebih bervariasi daripada Rust.

OCaml


Karena semua anggota tim kami menjalani magang di Jane Street (perusahaan perdagangan teknologi - kira-kira Per), saya sangat tertarik untuk melihat hasil dari mantan pekerja magang Jane Street lainnya yang memilih OCaml untuk menulis kompiler.

Kompiler mereka adalah 10.914 baris dan 377 KB, termasuk sejumlah kecil kode uji dan tidak ada fitur tambahan. Mereka lulus 9/10 tes rahasia dan semua tes publik.

Seperti kelompok lain, tampaknya perbedaan utama dalam ukuran adalah karena penggunaan parser LR dan penulisan ulang pohon untuk parsing, serta pipa konversi DFA regex-> NFA-> untuk analisis leksikal. Frontend mereka (analisis leksikal, penguraian, konstruksi AST) adalah 5548 baris, dan kita - 2164, dengan rasio yang sama untuk byte. Mereka juga menggunakan pengujian untuk parser mereka dengan harapan bahwa itu mirip dengan tes snapshot kami, yang menempatkan output yang diharapkan di luar kode, sehingga tes parser mereka membuat ~ 600 baris dari total, dan kami - sekitar 200.

Ini menyisakan 5366 baris untuk sisa kompiler (461 baris di antaranya adalah file antarmuka dengan deklarasi tipe) dan 4642 untuk kita, perbedaannya hanya 15%, jika kita menghitung file antarmuka mereka, dan ukurannya hampir sama, jika tidak dihitung. Tampaknya, terlepas dari solusi desain parsing kami, Rust dan OCaml tampaknya sama-sama ekspresif, kecuali bahwa OCaml membutuhkan file front-end, dan Rust tidak.

Kesimpulan


Secara umum, saya sangat senang bahwa saya membuat perbandingan ini, saya belajar banyak dan terkejut berkali-kali. Saya pikir kesimpulan umum adalah bahwa keputusan desain jauh lebih penting daripada bahasa, tetapi bahasa penting karena memberi Anda alat untuk menerapkan desain yang berbeda.

Source: https://habr.com/ru/post/id456638/


All Articles