Memperlambat Windows Bagian 3: Proses Pengakhiran
Penulis terlibat dalam mengoptimalkan kinerja Chrome di Google - kira-kira. trans.Pada musim panas 2017, saya berjuang dengan masalah kinerja Windows. Pengakhiran proses lambat, serial dan diblokir antrian input sistem, yang menyebabkan beberapa pembekuan kursor mouse selama perakitan Chrome. Alasan utama adalah bahwa pada akhir proses, Windows menghabiskan banyak waktu mencari objek GDI, sambil memegang bagian penting dari system-global user32. Saya membicarakan hal ini dalam artikel
"prosesor 24-core, tapi saya tidak bisa menggerakkan kursor .
"Microsoft memperbaiki bug, dan saya kembali ke bisnis saya, tetapi ternyata bug itu kembali. Ada keluhan tentang lambatnya operasi tes LLVM, dengan seringnya input hang.
Namun nyatanya, bug itu tidak kembali. Alasannya adalah perubahan kode kami.
Masalah 2017

Setiap proses Windows berisi beberapa deskriptor objek GDI standar. Untuk proses yang tidak melakukan apa pun dengan grafik, deskriptor ini biasanya NULL. Pada akhir proses, Windows memanggil beberapa fungsi untuk deskriptor ini, bahkan jika mereka NULL. Tidak masalah - fitur-fiturnya bekerja dengan cepat - sampai rilis Windows 10 Anniversary Edition, di mana
beberapa perubahan keamanan membuat fitur-fitur ini lambat . Selama operasi, mereka memegang kunci yang sama yang digunakan untuk acara masukan. Ketika sejumlah besar proses dihentikan secara bersamaan, masing-masing membuat beberapa panggilan ke fungsi lambat yang menahan kunci kritis ini, yang pada akhirnya menyebabkan input pengguna diblokir dan kursor membeku.
Tambalan Microsoft bukan untuk memanggil fungsi-fungsi ini untuk proses tanpa objek GDI. Saya tidak tahu detailnya, tapi saya pikir tambalan Microsoft adalah seperti ini:
+ if (IsGUIProcess())
+ NtGdiCloseProcess();
– NtGdiCloseProcess();
Artinya, lewati saja pembersihan GDI jika prosesnya bukan proses GUI / GDI.
Karena kompiler dan proses lain yang kami buat dan akhiri dengan cepat tidak menggunakan objek GDI, tambalan ini ternyata cukup untuk memperbaiki pembekuan UI.
Masalah 2018
Ternyata prosesnya sangat mudah sebenarnya dialokasikan beberapa objek GDI standar. Jika proses Anda memuat gdi32.dll, maka Anda akan secara otomatis menerima objek GDI (DC, permukaan, wilayah, kuas, font, dll.), Baik Anda membutuhkannya atau tidak (perhatikan bahwa objek GDI standar ini tidak ditampilkan di Task Manager di antara objek GDI untuk proses).
Tapi itu seharusnya tidak menjadi masalah. Maksud saya, mengapa kompiler memuat gdi32.dll? Nah, ternyata jika Anda memuat user32.dll, shell32.dll, ole32.dll atau banyak DLL lainnya, maka Anda akan secara otomatis mendapatkan tambahan gdi32.dll (dengan objek GDI standar yang disebutkan di atas). Dan sangat mudah untuk secara tidak sengaja mengunduh salah satu perpustakaan ini.
LLVM menguji ketika memuat setiap proses yang disebut
CommandLineToArgvW (shell32.dll), dan kadang-kadang disebut
SHGetKnownFolderPath (juga shell32.dll) Panggilan ini cukup untuk mengeluarkan gdi32.dll dan menghasilkan objek GDI standar yang menakutkan ini. Karena rangkaian uji LLVM menghasilkan
begitu banyak proses, pada akhirnya membuat cerita bersambung pada selesainya proses, menyebabkan penundaan besar dan pembekuan input, jauh lebih buruk daripada yang ada pada 2017.
Tapi kali ini kami tahu tentang masalah utama dengan pemblokiran, jadi kami segera tahu apa yang harus dilakukan.
Pertama-tama, kita menyingkirkan memanggil
CommandLineToArgvW ,
secara manual mem-parsing baris perintah . Setelah itu, test suite LLVM jarang memanggil fungsi dari DLL yang bermasalah. Tapi kami tahu sebelumnya bahwa ini tidak akan memengaruhi kinerja dengan cara apa pun. Alasannya adalah bahwa bahkan panggilan
bersyarat yang tersisa sudah cukup untuk selalu menarik shell32.dll, yang pada gilirannya menarik gdi32.dll, yang menciptakan objek GDI standar.
Perbaikan kedua adalah
penundaan loading shell32.dll . Penundaan pemuatan berarti bahwa perpustakaan memuat sesuai permintaan - saat fungsi dipanggil - alih-alih memuat saat proses dimulai. Ini berarti bahwa shell32.dll dan gdi32.dll jarang dimuat, dan tidak selalu.
Setelah itu, test suite LLVM mulai berjalan
lima kali lebih cepat - dalam satu menit, bukan lima. Dan tidak ada lagi tikus yang membeku di mesin pengembangan, sehingga karyawan dapat bekerja secara normal selama pelaksanaan tes. Ini adalah percepatan gila untuk perubahan yang begitu sederhana, dan penulis tambalan itu sangat bersyukur atas investigasi saya sehingga ia menominasikan saya untuk
bonus perusahaan .
Terkadang perubahan terkecil memiliki konsekuensi terbesar. Anda hanya perlu
tahu di mana harus memanggil "nol" .
Jalur eksekusi tidak diterima

Perlu diulang bahwa kita memperhatikan kode yang
tidak dieksekusi - dan ini adalah perubahan utama. Jika Anda memiliki alat baris perintah yang tidak mengakses gdi32.dll, maka menambahkan kode dengan panggilan fungsi
bersyarat akan memperlambat proses berkali-kali jika gdi32.dll dimuat. Dalam contoh di bawah ini,
CommandLineToArgvW tidak pernah dipanggil, tetapi bahkan kehadiran sederhana dalam kode (tanpa penundaan panggilan) mempengaruhi kinerja:
int main(int argc, char* argv[]) { if (argc < 0) { CommandLineToArgvW(nullptr, nullptr); // shell32.dll, pulls in gdi32.dll } }
Jadi ya, menghapus panggilan fungsi, bahkan jika kode tidak pernah dieksekusi, mungkin cukup untuk secara signifikan meningkatkan kinerja dalam beberapa kasus.
Reproduksi patologi

Ketika saya menyelidiki kesalahan awal, saya menulis sebuah program yang menciptakan 1000 proses dan kemudian membunuh semuanya secara paralel. Ini mereproduksi pembekuan, dan ketika Microsoft memperbaiki kesalahan, saya menggunakan program pengujian untuk memeriksa patch: lihat
videonya . Setelah reinkarnasi bug, saya mengubah program saya dengan
menambahkan opsi -user32 , yang memuat user32.dll untuk masing-masing dari ribuan proses pengujian. Seperti yang diharapkan, waktu penyelesaian semua proses pengujian meningkat secara dramatis dengan opsi ini, dan mudah untuk mendeteksi kursor mouse yang membeku. Waktu pembuatan proses juga meningkat dengan opsi -user32, tetapi tidak ada suspensi kursor selama pembuatan proses. Anda dapat menggunakan program ini dan melihat seberapa buruk masalahnya. Berikut adalah beberapa hasil khas notebook empat-inti / delapan-threaded saya setelah seminggu uptime. Opsi -user32 meningkatkan waktu untuk semuanya, tetapi kunci
UserCrit pada proses berakhir terutama secara dramatis:
> ProcessCreatetests.exe
Process creation took 2.448 s (2.448 ms per process).
Lock blocked for 0.008 s total, maximum was 0.001 s.
Process destruction took 0.801 s (0.801 ms per process).
Lock blocked for 0.004 s total, maximum was 0.001 s.
> ProcessCreatetests.exe -user32
Testing with 1000 descendant processes with user32.dll loaded.
Process creation took 3.154 s (3.154 ms per process).
Lock blocked for 0.032 s total, maximum was 0.007 s.
Process destruction took 2.240 s (2.240 ms per process).
Lock blocked for 1.991 s total, maximum was 0.864 s.
Menggali lebih dalam hanya untuk bersenang-senang
Saya memikirkan beberapa metode ETW yang dapat digunakan untuk mempelajari masalah secara lebih rinci, dan sudah mulai menulisnya. Tetapi saya menemukan perilaku yang tidak dapat dijelaskan, yang saya putuskan untuk mencurahkan artikel terpisah. Cukuplah untuk mengatakan bahwa dalam kasus ini, Windows berperilaku lebih aneh.
Artikel lain dalam seri:
Sastra