Bagaimana JIT menguraikan kode C # kami (heuristik)

Inlining adalah salah satu optimisasi terpenting dalam kompiler. Itu tidak hanya menghilangkan overhead dari panggilan, tetapi juga membuka banyak kemungkinan untuk optimasi lainnya, misalnya, lipat konstan, penghapusan kode mati, dll. Selain itu, terkadang inlining menyebabkan penurunan ukuran fungsi panggilan! Saya bertanya kepada beberapa orang apakah mereka tahu dengan aturan apa fungsi-fungsi dalam C # diuraikan, dan sebagian besar menjawab bahwa JIT melihat ukuran kode IL dan hanya sebaris fungsi kecil dalam ukuran, katakanlah, hingga 32 byte. Oleh karena itu, saya memutuskan untuk menulis posting ini untuk mengungkapkan detail implementasi dengan bantuan contoh seperti itu, yang akan menunjukkan beberapa heuristik dalam tindakan sekaligus:




Apakah Anda pikir panggilan ke konstruktor Volume akan diuraikan di sini? Tentu saja tidak. Itu terlalu besar, terutama karena operator kelas berat melemparkan baru , yang menyebabkan codegen yang agak berani. Mari kita periksa di Disasmo:



Inline! Selain itu, semua pengecualian dan cabangnya telah berhasil dihapus! Anda dapat mengatakan sesuatu dengan gaya "Ah, oke, jit sangat pintar dan melakukan analisis penuh dari semua kandidat untuk inline, lihat apa yang akan terjadi jika Anda melewati argumen tertentu" atau "Jit mencoba untuk menyatukan segala sesuatu yang mungkin, melakukan semua optimasi, dan kemudian memutuskan secara menguntungkan it or not โ€(ambil kombinatorik dan hitung kompleksitas operasi ini, misalnya, untuk grafik panggilan dari sepuluh atau dua metode).

Ya ... tidak, ini tidak realistis, terutama dalam hal waktu yang tepat. Oleh karena itu, sebagian besar penyusun menggunakan apa yang disebut pengamatan dan heuristik untuk menyelesaikan masalah ransel klasik ini dan mencoba menentukan anggaran mereka sendiri dan menyesuaikannya seefisien mungkin (dan tidak, PGO bukan obat mujarab). RyuJIT memiliki pengamatan positif dan negatif. Positif meningkatkan koefisien manfaat (multiplier manfaat). Semakin tinggi koefisien, semakin banyak kode yang bisa kita sebaris. Pengamatan negatif sebaliknya - turunkan atau bahkan melarang inlining. Mari kita lihat pengamatan apa yang RyuJIT buat sebagai contoh:



Pengamatan ini dapat dilihat di log dari COMPlus_JitDump (misalnya, di Disasmo):



Semua pengamatan sederhana ini meningkatkan koefisien dari 1.0ke 11.5 dan membantu mengatasi anggaran inlineer dengan sukses, misalnya, fakta bahwa kita melewati argumen konstan dan dibandingkan dengan konstanta lain memberi tahu kita bahwa dengan probabilitas tingkat tinggi setelah menciutkan konstanta, salah satu cabang kondisi akan dihapus dan kode akan menjadi lebih kecil. Atau, misalnya, fakta bahwa ini adalah konstruktor dan disebut di dalam loop juga merupakan petunjuk bagi jit bahwa ia harus melunakkan persyaratan untuk inlining.

Selain manfaat pengganda, RyuJIT juga menggunakan pengamatan untuk memprediksi ukuran kode fungsi asli dan dampak kinerjanya menggunakan konstanta ajaib di EstimateCodeSize () dan EstimatePerformanceImpact () yang diperoleh menggunakan ML.

Omong-omong, apakah Anda memperhatikan trik ini?:

if ((value - 'A') > ('Z' - 'A'))

Ini adalah versi yang dioptimalkan untuk:

if (value < 'A' || value > 'Z')

Kedua ekspresi adalah satu dan sama, tetapi dalam kasus pertama kita memiliki satu unit dasar, dan yang kedua ada tiga. Ternyata inliner memiliki batasan ketat pada jumlah blok dasar dalam fungsi, dan jika melebihi 5 maka tidak masalah seberapa besar pengganda manfaat kita - inlining dibatalkan. Jadi saya menerapkan trik ini agar sesuai dengan persyaratan ketat ini. Akan lebih bagus jika Roslyn melakukannya untuk saya.

Masalah di Roslyn : github.com/dotnet/runtime/issues/13347
PR di RyuJIT (upaya canggung saya): github.com/dotnet/coreclr/pull/27480

Di sana saya menggambarkan contoh mengapa masuk akal tidak hanya di Jit tetapi dan di kompiler C #.

Metode inlining dan virtual


Semuanya jelas di sini, Anda tidak dapat menyejajarkan apa yang tidak ada informasi tentang pada tahap kompilasi, meskipun jika jenis atau metode disegel maka mengapa tidak .

Menyertakan dan melempar pengecualian


Jika metode tidak pernah mengembalikan nilai (misalnya, itu hanya throw new...), maka metode tersebut secara otomatis ditandai sebagai pelempar-bantuan dan tidak akan sebaris. Ini adalah cara untuk menyapu codegen kompleks dari throw newbawah karpet dan menenangkan inliner.

Atribut Inlining dan [AggressiveInlining]


Dalam hal ini, Anda menganjurkan agar inlineer inline metode, tetapi Anda harus sangat berhati-hati karena dua alasan:

  • Mungkin Anda mengoptimalkan satu case dan memperburuk yang lainnya (misalnya, meningkatkan case dari argumen konstan) dengan ukuran codegen.
  • Inlining sering menghasilkan sejumlah besar variabel sementara yang dapat melebihi batas tertentu - jumlah variabel yang siklus hidupnya dapat dilacak RyuJIT (512) dan setelah itu kode akan mulai tumbuh menjadi tumpahan yang mengerikan di tumpukan dan melambat sangat. Dua contoh yang baik: tyts dan tyts .

Metode inlining dan dinamis


Saat ini, metode seperti itu tidak sebaris dan tidak berbaris sendiri: github.com/dotnet/runtime/issues/34500

Usaha saya untuk menulis heuristik saya


Baru-baru ini, saya mencoba menulis heuristik Anda sendiri untuk bantuan di sini kesempatan seperti itu:



Dalam sebuah postingan terakhir saya menyebutkan bahwa saya baru-baru ini dioptimalkan untuk perhitungan RyuJIT dari panjang string konstan ( "Hello".Length -> 5kita melihat bahwa jika zainlaynit), dan sebagainya, dalam contoh di atas ^ Validatedi Test, kita mendapatkan if ("hello".Length > 10)apa yang dioptimalkan dalam if (5 > 10)apa yang dioptimalkan dalam penghapusan seluruh kondisi / cabang. Namun, inliner menolak untuk sebaris Validate:



Dan masalah utama di sini adalah bahwa belum ada heuristik yang memberitahu jit bahwa kita meneruskan string konstan System.String::get_Length, yang berarti bahwa panggilan callvirt kemungkinan besar akan runtuh menjadi konstanta dan seluruh cabang akan dihapus. Sebenarnya, heuristik sayadan menambahkan pengamatan ini (satu-satunya minusnya adalah Anda harus menyelesaikan semua callvirts, yang tidak terlalu cepat).

Ada batasan lain, daftar yang dapat ditemukan secara umum di sini . Dan di sini Anda dapat membaca pemikiran salah satu pengembang JIT utama tentang desain inliner dan artikelnya tentang penggunaan Machine Learning untuk kasus ini.

All Articles