- Dengan membangun ulang source
jqyang sama dan mengganti allocator, waktu pemrosesan GeoJSON 500MB turun dari 4,606 detik menjadi 2,428 detik, sehingga 1,90x lebih cepat daripada biner Ubuntu - Benchmark dijalankan pada Ryzen 9 9950X menggunakan parcel map Alameda County Assessor, dengan mengekstrak
SitusCityuntuk entri yang memenuhi kondisiTotalNetValue < 193000 - Hanya dengan build ulang sederhana saja sudah ada peningkatan 2~4%, dan kombinasi
clang-18,-O3,-flto,-DNDEBUGmenghasilkan performa 1,20x dibanding paket Ubuntu - Profil menunjukkan biaya alokasi memori sangat besar, sehingga
TCMalloc,jemalloc, danmimallocdibandingkan; dalam eksperimenLD_PRELOAD,mimallocmenjadi yang tercepat - Build akhir yang ditautkan dengan
mimallocjuga mencatat 0,755 detik vs 1,424 detik pada kasus pemrosesan JSON 2,2GB terpisah, menunjukkan bahwa build bawaan distro bisa sangat berbeda tergantung beban kerja
Beban kerja acuan dan metode pengukuran
- Objek pengujian adalah alat pemrosesan JSON
jq, dan data masukannya adalah file GeoJSON 500MB yang berisi parcel map dari Alameda County Assessor - Query yang dijalankan mencetak
SitusCitydari daftar parcel yang memenuhi kondisiTotalNetValue < 193000.features[] | select(.properties.TotalNetValue < 193000) | .properties.SitusCity
/usr/bin/jqbawaan Ubuntu memerlukan sekitar 5 detik saat file sudah berada dalam cache, dan benchmark rinci diukur berulang denganhyperfine- Untuk mengurangi variasi saat eksekusi, proses dipatok ke logical CPU 2 dengan
taskset -c 2- Pengaturan ini dipakai untuk menghindari dampak interrupt sistem yang berjalan di CPU 0 dan migrasi CPU
Build ulang sederhana dari source yang sama
- Source code
jqyang digunakan Ubuntu diunduh lalu dikonfigurasi dan dibangun tanpa flag tambahan - Hanya dengan build ulang sederhana ini saja, hasilnya sekitar 2~4% lebih cepat daripada paket biner Ubuntu
- Biner hasil build ulang: rata-rata 4,517 detik
- Ubuntu
/usr/bin/jq: rata-rata 4,641 detik - Secara keseluruhan sekitar 1,03x performanya
Menerapkan clang dan flag optimasi
- Pada tahap berikutnya digunakan
clang-18, level optimasi yang lebih tinggi, LTO, serta flag terkait debugging dan profiling - Flag utama yang memengaruhi performa adalah
-O3,-flto, dan-DNDEBUG-O3memakai level optimasi yang lebih tinggi dibanding-O2-fltomengaktifkan link-time optimization-DNDEBUGmengurangi biaya assertion yang tampak besar pada profil
- Contoh
configureyang digunakan adalah sebagai berikutCC=clang-18LDFLAGS="-flto -g -Wl,--emit-relocs -Wl,-z,now -Wl,--gc-sections -fuse-ld=lld"CFLAGS="-flto -DNDEBUG -fno-omit-frame-pointer -gmlt -march=native -O3 -mno-omit-leaf-frame-pointer -ffunction-sections -fdata-sections"
- Build ini 1,20x lebih cepat daripada biner Ubuntu
- Build ulang teroptimasi: rata-rata 3,853 detik
- Ubuntu
/usr/bin/jq: rata-rata 4,631 detik
Eksperimen mengganti allocator
jqadalah program C yang kompleks, dan profil menunjukkan bahwa alokasi memori menjadi biaya terbesar- Pertama, dilakukan build ulang dengan menautkan
TCMallocyang disediakan sebagai paket UbuntuLDFLAGSditambah-L/usr/lib/x86_64-linux-gnu -ltcmalloc_minimal- Biner hasil build ulang: rata-rata 3,253 detik
- Ubuntu
/usr/bin/jq: rata-rata 4,611 detik - Hasilnya 1,42x lebih cepat dibanding biner Ubuntu
- Bahkan jika hanya allocator pada biner Ubuntu bawaan yang diganti melalui
LD_PRELOAD, tetap ada sedikit peningkatan- Default: rata-rata 4,601 detik
- Preload TCMalloc: rata-rata 4,082 detik
- 1,13x lebih cepat dibanding default
Dynamic preload dan pengaturan THP
jemalloc,mimalloc, danTCMallocyang disediakan Ubuntu dibandingkan melaluiLD_PRELOAD- Perbandingan ini diperoleh setelah menetapkan variabel lingkungan berikut
MIMALLOC_LARGE_OS_PAGES=1MALLOC_CONF="thp:always,metadata_thp:always"GLIBC_TUNABLES=glibc.malloc.hugetlb=1
- Hasil akhirnya, mimalloc adalah yang tercepat
- glibc default: rata-rata 4,123 detik
- Preload TCMalloc: rata-rata 4,130 detik
- Preload jemalloc: rata-rata 3,510 detik
- Preload mimalloc: rata-rata 3,154 detik
- Mengaktifkan THP menguntungkan glibc allocator, jemalloc, dan mimalloc
THP + mimalloc31% lebih cepat daripadaTHP + glibc, dan 48% lebih cepat daripada nilai default glibc
Hasil akhir build yang ditautkan dengan mimalloc
- Dynamic preload dinilai tidak ideal dari sisi performa, sehingga pada tahap akhir
jqdibangun ulang dengan menautkanmimalloc - Build akhir ini 1,90x lebih cepat daripada paket biner Ubuntu
- Build ulang
mimalloc: rata-rata 2,428 detik - Ubuntu
/usr/bin/jq: rata-rata 4,606 detik - Setiap benchmark dijalankan 10 kali
- Build ulang
- Build yang sama juga dipakai pada aplikasi lain
- Memproses JSON 2,2GB yang tersebar di 13.000 file
rushdigunakan untuk paralelisasijqhasil build ulangmimalloc: 0,755 detik- Paket Ubuntu
jq: 1,424 detik
- Bahkan pada kasus terpisah ini, peningkatan kecepatannya juga hampir 2x
1 komentar
Opini di Hacker News
Clickbait seperti “membangun ulang satu paket Ubuntu dan mengganti memory allocator agar 90% lebih cepat” itu sampai membuat ingin meninju seseorang lewat TCP/IP. Itu cuma satu paket, dan sebagian peningkatannya bahkan bukan berkat kompilasi ulang
Meski begitu, saya pernah menyisipkan jemalloc ke satu program dengan
LD_PRELOADuntuk mengganti implementasimalloc, dan hasilnya cukup bagus. Saya tidak mengukur performanya, tetapi penggunaan memori aplikasi itu menjadi stabil dan masalah yang tampak seperti kebocoran memori juga teratasi. Kemungkinan besar itu sebenarnya bukan masalah aplikasinya sendiri, melainkan fragmentasi memori padamallocstandarfree()dipanggil, secara eksternal memori tidak benar-benar dilepas kecuali dalam kondisi khususSemakin banyak thread dan core CPU, semakin parah masalah ini. Salah satu solusi mudah adalah menetapkan variabel lingkungan “ajaib”
MALLOC_ARENA_MAX=2untuk membatasi jumlah cache. Cara lain adalah aplikasi memanggilmalloc_trim()secara berkala untuk mengosongkan cache, tetapi ini memerlukan perubahan sourcehttps://www.joyfulbikeshedding.com/blog/2019-03-14-what-caus...
Sebaliknya, saya bersyukur mereka tidak mengompilasi dengan
-O3yang berpotensi mengandung bug. Untuk beberapa bagian yang performanya penting mungkin bagus, tetapi saya tidak ingin seluruh sistem dikompilasi dengan-O3Dulu sekali saya mulai membangun Mozilla dan kernel Linux saya sendiri sesuai selera, dan biasanya mendapat peningkatan performa yang lumayan. Seluruh tujuan distro Gentoo Linux, misalnya, adalah peningkatan performa yang bisa diperoleh dengan mengompilasi semuanya dari source secara optimal
jq,grep,ffmpeg,ocrmypdf, dan utilitas Unix umum seperti ini sering kali dibuat sebagai target build serbaguna, bukan untuk aplikasi tertentuRekayasa adalah kompromi. Sebagian besar keuntungan dalam tulisan itu berasal dari spesialisasi allocator memori. Perlu diingat bahwa sebagian proyek bersifat multi-threaded; satu thread mengalokasikan, thread lain menulis data, dan thread ketiga mungkin membebaskannya
Allocator harus menangani hal ini, sehingga peningkatan kecepatan pada satu proyek bisa menjadi sumber crash di proyek lain. Strategi realokasi juga jadi masalah. Ada program yang mengalokasikan di awal lalu tidak pernah menyentuh
malloclagi, sementara program lain terus membebaskan lalu mendapatkannya kembali. Seberapa baik fragmentasi ditangani, dan apakah waktu aktifnya 10 detik atau 10 tahun, juga penting. Kadang pilihan allocator adalah perbedaan antara stabilitas jangka panjang dan kecepatan jangka pendekSaat membuat editor video yang menguji video 4K dan melakukan cache frame, saya pernah bereksperimen dengan beberapa allocator. 32MB per frame pada 60fps berarti hampir 2GB per detik untuk satu track. Batas allocator langsung terasa, dan saya menyadari bahwa setidaknya allocator bawaan glibc adalah yang terbaik untuk stabilitas jangka panjang. Namun dalam benchmark singkat, ia yang paling lambat
Tentu besarnya peningkatan kecepatan bisa berbeda-beda, tetapi benchmark umumnya menunjukkan peningkatan menyeluruh. Apakah itu berarti bagi aplikasi tertentu adalah persoalan yang sama sekali berbeda. Soal crash pun, semuanya adalah allocator multi-threaded serbaguna sehingga tidak berperilaku berbeda dari glibc, dan bug juga bisa saja ada di glibc
DEFAULT_MMAP_THRESHOLD_MAX, dan karena di platform 64-bit nilainya 32MiB, glibc tidak bisa dibujuk untuk melakukan cache seperti yang didokumentasikan di manualmalloptSetiap kali, ia meminta memori langsung ke kernel dengan
mmapdan mengembalikannya denganmunmap. System call ini agak lambat, dan dalam kasus saya biaya page fault pada setiap halaman memori saat akses pertama cukup lambat hingga target performa tidak tercapai. Solusinya benar-benar sederhana. Untuk frame video saja, gunakan free list sendiri di atas allocator serbaguna ataummap. Ini bekerja dengan baik karena alokasi dengan ukuran yang persis sama berulang dengan sangat stabil[1] Dalam format UYVY ukurannya sedikit di bawah 64MiB, dan dalam format I420 sedikit di bawah 48MiB
Namun setelah membaca komentar tingkat atas ini, saya jadi khawatir apakah saya benar-benar salah memahami tulisannya. Dari nadanya, terdengar seolah-olah hal yang disarankan gist sama sekali tidak boleh dilakukan, dan itu adalah usulan buruk yang mengabaikan semua kompleksitas ini. Bisa bantu saya memahami apakah gist aslinya tulisan yang bagus, apakah ada poin yang valid, atau sama sekali tidak bernilai? Sampai melihat komentar ini saya menganggapnya bernilai, tetapi saya sadar saya tidak cukup pintar untuk membedakannya
Kalau memakai algoritma kompresi yang sesuai untuk data, orang akan menjelaskan kenapa Anda bodoh dan seharusnya memakai algoritma lain. Baru-baru ini saya harus mengompresi string JSON panjang tertentu untuk dimasukkan ke Dynamo, dan setelah menguji semua algoritma populer secara menyeluruh, Brotli jauh lebih unggul. Tetap saja itu tidak menghentikan setiap orang yang lewat untuk berkata bahwa zlib lebih baik. Kadang cukup melelahkan
Gentoo Linux pada dasarnya adalah distro yang dibuat untuk orang-orang seperti ini, agar mereka bisa mengoptimalkan mesin Linux sendiri sesuai kebutuhan mereka
Setelah konfigurasi awal, distro ini cukup sederhana dan mudah dipakai. Saya ingat mendapat banyak teman di kanal Gentoo Linux di Matrix; masa-masa yang menyenangkan
https://www.gentoo.org/
Fakta menarik: ChromeOS awal pada dasarnya adalah instalasi Gentoo Linux kustom. Saya tidak tahu apakah sekarang masih memakai Gentoo Linux secara internal
Saya sudah memakai Gentoo selama 20 tahun, tetapi tidak pernah memakainya karena alasan performa. Gentoo bagus ketika Anda tahu perilaku seperti apa yang Anda inginkan, dan ia membantu Anda mencapainya
Tujuan Gentoo adalah memiliki sistem operasi yang membangun semua program dari source, bukan memakai paket biner yang sudah dibangun sebelumnya. Ini memungkinkan peningkatan kecepatan tingkat lanjut dan kustomisasi, tetapi juga berarti komponen paling mendasar seperti kernel pun harus dikompilasi dari source. Di komunitas Linux, Gentoo dikenal sebagai sistem operasi yang sangat rumit karena proses instalasinya yang merepotkan. Instalasi Gentoo dasar langsung boot ke command prompt, dan pengguna harus mempartisi disk sendiri, mengunduh serta mengekstrak paket yang disebut “Stage 3 tarball”, lalu membangun sistem dengan memasang paket secara manual. Pengguna baru atau yang kurang berpengalaman sering tidak tahu harus berbuat apa ketika masuk ke installer tetapi tidak ada tampilan grafis. Anggota /g/ sering melebih-lebihkan nilai Gentoo untuk menipu pengguna baru agar mencoba menginstalnya
Mungkin ada satu-dua hambatan, tetapi jika pengalaman Linux Anda secara umum memadai, besar kemungkinan Anda bisa masuk ke resep build, memperbaikinya, membuatnya bekerja sesuai kebutuhan, dan menyumbangkan perbaikan ke upstream. Ini adalah hasil dari fokus yang keras kepala pada minimalisme dan menghindari segala bentuk over-engineering. Itu juga bagian yang selalu saya rindukan selama memakai Gentoo. Di Gentoo, saya selalu berakhir mengutak-atik USE flags dan package mask dengan cara yang tidak terlalu berguna bagi pengguna lain. Sistem build-nya terlalu kompleks, sehingga selama bertahun-tahun terlalu sulit untuk benar-benar menguasainya, memperbaiki masalah sampai tingkat akar penyebab, dan menyumbangkannya ke upstream. Void juga bisa menjadi basis ideal ketika Anda tidak ingin membangun seluruh sistem dari source, tetapi ingin mencampur biner yang disediakan distro dengan paket yang Anda bangun sendiri dari source
Setelah itu saya pindah ke ArchLinux, dan secara umum cocok untuk saya. Jika Anda memakai prosesor yang cukup standar, saya rasa Gentoo tidak memberi keuntungan sebesar itu
Dengan cara ini, Anda tidak hanya akan melewatkan pembaruan keamanan untuk
jq, tetapi juga untuk onigurama, dependensi parsing regex-nya. Dulu pernah ada pembaruan keamanan untuk onigurama, dan jika hal semacam ini terjadi lagi, Anda bisa menjadi rentan.jqsering dipakai untuk mem-parse JSON yang tidak tepercayaIsinya adalah “Pembaruan keamanan: memperbaiki beberapa dereference pointer yang salah, kerusakan memori akibat penulisan di luar batas, dan stack buffer overflow”, dan kasus itu terkait CVE-2017-9224, CVE-2017-9226, CVE-2017-9227, CVE-2017-9228, serta CVE-2017-9229
libonig5, dan akan diperbarui secara normalSaya tidak bermaksud meremehkan nilai sistem CVE, tetapi sulit dibantah bahwa dampak nyata antar-temuan bisa sangat berbeda
Sudah agak lama sejak saya menangani hal semacam ini, tetapi seingat saya, begitu Anda melampaui flag yang dipakai pengembang upstream, Anda akan mendapat bug aneh dan ketidakpedulian besar ketika bug muncul. Yang saya maksud di sini adalah pengembang upstream, bukan packager distro
Saya belum pernah memakai
mallocnon-libc, tetapi sepertinya prinsip yang sama berlakuNamun jika semua orang melakukan itu, jadilah monokultur, dan monokultur itu rapuh serta buruk. Kode baru menjadi sedikit lebih tangguh karena dibangun dalam konteks berbeda: platform, compiler, opsi, library, dan sebagainya yang berbeda. Bug yang kebetulan tidak tersentuh oleh kebanyakan orang karena platform atau flag build lewat tepat di sebelah jebakannya tetaplah bug, dan menemukan serta memperbaikinya lebih baik bagi kode. Sebagai individu, kita semua diuntungkan ketika kode secara keseluruhan menjadi lebih tangguh, bukan rapuh
Tentu saja, saya juga mengira
-march=nativeadalah peningkatan utama yang saya lihat, tetapi tulisan ini menunjukkan bahwa tidak selalu demikian. Aplikasi yang memakai floating point mungkin juga punya lebih banyak sisi kasarIni lebih seperti membangun ulang dengan allocator lain yang menghasilkan benchmark bagus pada alur kerja tertentu
malloc. Fakta bahwa distro masih terus memakai glibcmallocalih-alih mimalloc atau jemalloc pada dasarnya kelalaian tugasmalloc?Penasaran bagaimana performanya dibandingkan dengan klon
jqberbasis Rust inicargo install --locked jaqUntuk mengaktifkan optimisasi yang disesuaikan dengan keluarga CPU tertentu, bisa juga menambahkan
RUSTFLAGS="-C target-cpu=native".cargo installadalah fitur Rust yang kurang diapresiasi dan sangat pas untuk jenis penggunaan yang dijelaskan dalam tulisan itu. Karena tool dibangun dari source, kita bisa memilih fitur atau instruksi spesifik platform yang biasanya tidak disertakan dalam binary demi kompatibilitas dengan CPU lama. Tidak perlu meng-clone repository atau mencari tahu cara build-nya; semuanya sudah ikut tersedia begitu sajajaq[1] danyq[2] adalah pilihan saya setiap kali memakaijqlalu butuh peningkatan performa yang cepat dan mudah[1] https://github.com/01mf02/jaq
[2] https://github.com/mikefarah/yq
jqdangojq, memakai solusijqsaya untuk AoC 2022 day 13 sebagai teshttps://gist.github.com/oguz-ismail/8d0957dfeecc4f816ffee79d...
Sampai sekarang masih tertinggal dari keduanya
cargo installjuga punya flag--gituntuk menentukan URL repository. Ini bisa dipakai ketika tidak ada paket publik, atau ketika ingin commit terbaru yang belum dirilisSaya sudah beberapa kali memakainya dulu, dan ini sangat berguna terutama sebagai cara mudah untuk cepat menginstal tool yang saya buat tergesa-gesa secara pribadi setelah di-push ke repository, tanpa perlu membuat proses rilis atau menyalin binary secara manual ke mesin-mesin pribadi dan melacak commit persis yang dipakai untuk build
Kalau benar-benar ingin melakukan ini, cukup minta Ubuntu mengunduh paket source yang tepat. Dalam kasus ini, gunakan
apt-get source jqSetelah itu masuk ke paket tersebut dan kompilasi ulang sesuka hati. Bisa juga dipaketkan ulang untuk distribusi atau penyimpanan. Dengan cara ini hasilnya akan jauh lebih dekat dengan Ubuntu upstream, alih-alih mendapat banyak error aneh dan ketidaksesuaian
Judulnya menyesatkan. Maksudnya adalah 90% dari waktu yang menjadi lebih cepat, dan sebenarnya sekitar 45% lebih cepat
Ini agak menarik kalau kita peduli pada cara kita memakai bahasa. Bisa juga dikatakan melakukan 90% lebih banyak pekerjaan dalam waktu yang sama, dan itu cocok dengan satuan kecepatan lain yang umum kita pakai, seperti mil per jam, kata per menit, atau bit per detik. Namun dalam performa komputer, konvensinya adalah mengukur waktu yang diperlukan untuk jumlah pekerjaan tetap. Sepertinya karena biasanya beban kerja tetap, dan yang berubah adalah waktu tunggunya. Dalam tulisan blog ini pun persis seperti itu, sehingga waktu ditempatkan sebagai pembilang. Tulisannya sendiri sangat menarik dan ditulis dengan baik, tetapi bukan 90% lebih cepat
Selain itu, kalau memakai satuan waktu, saya tidak akan memakai kata “faster”. “45% less time” dan “45% faster” adalah klaim yang sangat berbeda, dan keduanya bermakna baik di dalam maupun di luar pemrograman
Kalau kita mengatakan “mengurangi sesuatu sebesar N%”, biasanya diasumsikan N% itu adalah N% dari hal yang dikurangi itu sendiri, bukan dari nilai lain
Setelah membaca bahwa perubahan sesederhana ini bisa memberi peningkatan kecepatan besar, hal pertama yang terpikir adalah memberi tahu para penulis jq. Mungkin ada jebakan yang perlu diwaspadai, atau setelah diuji mereka bisa membuatnya lebih cepat untuk semua orang
Apa pun hasilnya, sekadar memberi tahu tampaknya berguna. Namun tulisan itu sepertinya bahkan tidak mempertimbangkan opsi tersebut, dan saya juga tidak melihatnya di komentar sini. Apakah saya melewatkan sesuatu?
Saya juga tidak tahu apakah allocator glibc menjadi standar di sana
https://en.m.wikipedia.org/wiki/Clear_Linux_OS