Cliff Click adalah CTO Cratus (sensor IoT untuk peningkatan proses), pendiri dan salah satu pendiri beberapa startup (termasuk Rocket Realtime School, Neurensic dan H2O.ai) dengan beberapa pintu keluar yang sukses. Cliff menulis kompiler pertamanya pada usia 15 (Pascal untuk TRS Z-80)! Terkenal karena bekerja pada C2 di Jawa (Lautan IR). Kompiler ini menunjukkan kepada dunia bahwa JIT dapat menghasilkan kode berkualitas tinggi, yang telah menjadi salah satu faktor dalam menjadikan Java salah satu platform perangkat lunak modern utama. Cliff kemudian membantu Azul Systems membangun mainframe 864-core dengan perangkat lunak Java murni yang mendukung GC berhenti pada tumpukan 500-gigabyte selama 10 milidetik. Secara umum, Cliff berhasil mengerjakan semua aspek JVM.
Hubrapost ini adalah wawancara hebat dengan Cliff. Kami akan berbicara tentang topik-topik berikut:
- Transisi ke optimasi tingkat rendah
- Cara melakukan banyak refactoring
- Model biaya
- Pelatihan optimasi tingkat rendah
- Studi Kasus Peningkatan Produktivitas
- Mengapa membuat bahasa pemrograman Anda sendiri
- Karier Insinyur Kinerja
- Tantangan Teknis
- Sedikit tentang alokasi register dan multicore
- Tantangan terbesar dalam hidup
Wawancara dilakukan oleh:
- Andrey Satarin dari Amazon Web Services. Dalam karirnya, ia berhasil bekerja di proyek yang sama sekali berbeda: ia menguji database NewSQL yang didistribusikan di Yandex, sistem deteksi cloud di Kaspersky Lab, permainan multi-pengguna di Mail.ru dan layanan perhitungan pertukaran mata uang di Deutsche Bank. Dia tertarik untuk menguji sistem backend dan distribusi skala besar.
- Vladimir Sitnikov dari Netcracker. Selama sepuluh tahun, ia telah bekerja pada kinerja dan skalabilitas NetCracker OS, perangkat lunak yang digunakan oleh operator telekomunikasi untuk mengotomatisasi proses manajemen jaringan dan peralatan jaringan. Dia tertarik dengan masalah kinerja Java dan Oracle Database. Penulis peningkatan kinerja lebih dari selusin di driver JDBC PostgreSQL resmi.
Transisi ke optimasi tingkat rendah
Andrei : Anda adalah orang terkenal di dunia kompilasi JIT, di Jawa dan bekerja pada kinerja secara umum, bukan?
Cliff : Itu dia!
Andrew : Mari kita mulai dengan pertanyaan umum tentang bekerja pada kinerja. Apa pendapat Anda tentang pilihan antara optimasi level tinggi dan level rendah seperti bekerja di level CPU?
Cliff : Mudah. Kode tercepat adalah kode yang tidak pernah berjalan. Oleh karena itu, Anda selalu harus mulai dari level tinggi, bekerja pada algoritma. Notasi-O yang lebih baik akan mengalahkan notasi-O yang lebih buruk, kecuali beberapa konstanta yang cukup besar melakukan intervensi. Hal-hal tingkat rendah adalah yang terbaru. Biasanya, jika Anda mengoptimalkan sisa tumpukan dengan cukup baik, dan masih ada sesuatu yang menarik yang tersisa - ini adalah level rendah. Tetapi bagaimana memulai dari level yang tinggi? Bagaimana cara mengetahui bahwa cukup banyak pekerjaan telah dilakukan pada level tinggi? Yah ... tidak mungkin. Tidak ada resep yang sudah jadi. Anda perlu memahami masalahnya, memutuskan apa yang akan Anda lakukan (agar tidak membuat langkah yang tidak perlu di masa mendatang) dan kemudian Anda dapat menemukan profiler yang dapat mengatakan sesuatu yang bermanfaat. Pada titik tertentu, Anda sendiri mengerti bahwa Anda telah menyingkirkan hal-hal yang tidak perlu dan inilah saatnya untuk memperbaiki level rendah. Ini jelas merupakan jenis seni khusus. Banyak orang melakukan hal-hal yang tidak perlu, tetapi bergerak sangat cepat sehingga mereka tidak punya waktu untuk peduli dengan kinerja. Tapi ini asalkan pertanyaannya tidak berdiri tegak. Biasanya, 99% dari waktu tidak ada yang tertarik pada apa yang saya lakukan, sampai saat ketika hal penting yang dipedulikan seseorang tidak berada di jalur kritis. Dan di sini semua orang mulai mengomel Anda pada topik "mengapa itu tidak bekerja dengan sempurna sejak awal." Secara umum, selalu ada sesuatu untuk meningkatkan kinerja. Tapi 99% dari waktu Anda tidak memiliki petunjuk! Anda hanya berusaha mendapatkan sesuatu untuk bekerja dan dalam prosesnya Anda memahami apa yang penting. Anda tidak akan pernah tahu sebelumnya bahwa karya ini harus dibuat sempurna, oleh karena itu, pada dasarnya, Anda harus sempurna dalam segala hal. Dan ini tidak mungkin, dan Anda tidak melakukannya. Selalu ada banyak hal untuk diperbaiki - dan itu sangat normal.
Cara melakukan banyak refactoring
Andrew : Bagaimana Anda mengerjakan kinerja? Ini adalah masalah lintas sektoral. Misalnya, apakah Anda harus mengerjakan masalah yang timbul dari persimpangan sejumlah besar fungsi yang ada?
Cliff : Saya mencoba menghindari ini. Jika saya tahu bahwa kinerja akan menjadi masalah, maka saya memikirkannya sebelum saya mulai coding, terutama pada struktur data. Tetapi seringkali Anda menemukan semua ini nanti. Dan kemudian Anda harus melakukan langkah-langkah ekstrem dan melakukan apa yang saya sebut "menulis ulang dan menaklukkan": Anda perlu mengambil bagian yang cukup besar. Bagian dari kode masih harus ditulis ulang karena masalah kinerja atau sesuatu yang lain. Apa pun alasan untuk menulis ulang kode, hampir selalu lebih baik untuk menulis ulang bongkahan yang lebih besar daripada bongkahan yang lebih kecil. Pada saat ini, semua orang mulai gemetar ketakutan: "Ya Tuhan, Anda tidak dapat menyentuh begitu banyak kode!" Tetapi, pada kenyataannya, pendekatan ini hampir selalu bekerja lebih baik. Anda harus segera mengambil masalah besar, menggambar lingkaran besar di sekitarnya dan berkata: Saya akan menulis ulang semua yang ada di dalam lingkaran. Perbatasan jauh lebih kecil dari konten di dalamnya yang perlu diganti. Dan jika penggambaran batas seperti itu memungkinkan Anda melakukan pekerjaan di dalam dengan sempurna - tangan Anda dilepaskan, lakukan apa yang Anda inginkan. Setelah Anda memahami masalahnya, proses penulisan ulang jauh lebih mudah, jadi gigitlah!
Pada saat yang sama, ketika Anda menulis ulang dalam potongan besar dan memahami bahwa kinerja akan menjadi masalah, Anda dapat segera mulai mengkhawatirkannya. Biasanya ini berubah menjadi hal-hal sederhana seperti "jangan menyalin data, mengelola data sesederhana mungkin, membuatnya lebih kecil". Dalam penulisan ulang besar, ada cara standar untuk meningkatkan kinerja. Dan mereka hampir selalu berputar di sekitar data.
Model biaya
Andrew : Di salah satu podcast, Anda berbicara tentang model biaya dalam konteks produktivitas. Bisakah Anda menjelaskan apa yang dimaksud dengan ini?
Cliff : Tentu saja. Saya lahir di era ketika kinerja prosesor sangat penting. Dan era ini kembali lagi - nasib bukan tanpa ironi. Saya mulai hidup di zaman mesin delapan-bit, komputer pertama saya bekerja dengan 256 byte. Ini adalah byte. Semuanya sangat kecil. Kami harus membaca instruksinya dan segera setelah kami mulai naik tumpukan bahasa pemrograman, bahasa-bahasa itu semakin bertambah. Ada Assembler, lalu Basic, lalu C, dan C mengambil alih pekerjaan dengan banyak detail, seperti alokasi register dan pemilihan instruksi. Tapi semuanya sudah cukup jelas di sana, dan jika saya membuat pointer ke contoh variabel, maka saya akan mendapatkan beban, dan biayanya diketahui untuk instruksi ini. Besi menghasilkan sejumlah siklus mesin yang diketahui, sehingga kecepatan eksekusi berbagai bagian dapat dihitung hanya dengan menambahkan semua instruksi yang akan Anda jalankan. Setiap perbandingan / uji / cabang / panggilan / memuat / toko dapat dilipat dan berkata: di sini Anda memiliki waktu memimpin. Saat Anda meningkatkan kinerja, Anda pasti akan memperhatikan angka seperti apa yang berhubungan dengan siklus panas kecil.
Tetapi begitu Anda beralih ke Java, Python, dan hal-hal serupa, Anda dengan cepat berpindah dari setrika level rendah. Berapa biaya panggilan getter di Jawa? Jika JIT di HotSpot benar digarisbawahi , itu akan dimuat, tetapi jika tidak, itu akan menjadi panggilan fungsi. Karena tantangannya terletak pada loop panas, itu akan membatalkan semua optimasi lainnya dalam loop ini. Karena itu, nilai sebenarnya akan jauh lebih besar. Dan Anda segera kehilangan kemampuan untuk melihat sepotong kode dan memahami bahwa kita harus menjalankannya dalam hal kecepatan jam prosesor, memori yang digunakan dan cache. Semua ini menjadi menarik hanya jika Anda benar-benar mabuk dalam kinerja.
Sekarang kita berada dalam situasi di mana kecepatan prosesor hampir tidak tumbuh selama satu dekade. Masa lalu kembali! Anda tidak dapat lagi mengandalkan kinerja single-threaded yang bagus. Tetapi jika Anda tiba-tiba terlibat dalam komputasi paralel - itu sangat sulit, semua orang memandang Anda sebagai James Bond. Percepatan sepuluh kali lipat di sini biasanya terjadi di tempat-tempat seseorang menampar sesuatu. Konkurensi membutuhkan banyak pekerjaan. Untuk mendapatkan akselerasi sepuluh kali lipat yang sama, Anda harus memahami model biaya. Apa dan berapa biayanya. Dan untuk ini, Anda perlu memahami bagaimana lidah diletakkan pada besi yang mendasarinya.
Martin Thompson memiliki kata yang bagus untuk blog Simpati Mekanisnya ! Anda perlu memahami apa yang akan dilakukan besi, bagaimana tepatnya akan melakukannya, dan mengapa umumnya melakukan apa yang dilakukannya. Dengan menggunakan ini, cukup mudah untuk mulai membaca instruksi dan mencari tahu di mana waktu eksekusi mengalir. Jika Anda tidak memiliki pelatihan yang sesuai, Anda hanya mencari kucing hitam di ruangan gelap. Saya terus-menerus melihat orang-orang mengoptimalkan kinerja yang tidak tahu apa yang sedang mereka lakukan. Mereka sangat tersiksa dan tidak benar-benar pergi ke suatu tempat. Dan ketika saya mengambil potongan kode yang sama, menyelipkan beberapa retasan kecil di sana dan mendapatkan lima atau sepuluh kali percepatan, mereka seperti ini: yah, sangat tidak jujur, kami sudah tahu bahwa Anda lebih baik. Luar biasa. Apa yang saya bicarakan ... model biaya adalah tentang kode apa yang Anda tulis dan seberapa cepat kerjanya rata-rata dalam keseluruhan gambar.
Andrew : Dan bagaimana cara menyimpan volume sebesar itu di kepala Anda? Apakah ini dicapai dengan lebih banyak pengalaman, atau? Di mana pengalaman seperti itu diperoleh?
Cliff : Yah, pengalaman saya bukanlah cara termudah. Saya diprogram dalam Assembler pada saat memungkinkan untuk memahami setiap instruksi individu. Kedengarannya konyol, tetapi sejak itu di kepala saya, dalam ingatan saya, set instruksi Z80 tetap selamanya. Saya tidak ingat nama orang satu menit setelah percakapan, tapi saya ingat kode yang ditulis 40 tahun yang lalu. Lucu, kelihatannya seperti sindrom " idiot terpelajar ".
Pelatihan optimasi tingkat rendah
Andrew : Apakah ada cara yang lebih sederhana untuk masuk ke bisnis?
Cliff : Ya dan tidak. Setrika yang kita semua gunakan tidak banyak berubah selama ini. Semua orang menggunakan x86, kecuali untuk smartphone Arm. Jika Anda tidak melakukan penyisipan hardcore, Anda memiliki hal yang sama. Ok selanjutnya. Instruksi juga tidak berubah selama berabad-abad. Anda harus pergi dan menulis sesuatu di Assembler. Sedikit, tetapi cukup untuk mulai mengerti. Anda tersenyum, tapi saya benar-benar serius. Penting untuk memahami korespondensi bahasa dan besi. Setelah itu, Anda perlu pergi, buang air kecil dan buat kompiler mainan kecil untuk bahasa mainan kecil. "Mainan" berarti Anda harus membuatnya dalam jumlah waktu yang wajar. Ini bisa sangat sederhana, tetapi harus menghasilkan instruksi. Tindakan menghasilkan instruksi akan memungkinkan kita untuk memahami model biaya untuk jembatan antara kode tingkat tinggi di mana setiap orang menulis dan kode mesin yang berjalan pada perangkat keras. Korespondensi ini akan terbakar di otak pada saat penulisan kompiler. Bahkan kompiler paling sederhana. Setelah itu, Anda dapat mulai melihat Jawa dan fakta bahwa ia memiliki celah semantik yang lebih dalam, dan membangun jembatan di atasnya jauh lebih sulit. Di Jawa, jauh lebih sulit untuk memahami apakah jembatan kita ternyata baik atau buruk, yang akan membuatnya berantakan dan tidak. Tetapi Anda perlu titik awal ketika Anda melihat kode dan memahami: "Ya, rajin rajin harus inline setiap waktu". Dan kemudian ternyata kadang-kadang ini terjadi, dengan pengecualian situasi ketika metode menjadi terlalu besar dan JIT mulai menyatukan semuanya. Kinerja tempat-tempat tersebut dapat diprediksi secara instan. Biasanya getter bekerja dengan baik, tetapi kemudian Anda melihat loop panas besar dan menyadari bahwa ada beberapa jenis panggilan fungsi mengambang di yang tidak tahu apa yang mereka lakukan. Ini adalah masalah dengan meluasnya penggunaan getter, alasan mengapa mereka tidak sejalan - tidak jelas apakah ini rajin rajin. Jika Anda memiliki basis kode yang sangat kecil, Anda dapat mengingatnya lalu berkata: ini adalah pengambil, tetapi ini adalah setter. Dalam basis kode besar, setiap fungsi menjalankan kisahnya sendiri, yang, secara umum, tidak diketahui siapa pun. Profiler mengatakan bahwa kita kehilangan 24% dari waktu kita pada beberapa jenis siklus, dan untuk memahami apa yang siklus ini lakukan, kita perlu melihat setiap fungsi di dalamnya. Tidak mungkin untuk memahami ini tanpa mempelajari fungsinya, dan ini secara serius memperlambat proses pemahaman. Itu sebabnya saya tidak menggunakan getter dan setter, saya naik ke level baru!
Di mana mendapatkan model biaya? Ya, Anda dapat membaca sesuatu, tentu saja ... Tapi saya pikir cara terbaik adalah bertindak. Buat kompiler kecil dan ini akan menjadi cara terbaik untuk merealisasikan model biaya dan memasangnya di kepala Anda sendiri. Kompiler kecil yang bisa digunakan untuk pemrograman gelombang mikro adalah tugas untuk pemula. Ya, maksud saya, jika Anda sudah memiliki keterampilan pemrograman, maka itu sudah cukup. Semua hal ini seperti parsing string, yang Anda akan memiliki semacam ekspresi aljabar, mengeluarkan instruksi operasi matematika dari sana dalam urutan yang benar, mengambil nilai yang benar dari register - semua ini dilakukan sekaligus. Dan sementara Anda akan melakukannya, itu akan tercetak di otak. Saya pikir semua orang tahu apa yang dilakukan kompiler. Dan ini akan memberikan pemahaman tentang model biaya.
Studi Kasus Peningkatan Produktivitas
Andrew : Apa lagi yang perlu diperhatikan ketika mengerjakan kinerja?
Tebing : Struktur Data. Ngomong-ngomong, ya, saya sudah lama tidak mengajar kelas-kelas ini ... Sekolah Rocket . Itu lucu, tetapi butuh banyak upaya untuk berinvestasi, dan saya juga punya kehidupan! Baiklah Jadi, di salah satu kelas besar dan menarik, "Kemana perginya kinerja Anda," saya memberi contoh kepada siswa: dua setengah gigabyte data fintech dibaca dari file CSV dan kemudian kami harus menghitung jumlah produk yang terjual. Centang data pasar reguler. Paket UDP dikonversi ke format teks sejak 70-an. Chicago Mercantile Exchange adalah segala macam hal seperti mentega, jagung, kedelai, dan sejenisnya. Itu perlu untuk menghitung produk-produk ini, jumlah transaksi, volume rata-rata pergerakan dana dan barang, dll. Ini adalah matematika perdagangan yang cukup sederhana: cari kode produk (ini adalah 1-2 karakter di tabel hash), dapatkan jumlahnya, tambahkan ke salah satu set transaksi, tambahkan volume, tambah nilai, dan beberapa hal lainnya. Matematika yang sangat sederhana. Implementasi mainan sangat mudah: semuanya ada di file, saya membaca file dan bergerak di sekitarnya, memisahkan entri individu menjadi string Java, mencari hal-hal yang diperlukan di dalamnya dan melipatnya sesuai dengan matematika yang dijelaskan di atas. Dan itu bekerja pada kecepatan rendah.
Dengan pendekatan ini, semuanya jelas apa yang terjadi, dan komputasi paralel tidak akan membantu di sini, kan? Ternyata peningkatan lima kali lipat dalam produktivitas hanya dapat dicapai dengan memilih struktur data yang tepat. Dan ini bahkan mengejutkan programmer yang berpengalaman! Dalam kasus khusus saya, triknya adalah Anda tidak boleh melakukan alokasi memori dalam hot loop. Nah, ini bukan keseluruhan kebenaran, tetapi secara umum - Anda tidak harus menyoroti "satu kali dalam X" ketika X cukup besar. Ketika X adalah dua setengah gigabytes, Anda tidak boleh mengalokasikan apa pun "sekali per huruf", atau "sekali per baris", atau "sekali per bidang", tidak seperti itu. Itulah tepatnya yang membutuhkan waktu. Bagaimana cara kerjanya? Bayangkan melakukan panggilan ke String.split()
atau BufferedReader.readLine()
. Readline
membuat garis dari sekumpulan byte yang datang melalui jaringan, satu kali untuk setiap baris, untuk masing-masing dari ratusan juta baris. Saya mengambil baris ini, parse et dan membuangnya. Mengapa membuangnya - yah, saya sudah memprosesnya, itu saja. Jadi, untuk setiap byte yang dibaca dari 2.7G ini, dua karakter akan ditulis dalam baris, yaitu, 5.4G sudah, dan saya tidak membutuhkannya lagi, oleh karena itu mereka dibuang. Jika Anda melihat bandwidth memori, kami memuat 2.7G, yang melewati memori dan bus memori di prosesor, dan kemudian dua kali lebih banyak dikirim ke garis yang terletak di memori, dan semua ini terhapus saat membuat setiap baris baru. Tetapi saya perlu membacanya, setrika membacanya, bahkan jika semuanya akan terhapus. Dan saya harus menuliskannya, karena saya membuat baris dan cache penuh - cache tidak dapat memuat 2.7G. Secara total, untuk setiap byte yang dibaca, saya membaca dua byte lagi dan menulis dua byte tambahan, dan sebagai hasilnya mereka memiliki rasio 4: 1 - dalam rasio ini kita membuang bandwidth memori. Dan kemudian ternyata jika saya melakukan String.split()
, maka saya melakukan ini bukan yang terakhir kali, mungkin ada 6-7 bidang di dalamnya. Oleh karena itu, kode pembacaan CSV klasik diikuti oleh penguraian baris menyebabkan hilangnya bandwidth memori di wilayah 14: 1 relatif terhadap apa yang benar-benar ingin Anda miliki. Jika Anda membuang sekresi ini, Anda bisa mendapatkan akselerasi lima kali lipat.
Dan itu tidak terlalu sulit. Jika Anda melihat kode dari sudut kanan, semuanya menjadi sangat sederhana, segera setelah Anda menyadari esensi masalahnya. Bahkan tidak berhenti mengalokasikan memori sama sekali: satu-satunya masalah adalah Anda mengalokasikan sesuatu dan segera mati, dan membakar sumber daya penting di sepanjang jalan, yang dalam hal ini adalah bandwidth memori. Dan semua ini menghasilkan penurunan produktivitas. Pada x86, Anda biasanya perlu secara aktif membakar jam prosesor, dan di sini Anda membakar semua memori lebih awal. Solusi - Anda perlu mengurangi jumlah debit.
Bagian lain dari masalah adalah bahwa jika Anda memulai profiler ketika setrip memori telah berakhir, tepat pada saat ini terjadi, Anda biasanya menunggu cache kembali, karena penuh dengan sampah yang baru saja Anda buat dengan semua baris ini. Karenanya, setiap operasi pemuatan atau penyimpanan menjadi lambat, karena menyebabkan kesalahan dalam cache - seluruh cache menjadi lambat, menunggu sampah meninggalkannya. Oleh karena itu, profiler hanya akan menampilkan suara acak hangat yang dioleskan di sepanjang siklus - tidak akan ada instruksi atau tempat terpisah yang panas dalam kode. Hanya suara berisik. Dan jika Anda melihat siklus GC, mereka semua akan menjadi Generasi Muda dan super cepat - mikrodetik atau milidetik maksimum. Lagipula, semua memori ini langsung mati. Anda mengalokasikan miliaran gigabyte, dan itu memotong mereka, dan memotong, dan memotongnya lagi. Semua ini terjadi dengan sangat cepat. Ternyata ada siklus GC murah, kebisingan hangat di sepanjang siklus, tapi kami ingin mendapatkan akselerasi 5x. Pada saat itu, sesuatu harus menutup di kepalaku dan terdengar: "kenapa begitu?!" Bandwidth overflow tidak muncul di debugger klasik, Anda harus menjalankan debugger penghitung kinerja perangkat keras dan melihatnya sendiri dan langsung. Dan tidak secara langsung, bisa dicurigai dari ketiga gejala ini. Gejala ketiga adalah ketika Anda melihat apa yang Anda sorot, tanyakan pada profiler, dan dia menjawab: "Anda membuat satu miliar baris, tetapi GC bekerja secara gratis." Segera setelah ini terjadi, Anda menyadari bahwa Anda telah menelurkan terlalu banyak objek dan membakar seluruh keping memori. Ada cara untuk mencari tahu ini, tetapi tidak jelas.
Masalahnya adalah dalam struktur data: struktur telanjang di balik segala sesuatu yang terjadi, terlalu besar, 2.7G pada disk, jadi membuat salinan dari hal ini sangat tidak diinginkan - saya ingin memuatnya dari buffer byte jaringan segera ke register agar tidak membaca-menulis ke string bolak-balik lima kali. Sayangnya, Java secara default tidak memberi Anda perpustakaan seperti itu sebagai bagian dari JDK. Tapi ini sepele, bukan? Bahkan, ini adalah 5-10 baris kode yang akan digunakan untuk mengimplementasikan loader line buffered Anda sendiri, yang mengulangi perilaku kelas baris, sambil menjadi pembungkus di sekitar buffer byte yang mendasarinya. Akibatnya, ternyata Anda bekerja hampir seolah-olah dengan string, tetapi sebenarnya ada pointer bergerak ke buffer, dan byte mentah tidak disalin di mana saja, dan dengan demikian buffer yang sama digunakan kembali, berulang-ulang, dan sistem operasi senang untuk mengambil hal-hal yang dimaksudkan, seperti penyangga ganda tersembunyi dari buffer byte ini, dan Anda sendiri tidak lagi menggiling aliran data yang tidak perlu tanpa akhir. Ngomong-ngomong, Anda mengerti, ketika bekerja dengan GC, dijamin bahwa setiap alokasi memori tidak akan terlihat oleh prosesor setelah siklus GC terakhir? Oleh karena itu, semua ini tidak mungkin berada dalam cache, dan kemudian kehilangan 100% dijamin terjadi. Ketika bekerja dengan pointer pada x86, mengurangi register dari memori membutuhkan 1-2 siklus, dan segera setelah ini terjadi, Anda membayar, membayar, membayar, karena semua memori ada pada cache NINE - dan ini adalah biaya mengalokasikan memori. Nilai sekarang.
Dengan kata lain, struktur data adalah yang paling sulit diubah. Dan segera setelah Anda menyadari bahwa Anda telah memilih struktur data yang salah yang akan membunuh produktivitas di masa depan, Anda biasanya perlu memutar pekerjaan penting, tetapi jika tidak, maka itu akan menjadi lebih buruk. Pertama-tama, Anda perlu berpikir tentang struktur data, ini penting. Biaya utama di sini terletak pada struktur data yang tebal, yang mulai mereka gunakan dengan gaya "Saya menyalin struktur data X ke dalam struktur data Y, karena saya menyukai bentuk yang lebih baik." Tetapi operasi penyalinan (yang tampaknya murah) sebenarnya menghabiskan satu set memori dan di sini semua runtime yang hilang dikubur. Jika saya memiliki string raksasa dengan JSON dan saya ingin mengubahnya menjadi pohon DOM terstruktur dari POJO atau sesuatu seperti itu, pengoperasian parsing string ini dan membangun POJO, dan kemudian panggilan baru ke POJO di masa depan akan berubah menjadi tidak berharga - itu bukan hal yang mahal. Kecuali jika Anda akan berjalan di POJO lebih sering daripada di telepon. Sebaliknya, Anda dapat mencoba mendekripsi string dan hanya menarik apa yang Anda butuhkan darinya, tanpa mengubahnya menjadi POJO. Jika semua ini terjadi pada jalur dari mana kinerja maksimum diperlukan, tidak ada POJO untuk Anda - Anda harus entah bagaimana menggali langsung ke dalam saluran.
Mengapa membuat bahasa pemrograman Anda sendiri
Andrei : Anda mengatakan bahwa untuk memahami model biaya, Anda perlu menulis bahasa kecil kecil Anda sendiri ...
Cliff : Bukan bahasa, tapi kompiler. Bahasa dan kompiler adalah dua hal yang berbeda. Perbedaan paling penting adalah di kepala Anda.
Andrei : Ngomong-ngomong, sejauh yang saya tahu, Anda bereksperimen dengan menciptakan bahasa Anda sendiri. Mengapa
Cliff : Karena saya bisa! Saya setengah pensiun, jadi ini hobi saya. Saya telah menerapkan bahasa orang lain sepanjang hidup saya. Saya juga bekerja keras pada gaya pengkodean. Dan juga karena saya melihat masalah dalam bahasa lain. Saya melihat bahwa ada cara yang lebih baik untuk melakukan hal-hal yang biasa. Dan saya akan menggunakannya. Saya bosan melihat masalah dalam diri saya, di Jawa, dengan Python, dalam bahasa apa pun. Saya menulis tentang React Native, JavaScript dan Elm sebagai hobi, yang bukan tentang pensiun, tetapi tentang pekerjaan aktif. Dan saya juga menulis dengan Python dan, kemungkinan besar, saya akan terus bekerja pada pembelajaran mesin untuk backend Java. Ada banyak bahasa populer dan semuanya memiliki fitur menarik. Setiap orang pandai melakukan sesuatu sendiri dan Anda dapat mencoba menyatukan semua chip ini. Jadi, saya mempelajari hal-hal yang menarik bagi saya, perilaku bahasanya, mencoba memunculkan semantik yang masuk akal. Dan sejauh ini saya melakukannya! Saat ini, saya berjuang dengan semantik memori, karena saya ingin memiliki keduanya di C dan Java, dan mendapatkan model memori yang kuat dan semantik memori untuk memuat dan menyimpan. Pada saat yang sama, memiliki inferensi tipe otomatis seperti di Haskell. Di sini, saya mencoba untuk mencampur inferensi tipe Haskell seperti dengan memori yang bekerja di C dan Java. Saya telah melakukan ini selama 2-3 bulan terakhir, misalnya.
Andrei : Jika Anda membangun bahasa yang mengambil aspek yang lebih baik dari bahasa lain, apakah Anda berpikir seseorang akan melakukan yang sebaliknya: ambil ide Anda dan gunakan?
Cliff : Begitulah bahasa baru muncul! Mengapa Java mirip dengan C? Karena C memiliki sintaksis yang baik yang dipahami semua orang dan Java terinspirasi oleh sintaksis ini, menambahkan keamanan tipe, memeriksa batas-batas array, GC, dan mereka juga meningkatkan beberapa hal dari C. Mereka menambahkan milik mereka. Tapi mereka sedikit terinspirasi, kan? Semua orang berdiri di pundak para raksasa yang datang sebelum Anda - begitulah kemajuan dibuat.
Andrew : Seperti yang saya pahami, bahasa Anda akan aman terkait penggunaan memori. Pernahkah Anda berpikir untuk mengimplementasikan sesuatu seperti pemeriksa pinjaman dari Rust? Anda memandangnya, bagaimana dia menyukai Anda?
Cliff : Yah, saya telah menulis C selama berabad-abad, dengan semua malloc ini dan gratis, dan saya mengelola masa pakai secara manual. Anda tahu, 90-95% dari waktu hidup yang dikelola secara manual memiliki struktur yang sama. Dan sangat, sangat menyakitkan untuk melakukan ini secara manual. Saya ingin kompiler untuk hanya mengatakan apa yang terjadi di sana dan apa yang Anda capai dengan tindakan Anda. Untuk beberapa hal, pemeriksa pinjaman melakukan ini di luar kebiasaan. Dan dia harus secara otomatis menampilkan informasi, memahami segalanya dan bahkan tidak membebani saya untuk menyatakan pemahaman ini. Dia harus melakukan setidaknya analisis pelarian lokal, dan hanya jika dia tidak berhasil, maka Anda perlu menambahkan anotasi jenis yang akan menggambarkan waktu hidup - dan skema seperti itu jauh lebih rumit daripada pemeriksa pinjaman, atau pemeriksa memori yang ada. Pilihan antara "semuanya beres" dan "Saya tidak mengerti apa-apa" - tidak, pasti ada sesuatu yang lebih baik.
Jadi, sebagai orang yang menulis banyak kode C, saya pikir memiliki dukungan untuk kontrol seumur hidup otomatis adalah hal yang paling penting. Dan saya bosan dengan berapa banyak Java menggunakan memori dan keluhan utama adalah di GC. Saat mengalokasikan memori di Java, Anda tidak akan mengembalikan memori yang lokal pada loop GC terakhir. Dalam bahasa dengan manajemen memori yang lebih tepat, ini tidak begitu. Jika Anda memanggil malloc, maka Anda segera mendapatkan memori yang biasanya hanya digunakan. Biasanya Anda melakukan beberapa hal sementara dengan ingatan Anda dan segera membawanya kembali. Dan dia segera kembali ke kolam malloc, dan siklus malloc berikutnya menariknya keluar lagi. Oleh karena itu, penggunaan memori aktual dikurangi menjadi satu set objek hidup pada titik waktu tertentu, ditambah kebocoran. Dan jika semuanya tidak mengalir dengan cara tidak senonoh Anda, sebagian besar memori tersimpan dalam cache dan prosesor, dan itu bekerja dengan cepat. Tetapi membutuhkan banyak manajemen memori manual dengan malloc dan gratis, disebut dalam urutan yang tepat, di tempat yang tepat. Rust sendiri dapat menangani ini dengan benar dan dalam banyak kasus memberikan kinerja yang lebih besar, karena konsumsi memori dipersempit hanya untuk perhitungan saat ini - sebagai lawan dari menunggu siklus GC berikutnya untuk membebaskan memori. Hasilnya, kami mendapat cara yang sangat menarik untuk meningkatkan kinerja. Dan cukup kuat - dalam arti, saya melakukan hal-hal seperti itu ketika memproses data untuk fintech, dan ini memungkinkan saya untuk mendapatkan akselerasi lima kali. Ini adalah akselerasi yang cukup besar, terutama di dunia di mana prosesor tidak semakin cepat, dan kita semua terus menunggu peningkatan.
Andrew : Saya juga ingin bertanya tentang karier secara keseluruhan. Anda menjadi terkenal karena bekerja di JIT di HotSpot dan kemudian pindah ke Azul - dan ini juga merupakan perusahaan JVM. Tetapi mereka sudah lebih terlibat dalam besi daripada perangkat lunak. Dan tiba-tiba beralih ke Big Data dan Pembelajaran Mesin, dan kemudian ke deteksi penipuan. Bagaimana itu bisa terjadi? Ini adalah area pengembangan yang sangat berbeda.
Cliff : Saya telah pemrograman untuk beberapa waktu sekarang dan berhasil check in di kelas yang sangat berbeda. Dan ketika orang-orang berkata, "Oh, kaulah yang membuat JIT untuk Jawa!", Itu selalu lucu. Tapi sebelum itu, saya terlibat dalam klon PostScript - bahasa yang pernah digunakan Apple untuk printer lasernya. Dan sebelum itu dia melakukan implementasi bahasa Forth. Saya pikir tema umum bagi saya adalah pengembangan alat. Sepanjang hidup saya, saya telah membuat alat yang dengannya orang lain menulis program keren mereka. Tetapi saya juga terlibat dalam pengembangan sistem operasi, driver, debugger tingkat kernel, bahasa untuk mengembangkan OS, yang dimulai secara sepele, tetapi seiring berjalannya waktu semuanya menjadi rumit dan rumit. Namun topik utama, bagaimanapun, adalah pengembangan alat. Sebagian besar kehidupan terjadi antara Azul dan Sun, dan itu tentang Jawa. Tetapi ketika saya mulai Big Data dan Pembelajaran Mesin, saya mengenakan topi depan saya lagi dan berkata: "Oh, dan sekarang kami memiliki masalah yang tidak sepele, dan di sini banyak hal menarik dan orang-orang yang melakukan sesuatu" terjadi. Ini adalah jalur pengembangan yang bagus untuk diambil.
Ya, saya sangat suka komputasi terdistribusi. Pekerjaan pertama saya adalah sebagai mahasiswa di C, di proyek periklanan. Ini adalah komputasi terdistribusi pada chip Zilog Z80, yang mengumpulkan data untuk pengenalan teks optik analog yang dihasilkan oleh penganalisa analog nyata. Itu adalah topik yang keren dan benar-benar abnormal. Tetapi ada masalah, beberapa bagian tidak dikenali dengan benar, sehingga perlu untuk mendapatkan gambar dan menunjukkannya kepada orang yang sudah membaca dengan matanya dan memberi tahu apa yang dikatakan di sana, dan karena itu ada jugger data, dan pekerjaan ini memiliki bahasa sendiri . Ada backend yang menangani semua ini - berjalan paralel ke Z80 dengan menjalankan terminal vt100 - satu per orang, dan ada model pemrograman paralel pada Z80. Sepotong memori tertentu yang digunakan bersama oleh semua Z80 di dalam konfigurasi bintang; backplane dibagikan, dan setengah dari RAM dibagi dalam jaringan, dan setengah lainnya bersifat pribadi atau dihabiskan untuk sesuatu yang lain. Sistem paralel terdistribusi yang bermakna rumit dengan memori ... dibagi-pakai. Ketika itu ... Sudah tidak ingat, di suatu tempat di pertengahan 80-an. Sudah lama sekali.
Ya, kami akan menganggap bahwa 30 tahun adalah waktu yang cukup lama.Tugas-tugas yang terkait dengan komputasi terdistribusi telah ada sejak lama, orang telah lama berkelahi dengan kelompok Beowulf . Cluster seperti ini terlihat seperti ... Misalnya: ada Ethernet dan x86 cepat Anda terhubung ke Ethernet ini, dan sekarang Anda ingin mendapatkan memori bersama palsu, karena tidak ada yang bisa berurusan dengan pengkodean komputasi terdistribusi, itu terlalu rumit dan oleh karena itu memori bersama palsu dengan perlindungan halaman memori x86, dan jika Anda menulis ke halaman ini, kami memberi tahu prosesor lain bahwa jika mereka mendapat akses ke memori bersama yang sama, itu perlu diunduh dari Anda, dan dengan demikian sesuatu seperti protokol dukungan koherensi cache muncul dan perangkat lunak untuk ini. Konsep yang menarik. Masalah sebenarnya, tentu saja, berbeda. Semua ini berhasil, tetapi Anda dengan cepat mendapatkan masalah kinerja, karena tidak ada yang memahami model kinerja pada tingkat yang cukup baik - pola akses memori apa yang ada, bagaimana memastikan bahwa node tidak saling ping tanpa henti, dan seterusnya.
Dalam H2O, saya menemukan ini: pengembang sendiri bertanggung jawab untuk menentukan di mana paralelisme disembunyikan dan di mana tidak. Saya datang dengan model pengkodean yang menulis kode kinerja tinggi mudah dan sederhana. Tetapi menulis kode yang berjalan lambat itu sulit, itu akan terlihat buruk. Anda perlu serius mencoba menulis kode lambat, Anda harus menggunakan metode non-standar. Sekilas tentang kode pengereman. Akibatnya, kode biasanya ditulis yang bekerja dengan cepat, tetapi Anda harus mencari tahu apa yang harus dilakukan dalam kasus memori bersama. Semua ini terkait dengan array besar dan perilaku di sana mirip dengan array besar non-volatile di Jawa paralel. Maksud saya, bayangkan dua utas menulis ke array paralel, satu dari mereka menang, dan yang lainnya masing-masing, hilang, dan Anda tidak tahu yang mana dari mereka. Jika mereka tidak stabil, maka pesanannya bisa apa saja - dan itu benar-benar berfungsi dengan baik. Orang-orang sangat peduli dengan urutan operasi, mereka mengatur volatile dengan benar, dan mereka mengharapkan masalah memori di tempat yang tepat. Kalau tidak, mereka hanya akan menulis kode dalam bentuk siklus dari 1 ke N, di mana N adalah beberapa triliun, dengan harapan bahwa semua kasus kompleks akan secara otomatis menjadi paralel - dan ini tidak berfungsi di sana. Tetapi dalam H2O ini bukan Java atau Scala, Anda dapat menganggapnya "Java minus minus" jika Anda mau. Ini adalah gaya pemrograman yang sangat dimengerti dan mirip dengan menulis kode C atau Java sederhana dengan loop dan array. Tetapi pada saat yang sama, memori dapat diproses dengan terabyte. Saya masih menggunakan H2O. β , . Big Data , H2O.
: ?
: ? , β .
. . , , , , . Sun, , , , . , , . , C1, , β . , . , x86- , , 5-10 , 50 .
, , , , C. , , - , C . C, C . , , C, - β¦ , . , . , , . , , 5% . - β , Β« Β», , . : , , . . , β , . , . - β . , , ( , ), , , . , , , .
, , , , , , . , , , - . , , , . , , , , . , : , . , , - : , , - , . β , , β ! β , . Java. Java , , , , β , Β« Β». , , . , Java C . β Java, C , , , . , β , . , . , , . : .
: - . , , - , ?
: ! β , NP- - . , ? . , Ahead of Time β . - . , , β , ! β , . , , . . ? , : , , - ! - , . . , , . : - , - . , , . , , , , - . ! , , , β . . NP- .
: , β . , , , , β¦
: . «». . , . β , , , ( , ). , - . , , , . , , . , . , , . , , . , , - , β . β . , GC, , , , β , . , . , , . , β , ? , .
: , ? ?
: GPU , !
: . ?
: , - Azul. , . . H2O , . , GPU. ? , Azul, : β .
: ?
: , β¦ . , . , , , , . , , . , Java C1 C2 β . , Java β . , , β . β¦ . - , Sun, β¦ , , . , . , . β¦ β¦ , . , , . . - , : . , , , , , , . , . . , . Β« , , Β». : Β«!Β». , , , : , .
β , , , . . , , , , . , Java JIT, C2. , β . , β ! . , , , , , , . . . , . , , , , : , , . , β . , , - . : Β« ?Β». , . , , : , , β ? , . , , , , , , - .
: , -. ?
: , , . β . . , . . . : , , - β . . , , β , . , , , , - , . , . , , - . , , β , .
, . , β , , . , . , β . , . , , Β« Β», , β , , , , . , , Β« Β».
. . - , , «»: , β . β . , , . Β«, -, , Β». , : , . , , . . β , . , ? , ? ? , ? . , . β . . , . β β , . , Β« Β» . : Β«--Β», : Β«, !Β» . . , , , , . , . , . , β , . β , . , , , .
, β , . , , . , . , , , , . , , . , , , , . . , , , . , , , , . , , , . , β , , , . , .
: β¦ . , . . Hydra!
Hydra 2019, 11-12 2019 -. Β«The Azul Hardware Transactional Memory experienceΒ» . .