Cara mempercepat kode Anda menggunakan cache CPU

Cache CPU mengurangi latensi memori saat data diakses dari memori sistem utama. Pengembang dapat dan harus memanfaatkan cache CPU untuk meningkatkan kinerja aplikasi.

Cara kerja cache CPU

CPU modern biasanya memiliki tiga tingkat cache, berlabel L1, L2, dan L3, yang mencerminkan urutan pemeriksaan CPU. CPU sering memiliki cache data, cache instruksi (untuk kode), dan cache terpadu (untuk apa pun). Mengakses cache ini jauh lebih cepat daripada mengakses RAM: Biasanya, cache L1 sekitar 100 kali lebih cepat daripada RAM untuk akses data, dan cache L2 25 kali lebih cepat daripada RAM untuk akses data.

Ketika perangkat lunak Anda berjalan dan perlu menarik data atau instruksi, cache CPU diperiksa terlebih dahulu, kemudian RAM sistem yang lebih lambat, dan terakhir drive disk yang jauh lebih lambat. Itulah mengapa Anda ingin mengoptimalkan kode Anda untuk mencari apa yang mungkin diperlukan dari cache CPU terlebih dahulu.

Kode Anda tidak dapat menentukan di mana instruksi data dan data berada — perangkat keras komputer yang melakukannya — jadi Anda tidak dapat memaksa elemen tertentu ke dalam cache CPU. Tetapi Anda dapat mengoptimalkan kode Anda untuk mengambil ukuran cache L1, L2, atau L3 di sistem Anda menggunakan Windows Management Instrumentation (WMI) untuk mengoptimalkan ketika aplikasi Anda mengakses cache dan dengan demikian kinerjanya.

CPU tidak pernah mengakses cache byte demi byte. Sebaliknya, mereka membaca memori dalam baris cache, yang merupakan potongan memori yang umumnya berukuran 32, 64, atau 128 byte.

Daftar kode berikut menggambarkan bagaimana Anda dapat mengambil ukuran cache CPU L2 atau L3 di sistem Anda:

public static uint GetCPUCacheSize (string cacheType) {coba {menggunakan (ManagementObject managementObject = new ManagementObject ("Win32_Processor.DeviceID = 'CPU0'")) {return (uint) (managementObject [cacheType]); }} menangkap {return 0; }} static void Main (string [] args) {uint L2CacheSize = GetCPUCacheSize ("L2CacheSize"); uint L3CacheSize = GetCPUCacheSize ("L3CacheSize"); Console.WriteLine ("L2CacheSize:" + L2CacheSize.ToString ()); Console.WriteLine ("L3CacheSize:" + L3CacheSize.ToString ()); Console.Read (); }

Microsoft memiliki dokumentasi tambahan pada kelas WMI Win32_Processor.

Pemrograman untuk kinerja: Contoh kode

Saat Anda memiliki objek dalam tumpukan, tidak ada overhead pengumpulan sampah. Jika Anda menggunakan objek berbasis heap, selalu ada biaya yang terlibat dengan pengumpulan sampah generasi untuk mengumpulkan atau memindahkan objek dalam heap atau memadatkan memori heap. Cara yang baik untuk menghindari overhead pengumpulan sampah adalah dengan menggunakan struct, bukan class.

Cache berfungsi paling baik jika Anda menggunakan struktur data berurutan, seperti array. Pengurutan berurutan memungkinkan CPU dapat membaca ke depan dan juga membaca secara spekulatif untuk mengantisipasi permintaan selanjutnya. Dengan demikian, algoritme yang mengakses memori secara berurutan selalu cepat.

Jika Anda mengakses memori secara acak, CPU memerlukan baris cache baru setiap kali Anda mengakses memori. Itu mengurangi kinerja.

Cuplikan kode berikut mengimplementasikan program sederhana yang mengilustrasikan manfaat menggunakan struct di atas kelas:

 struct RectangleStruct {public int breadth; tinggi int publik; } kelas RectangleClass {public int breadth; tinggi int publik; }

Kode berikut memprofilkan kinerja penggunaan array struct terhadap array kelas. Untuk tujuan ilustrasi, saya telah menggunakan sejuta objek untuk keduanya, tetapi biasanya Anda tidak membutuhkan banyak objek dalam aplikasi Anda.

kekosongan statis Utama (string [] args) {const int size = 1000000; var structs = new RectangleStruct [size]; var class = new RectangleClass [size]; var sw = Stopwatch baru (); sw.Start (); untuk (var i = 0; i <size; ++ i) {structs [i] = new RectangleStruct (); struct [i] .breadth = 0 struct [i] .height = 0; } var structTime = sw.ElapsedMilliseconds; sw.Reset (); sw.Start (); untuk (var i = 0; i <size; ++ i) {class [i] = new RectangleClass (); kelas [i] .breadth = 0; kelas [i] .height = 0; } var classTime = sw.ElapsedMilliseconds; sw. Berhenti (); Console.WriteLine ("Waktu yang dibutuhkan oleh array kelas:" + classTime.ToString () + "milidetik."); Console.WriteLine ("Waktu yang dibutuhkan oleh array struct:" + structTime.ToString () + "milidetik."); Console.Read (); }

Programnya sederhana: Ini membuat 1 juta objek struct dan menyimpannya dalam sebuah array. Ini juga membuat 1 juta objek kelas dan menyimpannya dalam array lain. Lebar dan tinggi properti diberi nilai nol pada setiap instance.

Seperti yang Anda lihat, menggunakan struct ramah-cache memberikan keuntungan kinerja yang besar.

Aturan praktis untuk penggunaan cache CPU yang lebih baik

Jadi, bagaimana Anda menulis kode yang paling baik menggunakan cache CPU? Sayangnya, tidak ada formula ajaib. Tetapi ada beberapa aturan praktis:

  • Hindari penggunaan algoritma dan struktur data yang menunjukkan pola akses memori yang tidak teratur; gunakan struktur data linier sebagai gantinya.
  • Gunakan tipe data yang lebih kecil dan atur data sehingga tidak ada lubang perataan.
  • Pertimbangkan pola akses dan manfaatkan struktur data linier.
  • Tingkatkan lokalitas spasial, yang menggunakan setiap baris cache secara maksimal setelah dipetakan ke cache.