- C memang cocok dengan abstraksi perangkat keras pada era PDP-11, tetapi pada CPU modern, mesin abstrak C berupa eksekusi berurutan dan memori datar sangat menyimpang dari perangkat keras nyata
- Spectre dan Meltdown terkait dengan akibat dari besarnya ketergantungan prosesor pada prediksi cabang, eksekusi spekulatif, dan paralelisme tingkat instruksi untuk menjalankan model berurutan C dengan cepat
- Untuk membuat kode C cepat, yang dibutuhkan bukan sekadar penerjemahan sederhana ke bahasa mesin, melainkan optimisasi kompleks setingkat LLVM/Clang; sebagian optimisasi dapat bertentangan dengan semantik C
- Aturan seperti asal-usul pointer, padding struktur, nilai yang belum diinisialisasi, dan signed integer overflow membuat hasil eksekusi sulit diprediksi dan juga dapat berujung pada kerentanan keamanan
- Model yang lebih cocok untuk perangkat keras modern memanfaatkan banyak thread, unit vektor lebar, dan model memori sederhana, tetapi kompatibilitas dengan kode C yang sudah ada tetap menjadi kendala terbesar
Mengapa C dulu terlihat “tingkat rendah”
- Jika sebuah bahasa disebut tingkat rendah, mesin abstrak bahasa tersebut semestinya mudah dipetakan ke abstraksi yang disediakan perangkat keras
- Pada PDP-11, C dapat dianggap sebagai bahasa tingkat rendah
- Program dieksekusi secara berurutan
- Memori diperlakukan seperti ruang datar
- Operator pre-increment dan post-increment cocok dengan mode pengalamatan PDP-11
- Alan Perlis mendefinisikan bahwa “sebuah bahasa adalah tingkat rendah ketika program harus memperhatikan hal-hal yang tidak relevan”, tetapi definisi ini saja sulit menjelaskan sepenuhnya “kedekatan dengan perangkat keras” yang diharapkan orang dari bahasa tingkat rendah
CPU modern bertindak seperti emulator PDP-11 yang cepat
- Akar masalah Spectre dan Meltdown tidak berhenti pada pembuatan prosesor cepat, tetapi juga berkaitan dengan desain prosesor yang berusaha mengekspos mesin abstrak seperti PDP-11 secara cepat
- Hingga sebelum C11, selain ekstensi vendor nonstandar, kode C pada dasarnya menyediakan mesin sepenuhnya berurutan, dan bahkan setelah C11 pun sebagian besar tetap mempertahankan mesin abstrak yang berurutan
- CPU modern mengekstraksi paralelisme tingkat instruksi (ILP) agar unit eksekusi tetap sibuk
- CPU memeriksa operasi yang berdekatan dan menerbitkan operasi independen secara paralel
- Sebagai gantinya, programmer tetap diizinkan menulis sebagian besar kode secara berurutan, tetapi kompleksitas dan konsumsi daya meningkat
- GPU dapat menghasilkan performa tinggi tanpa logika semacam ini, tetapi membutuhkan program paralel yang eksplisit
Spectre, Meltdown, dan biaya eksekusi spekulatif
- Prosesor Intel modern dapat memiliki hingga 180 instruksi sekaligus dalam keadaan in-flight
- Dalam kode C, rata-rata dapat diasumsikan ada cabang kira-kira setiap 7 instruksi
- Untuk mengisi pipeline pada satu thread, target dari 25 cabang berikutnya harus ditebak
- Tebakan yang salah menghasilkan pekerjaan yang dilakukan lalu dibuang, sekaligus memboroskan daya
- Spectre dan Meltdown dapat mengeksploitasi efek samping yang terlihat dari pekerjaan yang dibuang ini sebagai side channel
- Register rename engine pada core modern berperforma tinggi adalah salah satu konsumen besar area die dan daya
- Ketika instruksi sedang berjalan, unit ini sulit dimatikan atau diputus dayanya
- GPU tidak memiliki unit semacam ini; paralelisme berasal dari banyak thread
Model memori datar C tidak sesuai dengan realitas cache
- Memori datar, inti dari mesin abstrak C, sudah tidak cocok dengan perangkat keras nyata selama lebih dari 20 tahun
- Prosesor modern biasanya memiliki tiga tingkat cache antara register dan memori utama
- Sesuai namanya, cache disembunyikan dari programmer dan tidak terlihat oleh C
- Untuk menghasilkan kode cepat pada prosesor modern, cache harus digunakan secara efisien
- Programmer C harus memahami bukan hanya mesin abstrak, tetapi juga detail implementasi agar mendapatkan performa
- Misalnya, dua nilai yang disejajarkan ke 64 byte bisa masuk ke cache line yang sama
Kompleksitas compiler yang diperlukan untuk membuat kode C cepat
- Jika sebuah bahasa tingkat rendah, ia semestinya mudah diterjemahkan menjadi kode cepat tanpa compiler yang kompleks, tetapi C tidak demikian
- Clang dan bagian LLVM terkait berukuran sekitar 2 juta baris
- Bahkan hanya menghitung pass analisis dan transformasi yang diperlukan untuk menjalankan C dengan cepat, tanpa komentar dan baris kosong, jumlahnya hampir 200 ribu baris
- Saat memproses data dalam jumlah besar di C, biasanya programmer menulis loop yang memproses setiap elemen secara berurutan
- Agar dapat berjalan optimal pada CPU modern, compiler harus terlebih dahulu menentukan independensi antar-iterasi loop
- Kata kunci
restrictdapat memberikan jaminan bahwa penulisan melalui satu pointer tidak mengganggu pembacaan melalui pointer lain
- Fortran lebih unggul daripada C dalam hal penyediaan informasi semacam ini, dan ini adalah salah satu alasan utama C tidak menggantikan Fortran dalam komputasi berperforma tinggi
Konflik antara vektorisasi dan jaminan tata letak memori C
- Jika iterasi loop independen, compiler akan mencoba memvektorisasi hasilnya
- Prosesor modern dapat memperoleh throughput 4–8 kali lebih tinggi pada kode vektor dibanding kode skalar
- Untuk bahasa tingkat rendah bagi prosesor seperti ini, wajar jika ia memiliki tipe vektor native dengan panjang arbitrer
- LLVM IR menyediakan model seperti ini karena lebih mudah memecah operasi vektor besar menjadi operasi kecil daripada sebaliknya
- Jaminan tata letak memori C bertentangan dengan optimisasi
- Struktur dengan prefix yang sama dapat digunakan secara saling menggantikan
- Offset field struktur terekspos ke bahasa
- Compiler sulit mengubah urutan field atau menyisipkan padding untuk meningkatkan vektorisasi
- Kemampuan mengendalikan tata letak struktur data secara rinci bisa menjadi keunggulan bahasa tingkat rendah, tetapi sekaligus membuat C sulit dipercepat
Masalah padding, SROA, dan loop unswitching
- C mensyaratkan padding di akhir struktur untuk menjamin tidak ada padding di dalam array
- Karena struktur harus memungkinkan perbandingan yang tidak bergantung tipe seperti
memcmp, penyalinan struktur juga harus mempertahankan padding- Dalam beberapa eksperimen, porsi yang cukup terlihat dari total waktu eksekusi workload tertentu dipakai untuk menyalin padding
- SROA adalah optimisasi yang mencoba mengganti struktur dan array berukuran tetap dengan variabel-variabel individual
- Ini memungkinkan akses diperlakukan secara independen, dan operasi yang hasilnya tidak diamati dapat dihapus
- Dalam beberapa kasus, padding dapat dihilangkan, tetapi tidak selalu demikian
- Loop unswitching adalah optimisasi yang memindahkan conditional dari dalam loop ke luar conditional dan menempatkan loop di kedua jalur
- Ini bertentangan dengan gagasan bahwa saat kode bahasa tingkat rendah dijalankan, programmer tahu kode apa yang akan berjalan dan kapan
- Ini juga dapat menimbulkan masalah dengan konsep unspecified value dan undefined behavior dalam C
Nilai yang belum diinisialisasi dan perilaku tak terdefinisi
- Dalam C, membaca variabel yang belum diinisialisasi menghasilkan unspecified value, dan nilainya bisa berbeda setiap kali dibaca
- Aturan ini memungkinkan perilaku seperti daur ulang halaman secara malas
- Implementasi
mallocFreeBSD memberi tahu sistem operasi tentang halaman yang saat ini tidak digunakan - Sistem operasi dapat menggunakan penulisan pertama ke halaman sebagai petunjuk bahwa halaman tersebut kembali dibutuhkan
- Implementasi
- Jika unspecified value digunakan dalam kontrol alur, itu menjadi undefined behavior
- Contohnya adalah penggunaan nilai yang belum diinisialisasi dalam kondisi
if
- Contohnya adalah penggunaan nilai yang belum diinisialisasi dalam kondisi
- Dalam loop unswitching, jika loop dieksekusi 0 kali, pada kode asli seluruh badan loop adalah dead code
- Setelah unswitching, kode dapat bercabang berdasarkan variabel yang mungkin belum diinisialisasi
- Dengan kata lain, dead code berubah menjadi perilaku tak terdefinisi
- Kode C memang bisa dibuat cepat, tetapi membuat compiler yang cukup pintar membutuhkan ribuan person-year dan terkadang harus melanggar sebagian aturan bahasa
Mengapa C menjadi sulit dipahami
- Jika sebuah bahasa tingkat rendah, programmer semestinya dapat dengan mudah memahami pemetaan antara mesin abstrak dan mesin fisik nyata
- Pada PDP-11, ekspresi C mudah dipetakan menjadi satu atau dua instruksi, dan variabel lokal serta tipe primitif juga berkorespondensi sederhana dengan perangkat keras
- Setelah itu, implementasi C menjadi semakin kompleks demi mempertahankan ilusi kode cepat dan korespondensi dengan perangkat keras
- Sebuah survei tahun 2015 terhadap programmer C, penulis compiler, dan anggota komite standar mengungkap masalah keterpahaman C
- Ketika sebuah struktur diinisialisasi ke 0 lalu sebagian field disetel, 36% yakin bahwa semua bit padding bernilai 0, sementara 29% menjawab tidak tahu
- Hasil sebenarnya dapat berbeda tergantung compiler dan tingkat optimisasi
Asal-usul pointer dan kerentanan keamanan
- Model BCPL adalah model yang relatif sederhana: nilai adalah word, dan setiap word adalah data atau alamat data
- Model C dirancang agar dapat diimplementasikan pada berbagai target, termasuk arsitektur bersegmen atau mesin virtual dengan garbage collection
- Standar C membatasi operasi yang valid atas pointer untuk menghindari masalah pada sistem semacam itu
- C Defect Report 260 memasukkan konsep pointer provenance ke dalam definisi pointer
- Implementasi dapat melacak asal-usul pola bit
- Pointer dengan asal berbeda dapat dibedakan meskipun identik pada tingkat bit
- Kata
provenancetidak muncul dalam spesifikasi C11, sehingga penulis compiler harus menentukan maknanya- GCC dan Clang berbeda mengenai apakah provenance dipertahankan ketika pointer dikonversi menjadi integer lalu kembali menjadi pointer
- Pernah diamati kasus kerentanan keamanan pada kode dengan signed integer overflow dan dereferensi pointer sebelum null check
- Karena dereferensi null pointer adalah undefined behavior dalam C, compiler dapat mengasumsikan bahwa pointer yang sudah didereferensi tidak mungkin null
- Contohnya adalah CVE-2009-1897
Membayangkan prosesor yang bukan untuk C
- Perbaikan yang diusulkan untuk Spectre dan Meltdown memberikan penalti performa yang signifikan, dan meniadakan sebagian besar kemajuan mikroarsitektur selama 10 tahun terakhir
- Sudah waktunya memikirkan ulang model pemrograman yang cocok untuk prosesor cepat, alih-alih membuat kode C cepat
- Chip highly multithreaded seperti Sun/Oracle UltraSPARC Tx tidak membutuhkan banyak cache untuk mengisi unit eksekusi
- Jika ada cukup paralelisme tingkat tinggi, thread yang menunggu memori dapat dihentikan dan unit eksekusi diisi dengan instruksi dari thread lain
- Masalahnya, program C cenderung memiliki sedikit thread yang sibuk
- ARM SVE (Scalar Vector Extensions) adalah contoh antarmuka yang lebih baik antara program dan perangkat keras
- Unit vektor tradisional mengekspos operasi vektor berukuran tetap dan berharap compiler menyesuaikan algoritme dengan ukuran tersebut
- SVE memungkinkan programmer mendeskripsikan tingkat paralelisme yang tersedia, lalu perangkat keras memetakannya ke jumlah unit eksekusi
- Dalam C, autovectorizer harus menyimpulkan paralelisme dari struktur loop sehingga kompleks, tetapi dalam operasi
mapbergaya fungsional, panjang array yang dipetakan sama dengan paralelisme yang tersedia, sehingga pembuatan kode menjadi sederhana
Model memori yang lebih sederhana dan pemrograman paralel
- Pada CPU modern, cache coherency protocol adalah salah satu bagian yang sulit dibuat cepat dan benar
- Sebagian besar kompleksitas berasal dari dukungan terhadap bahasa yang mengharapkan data dapat dibagikan sekaligus dapat diubah
- Dalam mesin abstrak bergaya Erlang, semua objek bersifat thread-local atau immutable
- Erlang memiliki model yang lebih sederhana: hanya ada satu objek mutable per thread
- Protokol koherensi cache pada sistem seperti ini dapat dibagi menjadi dua kasus: mutable atau shared
- Objek immutable dapat menyederhanakan cache dan membuat banyak operasi lebih murah
- Project Maxwell dari Sun Labs memperhatikan bahwa objek di dalam cache dan objek yang akan dialokasikan ke young generation hampir merupakan himpunan yang sama
- Jika objek mati sebelum dikeluarkan dari cache, objek itu tidak perlu ditulis kembali ke memori utama sehingga menghemat daya
- Dengan objek immutable di heap dan stack yang mutable, garbage collector dapat menjadi mesin keadaan sederhana yang mudah diimplementasikan di perangkat keras
- Prosesor yang dirancang murni untuk kecepatan kemungkinan besar akan mendukung banyak thread, unit vektor lebar, dan model memori yang lebih sederhana
- Menjalankan kode C pada sistem semacam ini bisa menjadi masalah
- Karena ada begitu banyak kode C legacy di seluruh dunia, keberhasilan komersialnya akan sulit
- Pandangan umum bahwa pemrograman paralel itu sulit lebih tepat berlaku untuk pemrograman paralel dalam bahasa dengan mesin abstrak seperti C
- Alan Kay mengajarkan bahasa actor-model kepada anak-anak, dan mereka menulis program yang berjalan dengan lebih dari 200 thread
- Programmer Erlang lazim menulis program dengan ribuan komponen paralel
- Dalam situasi ketika CPU multicore dan GPU many-core digunakan luas, C tidak terpetakan dengan baik ke perangkat keras modern
1 komentar
Opini Hacker News
Alasan C termasuk level rendah setidaknya adalah karena manajemen memori manual
Khususnya pada perangkat keras modern, manajemen memori berada di pusat pemrograman. Rust menonjolkan keamanan memori tanpa garbage collector pada akhirnya juga karena alasan utama keberadaan Rust hampir selalu berkaitan dengan manajemen memori. Alasan C cepat juga memori, dan alasan C tidak aman pun umumnya memori. Salah satu alasan besar komputasi paralel sulit adalah akses memori bersamaan. Pemrograman fungsional sering dibungkus dengan konsep matematika, tetapi sebagian besarnya adalah membuat objek seolah-olah immutable, sementara di dalamnya compiler menangani memori yang mutable
Di C, jika memakai allocator, semua pemanggilannya eksplisit.
new/deletedan raw pointer pada C++ lama memang memanggil allocator secara eksplisit, tetapi banyak hal juga terjadi otomatis di destructor. Smart pointer pada C++ modern pada dasarnya mirip bahasa dengan garbage collection, dalam arti alokasi dan dealokasi sama-sama terjadi otomatisKita tidak bisa menginstruksikan prosesor data mana yang diletakkan di tingkat cache tertentu, apa yang dikirim ke virtual memory, dan sebagainya. Memang lebih level rendah daripada Python, tetapi sulit menyebutnya manajemen memori level rendah seperti C pada era PDP-11
Pada sistem sekelas mikrokontroler atau sistem tanpa MMU ceritanya berbeda, tetapi itu persoalan tersendiri
Bahkan saya sebagai pengembang Rust bekerja dalam ilusi bahwa pointer adalah objek fisik nyata seperti alamat memori. Rust, dan sampai batas tertentu C++, menempatkan abstraksi pengelolaan berupa reference dan borrowing di depan, tetapi konsep intinya tetap ada
Kenyataannya, kernel sistem operasi menempatkan lapisan besar di antara memori fisik dan program, dan “address” serta “pointer” lebih mirip handle yang diproses dengan berbagai cara oleh OS dan MMU
“Raw pointer” pun sebenarnya tidak raw. Itu adalah handle terhadap offset di dalam page, sementara page sebenarnya bisa tersebar di berbagai tempat. Jika sepenuhnya meninggalkan libc dan model C lalu masuk ke dunia referensi murni yang berinteraksi langsung dengan page milik subsistem VM, semacam “object handle”, justru itu mungkin lebih dekat dengan cara kerja subsistem bawah yang sebenarnya
mallocdanfreeadalah fungsi libraryKarena perangkat keras tidak punya alokasi per byte seperti itu, ini bukan hanya abstraksi di atas hardware, tetapi juga mengabstraksikan cara sistem operasi mengalokasikan memori
Di C, kita juga tidak bisa mengakses stack secara langsung. Stack frame diabstraksikan, dan yang bisa digunakan kurang lebih hanya
longjmpJika memperhitungkan undefined behavior dan strict aliasing rule, hak akses untuk mengutak-atik memori sesuka hati pun tidak banyak
Sebagai programmer C sekaligus penulis compiler, bagi orang yang memahami C dan menggunakannya secara profesional, C jelas merupakan bahasa tingkat rendah.
Jika Anda mencari bahasa tingkat rendah, C dan kerabatnya adalah pilihan terbaik.
Jika Anda baru belajar C dan ingin tahu cara menggunakannya seperti seorang ahli, sebaiknya abaikan tulisan ini. Tulisan ini bisa hanya membuat bingung dan menurunkan kemampuan Anda menggunakan C secara efektif.
Ia menyediakan akses tingkat rendah ke sebuah mesin yang harus diemulasikan dengan cukup susah payah oleh mesin nyata. Berbagai perangkat tempelan dan tambalan yang ditambahkan selama bertahun-tahun untuk mengakses mesin nyata terasa relatif asing di dalam C.
Namun, saya setuju bahwa judulnya secara retoris kasar. Menjadi bahasa tingkat rendah yang keliru bukan berarti ia menjadi bahasa tingkat tinggi. WASM juga akan “keliru” jika mengklaim berpadanan langsung dengan hardware modern, tetapi itu tidak membuatnya menjadi tingkat tinggi.
Fakta bahwa C merupakan pemetaan yang buruk itu sendiri bukan hal yang membuat frustrasi. Itu bahasa dari tahun 1970-an, jadi bisa dimaklumi, dan sampai sekarang pun jelas berguna dalam banyak kasus. Yang lebih membuat frustrasi adalah C masih sangat memengaruhi desain bahasa, dan sangat mewarnai cara para perancang bahasa memandang hardware. Akibatnya, desain bahasa modern terlalu sering hanya mencampur ulang potongan-potongan C, alih-alih membuat bahasa yang benar-benar cocok dengan hardware.
Anda akan mendapat masalah jika mengira kode yang Anda tulis akan memiliki hubungan satu-ke-satu dengan assembly. Jika ingin melihat lebih dalam bagaimana hal-hal seperti ini bisa menjegal Anda, lihat https://youtu.be/w3_e9vZj7D8.
Poin utama penulis mungkin bukan “C bukan bahasa yang baik untuk system programming”. Sulit menulis padanan sesuatu seperti
volatile int *dma_register = SCATTER_GATHER_BASE;di Haskell.Inti penulis adalah bahwa dorongan untuk menjalankan C dan bahasa lain yang “memodelkan mesin von Neumann” dengan cepat telah membuat compiler menjadi sangat kompleks, dan penulis menyiratkan bahwa “jika tingkat rendah, compiler yang dibutuhkan seharusnya sederhana”. Prosesor yang dibuat untuk menjalankan kode seperti itu dengan cepat juga sangat kompleks, dan kompleksitas itu punya biaya.
Dalam banyak hal, ini adalah tulisan yang menyerukan peralihan model pemrograman, dan GPU dijadikan contoh kemungkinan ketika “model pemrograman baru” dan “silikon yang mendukungnya” dibuat bersama-sama.
Makna aslinya lebih dekat dengan cara kata itu dipakai dalam tulisan tersebut. Bahasa tingkat rendah tidak portabel dan terikat pada hardware tempat ia berjalan, sedangkan bahasa tingkat tinggi dapat menargetkan banyak platform. Dalam definisi ini, C jelas merupakan bahasa tingkat tinggi.
Keluhannya bukan bahwa penulis sedang bermain kata, melainkan bahwa berpegang pada istilah usang justru mengaburkan pemahaman. Klasifikasi “generasi” biasanya lebih deskriptif.
Generasi pertama adalah bahasa mesin, generasi kedua adalah assembly, generasi ketiga adalah bahasa serbaguna, dan generasi keempat adalah bahasa khusus domain aplikasi.
Batas antara generasi ketiga dan keempat kadang kabur, dan pada era 80-an hingga 90-an juga ada pembicaraan tentang generasi kelima yang pada akhirnya tidak benar-benar mapan. Meski begitu, SQL, HyperCard, dan Mathematica menurut saya adalah contoh bahasa generasi keempat yang cukup jelas.
Alasan pendekatan ini bagus adalah karena bahasa dibagi menurut perbedaan yang relatif jelas tentang kapan bahasa itu digunakan. Setelah itu, “tingkat tinggi/tingkat rendah” bisa dipakai sebagai istilah relatif. Semakin tinggi tingkat sebuah bahasa, semakin banyak detail tentang apa yang sebenarnya dilakukan komputer yang cenderung diabstraksikan. Dengan cara ini, tetap benar bahwa bahasa dari generasi yang lebih tinggi umumnya lebih tingkat tinggi, dan yang hilang hanyalah perdebatan konyol soal garis batas yang sepenuhnya arbitrer dan, terus terang, tidak berguna.
Dengan cara ini, .NET IL, WebAssembly, dan Java bytecode bisa dilihat sebagai bahasa generasi kedua yang sangat tingkat tinggi, dan itu menarik. Dan Forth adalah bahasa generasi ketiga. Kalau Chuck mau menantang, silakan.
Ini bukan soal bagaimana memakai palu, melainkan lebih seperti mempertanyakan apakah cara memakai palu untuk segala hal—yakni desain C—sedang membatasi kita.
Saya tidak setuju dengan klaim penulis bahwa instruction set CPU seharusnya lebih banyak mengekspos implementasi CPU.
Ini sudah pernah dicoba di masa lalu dan gagal dalam jangka panjang. Contohnya adalah branch delay slot pada beberapa prosesor RISC yang dirancang pada akhir 80-an dan awal 90-an, misalnya MIPS dan SuperH. Untuk menjelaskan kepada yang belum tahu konsepnya: instruksi setelah instruksi branch akan dieksekusi terlepas dari apakah branch itu diambil atau tidak.
Dalam jangka pendek, hal ini memungkinkan prosesor dibuat lebih sederhana dan murah dengan menyerahkan tugas menghindari pipeline stall setelah branch kepada programmer. Namun seiring waktu, desain prosesor dan pipeline menjadi lebih kompleks, dan satu instruksi saja tidak cukup untuk menutupi delay branch. Pada akhirnya, demi kompatibilitas, itu menjadi warisan yang harus ditangani prosesor masa depan, dan membuat logika branch prediction serta pipeline menjadi lebih rumit.
Mengekspos detail yang salah tentu saja buruk. Namun yang dikatakan adalah bahwa model C punya keterbatasan besar di dunia CPU modern.
Saya pernah mendengar presentasi tentang para developer yang menggunakan subsistem semacam itu pada suatu prosesor. Jika tidak digunakan, 95% jendela waktu hanya dipakai untuk menyalin data, tetapi dengan engine itu mereka bisa meminta data lebih dulu sehingga pengambilan data hanya memakan 10% jendela waktu, dan menyelesaikan pekerjaan yang diinginkan dalam sekitar 50% dari keseluruhan jendela waktu, menyisakan banyak waktu untuk fitur tambahan dan perbaikan.
Jika x86 punya fitur seperti itu, saya akan memakainya saat program doktoral untuk meminta lebih dulu data matriks yang akan diakses. Pola yang saya pakai tidak linear, tetapi terdefinisi dengan baik. Saat ini, untuk mempercepat kode itu lebih jauh, saya harus menyusun ulang matriks agar disukai prefetcher dan me-refactor seluruh codebase dari atas ke bawah.
Kalau mau, tentu saja sesuatu bisa dirancang dengan buruk, tetapi saya penasaran seberapa banyak contoh historis yang cukup untuk digeneralisasi.
Saya melihat rendah-level hingga tinggi-level bukan sebagai dikotomi, melainkan sebagai spektrum
C bisa dibilang masuk sepertiga bagian bawah di antara bahasa-bahasa pemrograman, dan mengekspos banyak unsur primitif mesin seperti manajemen memori dan thread. Meski tidak serendah assembly, C lebih rendah daripada Java atau Go, dan jelas cukup jauh dari Python atau JavaScript
Selain itu, C cukup tidak cocok untuk platform yang memakai segmented memory atau alamat yang tidak flat. Ada tanda-tanda hal semacam itu mulai ingin populer lagi, dan penyebaran C yang begitu luas benar-benar menjadi penghalang besar untuk itu
Jadi model di kepala saya selalu “C adalah tingkatan paling rendah yang bisa dicapai sebelum memberi instruksi langsung ke prosesor”
“C tidak berperilaku seperti bahasa ‘tingkat tinggi’ yang tipikal. Sebab, C menyediakan berbagai fitur yang lebih sering dikaitkan dengan bahasa ‘tingkat rendah’ seperti bahasa assembly. Ini mencakup kemampuan menulis dan membaca data pada alamat memori tertentu, kemampuan melakukan operasi atas isi lokasi memori, serta instruksi untuk menaikkan dan menurunkan variabel integer … Karena itu, C memberi programmer fleksibilitas dan efisiensi untuk bekerja pada tingkat rendah, sekaligus juga memberi manfaat operasi tingkat tinggi yang khas dalam bahasa komputer masa kini, seperti struktur data yang lebih maju dan kontrol alur program. Karena alasan ini, C kadang digambarkan sebagai ‘bahasa tingkat rendah yang tingkat tinggi’ atau ‘bahasa tingkat tinggi yang tingkat rendah’.” - https://archive.org/details/computerprogramm0000ford/page/13...
Kalimat di akhir tulisan, “dalam pengembangan perangkat lunak ada mitos umum bahwa pemrograman paralel itu sulit,” berpotensi menyesatkan
Penulis memang memberikan situasi spesifik yang tidak sulit, tetapi jika pertanyaannya diterapkan secara umum, pemrograman paralel itu sulit, dan itu bukan mitos umum
Apakah pemrograman paralel sulit? Jika ditanyakan tanpa syarat yang lebih rinci, ya. Mengonseptualisasikan instruksi kode yang berjalan serentak jauh lebih sulit daripada yang berjalan satu per satu secara berurutan
(map inc [0 1 2 3]), apakah tingkat kesulitan mengonseptualisasikan fungsiincdijalankan secara berurutan untuk tiap elemen benar-benar berbeda dari dijalankan secara paralel?Menurut saya kesulitan pemrograman paralel bukan sesuatu yang bawaan, melainkan lebih mendekati dua hal
Pertama, bahasa biasanya menjadikan eksekusi berurutan sebagai default, sehingga untuk melakukan asinkron programmer harus memperkenalkan primitive tambahan
Kedua, kita harus tahu kapan pemrograman paralel efektif digunakan
Jika ada daftar atau stream berisi elemen-elemen independen yang hanya membutuhkan komputasi independen, pemrograman paralel itu intuitif
Titik orang sering tersandung adalah saat memaksakan asinkron di tempat yang tidak membutuhkannya—yakni tempat yang kinerjanya sama atau lebih buruk daripada eksekusi berurutan—atau saat komputasinya sebenarnya saling bergantung tetapi asinkron dimasukkan hingga merusak perilakunya
Saat mengatakan “tanpa detail tambahan atau kekhususan,” sebenarnya yang dipakai sebagai kerangka dasar adalah pandangan dunia C/keluarga C
Inti penulis adalah bahwa pemrograman berurutan hanyalah salah satu jenis pemrograman sederhana, bukan satu-satunya, dan tidak mudah cocok dengan perangkat keras modern
Fakta bahwa Erlang ada dan orang-orang berhasil memakainya bukan berarti sesuatu yang lebih sulit menjadi tidak sulit
Mengimplementasikan algoritma paralel dengan infrastruktur pemrograman konkurensi seperti proses atau thread juga sulit. Namun pemrograman paralel, yaitu membuat banyak elemen pemroses mengerjakan hal yang sama bersama-sama, jauh lebih mudah jika ada abstraksi yang tepat
Namun beberapa use case seperti perkalian matriks adalah pengecualian
Tulisan ini benar dalam hal bahwa komputer bukan PDP-11 yang cepat, tetapi keliru dalam hal bahwa itu terkait dengan C
Misalnya ada kalimat “Memori flat, inti lain dari model memori mesin abstrak C. Ini sudah tidak benar selama lebih dari 20 tahun”
Ini tidak ada hubungannya dengan C. Perangkat keraslah yang memaksakan abstraksi ini. Dan itu hal yang baik. Kalau tidak, program akan berhenti saat dipindahkan ke mesin dengan cache berbeda
Contohnya struktur memori hierarkis yang berpura-pura sebagai RAM flat, CPU yang jauh lebih besar daripada yang disiratkan instruction set serta eksekusi out-of-order dan spekulatif, dan compiler pengoptimasi yang semakin memisahkan program yang ditulis dari eksekusi sebenarnya
IBM sudah mengerjakan hal-hal ini pada 1970-an, jauh sebelum C naik daun. Mengkritik model ini dan mencari alternatif itu sah, tetapi menyalahkan C tidak adil
Tulisan ini kini sudah berusia 5 tahun, dan premis bahwa komputer secara struktural tidak terlalu mirip dengan PDP-11 justru makin benar, tetapi kesimpulan “mari membayangkan prosesor yang bukan C” terasa kurang kuat
Kita sedang melihat pemisahan yang kuat antara kode linear dan kode yang sangat paralel, dan pada 2018 pun hal itu sudah terjadi. Contoh paling jelas adalah naiknya Python dalam machine learning dan komputasi ilmiah. Ketika performa bukan prioritas utama, menulis dengan gaya single-thread dan model memori datar masih sangat nyaman
Ketika performa menjadi penting, beralih ke bahasa yang lebih cocok untuk pemrograman paralel adalah langkah yang tepat. Bahasa grafik komputasi pada hal seperti Pytorch, kumpulan primitive lain di atas CUDA, atau bahasa yang lebih eksperimental seperti Futhark termasuk di sini. Kode inti performa selalu menggunakan bahasa domain-specific, dan tampaknya bahasa-bahasa itu makin umum, bukan makin berkurang. Hardware juga dibuat untuk menyesuaikan diri dengan ini. Contohnya adalah kombinasi CPU+GPU yang umum di PC desktop, ekstensi vektor x86 dengan primitive yang pada dasarnya membentuk DSL sendiri, serta hal seperti M1 yang menempelkan GPU ke CPU sehingga keduanya dapat mengakses memori sistem yang sama dengan cepat
Dengan kata lain, yang benar-benar usang mungkin bukan C, melainkan konsep bahasa serbaguna yang sama-sama cocok untuk semua jenis pekerjaan
Jika karena kecanggihan CPU modern C tidak lagi menjadi bahasa “level rendah”, logika yang sama juga berlaku untuk bahasa assembly
Karena hal-hal seperti out-of-order execution dan register renaming juga berlaku pada assembly
Makin canggihnya compiler dalam beberapa dekade terakhir juga memperkuat argumen ini. Assembly yang dihasilkan compiler C, yaitu object code sekalipun, bisa keluar berbeda dari perkiraan karena loop hoisting, common subexpression elimination, dan sebagainya
Meski begitu, menurut saya konsep menyebut C sebagai bahasa “level rendah” masih merupakan label yang berguna. Kalau tidak, istilah itu sendiri harus dipensiunkan
Memang benar ia adalah abstraksi di atas komputer nyata, tetapi jauh lebih sedikit dibandingkan apa yang C bangun di atas model komputer virtual. Assembly saat ini kira-kira berada pada tingkat yang mirip dengan C ketika C dibuat. C saat ini terlalu high-level sehingga tidak menawarkan kemampuan yang tidak bisa didapat dari bahasa yang lebih baik dan modern
Namun saya setuju bahwa sekarang nama “level rendah” dan “level tinggi” tidak terlalu berguna
Tulisan ini tampaknya mengembangkan dua alur argumen yang sulit dipadukan satu sama lain
Pertama adalah klaim bahwa C bukan bahasa level rendah, dengan contoh seperti padding pada struct dan overflow integer bertanda sebagai undefined behavior. Bagian ini bisa dipahami, dan terlihat konstruktif karena mengusulkan fitur bahasa untuk bahasa “benar-benar level rendah” yang hipotetis
Kedua adalah klaim bahwa karena dominasi C, para perancang CPU harus berusaha keras membuat sesuatu yang menjalankan C secara alami. Contohnya mencakup register renaming, memori datar, dan caching. Argumen ini juga bisa dipahami, tetapi saya kurang yakin bagaimana kaitannya dengan argumen pertama dan konteks judul tulisan. Jika diterima secara harfiah, tampaknya berarti pada hardware modern membuat bahasa level rendah itu sendiri mustahil, bahkan machine code pun “high-level”. Maka kesimpulannya adalah kita harus terlebih dahulu membuat generasi hardware baru yang mengekspos jauh lebih banyak kompleksitas ke instruction set architecture, baru kemudian bisa merancang bahasa level rendah yang memanfaatkannya
Kedua argumen itu sama-sama bernilai, tetapi menaruhnya bersama dalam satu tulisan dan memberi judul “C bukan bahasa level rendah” terasa agak goyah. Argumen pertama cocok dengan judul itu, sedangkan argumen kedua sepertinya lebih baik dibahas dalam tulisan lanjutan berjudul “Machine code juga bukan bahasa level rendah”
Namun saya dengar waktu kompilasinya lama, dan compiler akhirnya tidak pernah mencapai tingkat optimisasi yang diharapkan. Fakta bahwa ia tidak kompatibel dengan x86 juga tidak membantu adopsinya
VLIW terlintas di pikiran. Menurut artikel Itanium di Wikipedia, isinya sebagai berikut
“Satu word instruksi VLIW dapat memuat beberapa instruksi independen yang bisa dieksekusi secara paralel tanpa evaluasi independensi. Compiler harus berusaha menemukan kombinasi instruksi valid yang dapat dieksekusi secara bersamaan; pada dasarnya, ia melakukan instruction scheduling yang pada prosesor superskalar konvensional dilakukan oleh hardware saat runtime.”
Jika CPU mengekspos paralelisme aliran tunggal pada antarmukanya, itu bisa ditangani saat kompilasi atau diputuskan langsung lewat inline assembly
Saya penasaran apakah alasan ini tidak pernah mapan adalah dinamika bisnis industri, atau apakah ada alasan teknis yang memang membuat strategi ini kurang bagus
Pertama, compiler tidak cukup baik melakukan jenis instruction scheduling seperti itu, dan ketika kemudian membaik, Itanium sudah keburu tenggelam. Kedua, instruction set yang sudah ada, yaitu x86, menjadi cukup baik menangani hal ini dengan hardware saat runtime, dan dalam praktiknya memberi hasil yang sedikit lebih baik daripada scheduling statis. Karena saat runtime ada data profiling
Menurut saya Linus pernah menulis rant bagus yang sedikit terkait topik ini di [0]. “Sementara orang-orang RISC berusaha mengoptimalkan compiler agar membuat loop yang memakai semua 32 register secara efisien, para implementor x86 justru membuat chip berjalan cepat pada berbagai beban dan menggunakan hardware register renaming yang sangat besar. Mereka juga melihat memory renaming.”
[0] https://yarchive.net/comp/linux/x86.html
VLIW benar-benar bekerja baik di beberapa ceruk. Lebih sulit diprogram, baik manual maupun oleh compiler, dibanding satu instruksi yang dieksekusi berurutan, tetapi menyederhanakan scheduling di hardware. Ini bekerja lebih baik jika latensi instruksi-instruksi yang dibundel mirip
Teka-teki desain utama saat ini adalah akses memori memakan jauh lebih banyak siklus daripada aritmetika. Tidak banyak gunanya membundel aritmetika beberapa siklus dengan load memori ratusan siklus. Jadi VLIW bekerja baik ketika kita tahu akses memori cepat, kira-kira ketika tahu datanya akan muat di cache L1 atau yang setara. Saya pikir itu salah satu alasan mengapa ia cocok untuk sistem bergaya DSP
Pipeline yang diekspos juga merupakan karakteristik menarik dari sebagian sistem semacam ini. Jika satu instruksi dalam bundle VLIW menulis ke register, instruksi-instruksi berikutnya yang membaca register yang sama akan melihat nilai lama selama N siklus berikutnya, dan baru setelah itu write tersebut terlihat. Ini benar-benar membingungkan jika diprogram manual, tetapi compiler bisa menangani scheduling semacam itu
Hingga belum lama ini, DSP dan HPC hanya bagian yang sangat kecil dari pasar, sehingga arsitektur yang mampu melakukan scheduling dinamis mendapat lebih banyak investasi dan akhirnya mendominasi juga pasar-pasar itu
Di GPU, tentu situasinya berbeda, dan GPU memang lebih banyak mengandalkan scheduling statis. Namun seiring GPU meluas ke beban yang lebih beragam, GPU juga makin mendapatkan unsur yang lebih dinamis
https://news.ycombinator.com/context?id=37900987
Itanium adalah upaya besar untuk merilis ini sebagai CPU. Sekarang AMD64 dan ARM mendominasi, tetapi mungkin saja kita akan melihatnya lagi di masa depan