Saya perhatikan bahwa orang sering menggunakan konstruksi seperti ini:
var length = array.Length; for (int i = 0; i < length; i++) {
Mereka berpikir bahwa memiliki panggilan ke Array. Panjang pada setiap iterasi akan membuat CLR mengambil lebih banyak waktu untuk mengeksekusi kode. Untuk menghindarinya mereka menyimpan nilai panjang dalam variabel lokal.
Mari kita cari tahu (sekali dan untuk semua!) Jika ini adalah hal yang layak atau menggunakan variabel sementara adalah buang-buang waktu.
Untuk memulai, mari kita periksa metode C # ini:
public int WithoutVariable() { int sum = 0; for (int i = 0; i < array.Length; i++) { sum += array[i]; } return sum; } public int WithVariable() { int sum = 0; int length = array.Length; for (int i = 0; i < length; i++) { sum += array[i]; } return sum; }
Berikut ini tampilannya setelah diproses oleh kompiler JIT (untuk .NET Framework 4.7.2 di bawah LegacyJIT-x86):
Sangat sepele untuk memperhatikan bahwa mereka memiliki jumlah instruksi assembler yang persis sama - 15. Bahkan logika dari instruksi ini hampir sama. Ada sedikit perbedaan dalam urutan variabel inisialisasi dan perbandingan apakah siklus harus dilanjutkan. Kita dapat mencatat bahwa dalam kedua kasus panjang array terdaftar dua kali sebelum siklus:
- Untuk memeriksa 0 (arrayLength)
- Ke dalam variabel sementara untuk memeriksa kondisi siklus (arrayLength2).
Ternyata kedua metode akan dikompilasi ke dalam kode yang sama persis, tetapi yang pertama ditulis lebih cepat, meskipun tidak ada manfaat dalam hal waktu eksekusi.
Kode assembler di atas membuat saya berpikir dan saya memutuskan untuk memeriksa beberapa metode lagi:
public int WithoutVariable() { int sum = 0; for(int i = 0; i < array.Length; i++) { sum += array[i] + array.Length; } return sum; } public int WithVariable() { int sum = 0; int length = array.Length; for(int i = 0; i < length; i++) { sum += array[i] + length; } return sum; }
Sekarang elemen dan panjang array saat ini sedang ditambahkan, tetapi dalam kasus pertama panjang array diminta setiap kali, dan dalam kasus kedua itu disimpan sekali ke dalam variabel lokal. Mari kita lihat kode assembler dari metode ini:
Sekali lagi, jumlah instruksi sama, dan juga (hampir) instruksi itu sendiri. Satu-satunya perbedaan adalah urutan variabel inisialisasi dan kondisi pemeriksaan untuk kelanjutan siklus. Anda dapat mencatat bahwa dalam perhitungan jumlah, hanya panjang array pertama yang diperhitungkan. Sudah jelas bahwa ini:
int arrayLength2 = localRefToArray . Panjang ;
mov edi , dword ptr [ edx + 4 ]
if ( i> = arrayLength2 ) melempar IndexOutOfRangeException baru ( ) ;
cmp ecx edi
jae 04b12d39
dalam keempat metode adalah array batas yang diperiksa dan dijalankan untuk setiap elemen array.
Kita sudah dapat membuat kesimpulan pertama: menggunakan variabel tambahan untuk mencoba mempercepat siklus adalah buang-buang waktu, karena kompiler akan tetap melakukannya untuk Anda. Satu-satunya alasan untuk menyimpan array panjang ke dalam variabel adalah untuk membuat kode lebih mudah dibaca.
ForEach adalah situasi lain sepenuhnya. Pertimbangkan tiga metode berikut:
public int ForEachWithoutLength() { int sum = 0; foreach (int i in array) { sum += i; } return sum; } public int ForEachWithLengthWithoutLocalVariable() { int sum = 0; foreach (int i in array) { sum += i + array.Length; } return sum; } public int ForEachWithLengthWithLocalVariable() { int sum = 0; int length = array.Length; foreach (int i in array) { sum += i + length; } return sum; }
Dan ini kode setelah JIT:
ForEachWithoutLength (); jumlah int = 0;
xor esi , esi
; int [] localRefToArray = this.array;
mov ecx , dword ptr [ ecx + 4 ]
; int i = 0;
xor edx , edx
; int arrayLength = localRefToArray.Length;
mov edi , dword ptr [ ecx + 4 ]
; if (arrayLength == 0) keluar goto;
menguji edi edi
keluar saja
; int t = array [i];
lingkaran :
mov eax , dword ptr [ ecx + edx * 4 + 8 ]
; jumlah + = i;
tambahkan esi , eax
; i ++;
termasuk edx
; if (i <arrayLength) goto loop
cmp edi edx
jg loop
; jumlah pengembalian;
keluar :
mov eax , esi
ForEachWithLengthWithoutLocalVariable (); jumlah int = 0;
xor esi , esi
; int [] localRefToArray = this.array;
mov ecx , dword ptr [ ecx + 4 ]
; int i = 0;
xor edx , edx
; int arrayLength = localRefToArray.Length;
mov edi , dword ptr [ ecx + 4 ]
; if (arrayLength == 0) pergi keluar
menguji edi edi
keluar saja
; int t = array [i];
lingkaran :
mov eax , dword ptr [ ecx + edx * 4 + 8 ]
; jumlah + = i;
tambahkan esi , eax
; jumlah + = localRefToArray.Length;
tambahkan esi , dword ptr [ ecx + 4 ]
; i ++;
termasuk edx
; if (i <arrayLength) goto loop
cmp edi edx
jg loop
; jumlah pengembalian;
keluar :
mov eax , esi
ForEachWithLengthWithLocalVariable (); jumlah int = 0;
xor esi , esi
; int [] localRefToArray = this.array;
mov edx , dword ptr [ ecx + 4 ]
; int length = localRefToArray.Length;
mov ebx , dword ptr [ edx + 4 ]
; int i = 0;
xor ecx , ecx
; int arrayLength = localRefToArray.Length;
mov edi , dword ptr [ edx + 4 ]
; if (arrayLength == 0) keluar goto;
menguji edi edi
keluar saja
; int t = array [i];
lingkaran :
mov eax , dword ptr [ edx + ecx * 4 + 8 ]
; jumlah + = i;
tambahkan esi , eax
; jumlah + = panjang;
tambahkan esi , ebx
; i ++;
inc ecx
; if (i <arrayLength) goto loop
cmp edi ecx
jg loop
; jumlah pengembalian;
keluar :
mov eax , esi
Hal pertama yang terlintas dalam pikiran adalah bahwa dibutuhkan lebih sedikit instruksi assembler daripada
untuk siklus (misalnya, untuk penjumlahan elemen sederhana dibutuhkan 12 instruksi di
muka , tetapi 15 di
untuk ).
Secara keseluruhan, berikut adalah hasil dari to
foreach benchmark untuk array 1 juta elemen:
sum+=array[i];
Dan untuk
sum+=array[i] + array.Length;
ForEach berjalan melalui array jauh lebih cepat daripada
untuk . Mengapa Untuk mengetahuinya, kita perlu membandingkan kode setelah JIT:
Perbandingan ketiga opsi foreach Mari kita lihat ForEachWithoutLength. Panjang array hanya diminta satu kali dan tidak ada pemeriksaan untuk batas array. Itu terjadi karena siklus ForEach pertama kali mengubah koleksi di dalam siklus, dan yang kedua tidak akan pernah keluar dari koleksi. Karena itu, JIT mampu menghapus batas-batas array pemeriksaan.
Sekarang mari kita perhatikan dengan seksama ForEachWithLengthWIthoutLocalVariable. Hanya ada satu bagian yang aneh, di mana jumlah + = panjang tidak terjadi pada arrayLength variabel lokal yang disimpan sebelumnya, tetapi ke bagian baru yang diminta aplikasi dari memori setiap kali. Itu berarti, akan ada N + 1 permintaan memori untuk panjang array, di mana N adalah panjang array.
Dan sekarang kita sampai pada ForEachWithLengthWithLocalVariable. Kode di sana persis sama dengan contoh sebelumnya, kecuali penanganan panjang array. Kompilator sekali lagi menghasilkan array variabel lokalPanjang yang digunakan untuk memeriksa apakah array kosong, tetapi kompiler masih dengan jujur menyimpan panjang variabel lokal yang kami nyatakan, dan itulah yang digunakan dalam penjumlahan di dalam siklus. Ternyata metode ini meminta panjang array dari memori hanya dua kali. Perbedaannya sangat sulit untuk diperhatikan di dunia nyata.
Dalam semua kasus, kode assembler ternyata sangat sederhana karena metode itu sendiri sederhana. Jika metode memiliki lebih banyak parameter, itu harus bekerja dengan stack, variabel mungkin mendapatkan toko di luar register, akan ada lebih banyak pemeriksaan, tetapi logika utama akan tetap sama: memperkenalkan variabel lokal untuk panjang array hanya berguna untuk membuat kode lebih mudah dibaca. Ternyata
Foreach sering berjalan melalui array lebih cepat daripada
For .