Dalam dunia pengaturcaraan, pengoptimuman penyusun sepatutnya menjadikan kod lebih pantas, bukan lebih perlahan. Namun, satu kajian mendalam baru-baru ini mengenai prestasi Rust mendedahkan satu kes mengejutkan di mana tahap pengoptimuman tertinggi sebenarnya melumpuhkan prestasi, mencetuskan perbincangan meluas dalam kalangan pembangun tentang bilakah penambahbaikan penyusun boleh menjadi perangkap prestasi.
Kes Prestasi Patologi
Seorang pembangun Rust baru-baru ini mendapati bahawa barisan keutamaan terbatas tersuai mereka berjalan dengan lebih perlahan apabila disusun dengan opt-level = 3 berbanding dengan opt-level = 2. Penalti prestasi adalah ketara - perlahan 113% dalam penanda aras. Keputusan yang bercanggah dengan intuisi ini berlaku walaupun kedua-dua tahap pengoptimuman mensasarkan seni bina Haswell yang sama dan diuji pada kedua-dua pemproses AMD Zen 3 dan Intel Haswell sebenar.
Kod bermasalah tersebut melibatkan vektor tersusun menggunakan binary_search_by dengan fungsi perbandingan yang terlebih dahulu membandingkan jarak titik-apung, kemudian ID integer. Walaupun ini kelihatan seperti kod yang mudah, strategi pengoptimuman penyusun yang berbeza menghasilkan output pemasangan yang sangat berbeza yang membawa kepada percanggahan prestasi.
Dalam fungsi bercabang, id hanya dibandingkan jika jarak adalah sama, dan memandangkan jarak adalah float rawak, ini hampir tidak pernah berlaku dan cabang sepadan diramalkan hampir sempurna. Fungsi tanpa cabang sentiasa membandingkan kedua-dua id dan jarak, secara efektif melakukan kerja dua kali ganda.
Perbandingan Prestasi: Tahap Pengoptimuman O2 vs O3
- Pengoptimuman O2: 44.1% sampel dalam binary_search_by, 25.68% dalam fungsi compare
- Pengoptimuman O3: 79.6% sampel dalam binary_search_by, 63.57% dalam fungsi compare
- Penalti prestasi: +113% kelembapan dengan O3 berbanding O2
Misteri di Peringkat Pemasangan
Apabila pembangun menyelami kod pemasangan, mereka mendapati opt-level = 2 menghasilkan kod yang mudah dengan lompatan bersyarat, manakala opt-level = 3 menjana kod yang lebih canggih menggunakan gerakan bersyarat. Gerakan bersyarat secara umumnya dianggap lebih moden dan cekap kerana ia mengelakkan ralat ramalan cabang, tetapi dalam kes khusus ini, ia mencipta rantaian kebergantungan yang menjadi kebuntuan prestasi.
Analisis teori menggunakan alat seperti uCA (uiCA) meramalkan versi gerakan bersyarat akan mempunyai daya pemprosesan 2.7x lebih rendah disebabkan oleh isu kebergantungan. Ini menyerlahkan bagaimana kerumitan CPU moden - dengan ciri seperti paralelisme di peringkat arahan, ramalan cabang, dan pelaksanaan spekulatif - kadangkala boleh bertindak menentang kod yang dioptimumkan dengan cara yang tidak dijangka.
Perbezaan Kod Assembly
- O2: Menggunakan lompatan bersyarat (5 lompatan bersyarat termasuk pemeriksaan NaN)
- O3: Menggunakan pergerakan bersyarat (4 pergerakan bersyarat, 1 lompatan bersyarat)
- Throughput teori: Assembly O3 diramalkan 2.7x lebih rendah oleh alat analisis uCA
Eksperimen dan Penyelesaian Komuniti
Komuniti pengaturcaraan bertindak balas dengan ujian dan analisis yang meluas. Sesetengah pembangun mendapati bahawa menambah #[inline(selalu)] pada fungsi perbandingan boleh mengurangkan penalti O3 sebanyak kira-kira 50%, walaupun ia sedikit menurunkan prestasi O2. Yang lain menemui bahawa menggunakan total_cmp untuk perbandingan titik-apung berbanding pengendalian NaN manual menghasilkan pemasangan yang berbeza tetapi masalah prestasi yang serupa.
Beberapa pengulas menyatakan bahawa pengoptimuman berpandukan profil (PGO) mungkin membantu LLVM membuat keputusan yang lebih baik tentang bila untuk menggunakan gerakan bersyarat berbanding lompatan. Perbincangan itu juga menyentuh konteks sejarah, dengan rujukan kepada kemerosotan prestasi Rust terdahulu yang berkaitan dengan pengoptimuman carian binari dan penggunaan gerakan bersyarat.
Penyelesaian Berpotensi yang Dibincangkan
- Tambah
[inline(always)]pada fungsi perbandingan: peningkatan ~50% O3, penurunan +10% O2 - Gunakan
std::hint::unlikelypada cabang yang jarang berlaku - Gantikan perbandingan float manual dengan kaedah
total_cmp - Pengoptimuman berpandukan profil (PGO) untuk keputusan kompiler yang lebih baik
Implikasi yang Lebih Luas
Kajian kes ini mendedahkan kebenaran yang lebih mendalam tentang strategi pengoptimuman penyusun. Seperti yang dinyatakan oleh seorang pembangun, LLVM tidak menganggap kesamaan float adalah kurang berkemungkinan berbanding syarat lain, yang boleh membawa kepada pilihan pengoptimuman yang tidak optimum untuk corak data tertentu. Kejadian ini berfungsi sebagai peringatan bahawa pengoptimuman penyusun bukanlah sihir - ia adalah algoritma yang membuat tekaan berpendidikan yang kadangkala boleh salah untuk corak kod tertentu.
Perbincangan itu juga menyerlahkan bagaimana pilihan bahasa pengaturcaraan bersilang dengan ciri-ciri perkakasan. CPU moden sangat kompleks, dan ciri prestasi mereka boleh mencabar intuisi mudah tentang apa yang membentuk kod yang dioptimumkan. Apa yang berfungsi dengan baik pada satu seni bina atau dengan satu corak data mungkin gagal secara dramatik dalam keadaan yang berbeza.
Mengemudi Perangkap Pengoptimuman
Untuk pembangun yang menghadapi isu serupa, komuniti mencadangkan beberapa pendekatan. Menggunakan std::hint::tidak_mungkin pada cabang yang jarang diambil boleh mempengaruhi keputusan pengoptimuman. Sesetengah menyebut bahawa __builtin_expect_with_probability GCC/Clang dengan kebarangkalian 0.5 boleh memaksa penggunaan gerakan bersyarat apabila sesuai.
Intipati utama ialah pengoptimuman prestasi memerlukan ujian empirikal dan bukan andaian. Seperti yang dinyatakan secara ringkas oleh seorang pengulas: Dan inilah sebabnya anda pergi dan melihat pemasangan dalam godbolt untuk melihat apa yang sedang berlaku. Kes ini menunjukkan bahawa walaupun pembangun yang berpengalaman boleh terkejut dengan tingkah laku penyusun, menekankan kepentingan penanda aras dan analisis di peringkat pemasangan untuk kod yang kritikal kepada prestasi.
Pasukan penyusun Rust secara sejarah telah mengimbangi pilihan pengoptimuman ini dengan berhati-hati, dengan kemerosotan prestasi terdahulu menunjukkan bahawa gerakan bersyarat boleh menjadi lebih pantas dalam beberapa penanda aras sementara lebih perlahan dalam yang lain. Kebolehubahan ini menekankan mengapa tiada strategi pengoptimuman yang sesuai untuk semua keadaan dan mengapa pembangun penyusun menyediakan pelbagai tahap pengoptimuman dan bukan satu tetapan terpantas tunggal.
Dalam landskap teknologi penyusun dan seni bina perkakasan yang sentiasa berkembang, kes seperti ini berfungsi sebagai peringatan berharga bahawa memahami interaksi antara kod, penyusun, dan CPU kekal penting untuk menulis perisian yang benar-benar berprestasi tinggi.
Rujukan: When O3 is 2x slower than O2
