Haruskah panjang array disimpan ke variabel lokal di C #?

Saya perhatikan bahwa orang sering menggunakan konstruksi seperti ini:

var length = array.Length; for (int i = 0; i < length; i++) {    //do smth } 

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):
Tanpa Variabel ()
; jumlah int = 0;
xor edi , edi
; int i = 0;
xor esi , esi
; int [] localRefToArray = this.array;
mov edx , dword ptr [ ecx + 4 ]
; int arrayLength = localRefToArray.Length;
mov ecx , dword ptr [ edx + 4 ]
; if (arrayLength == 0) mengembalikan jumlah;
uji ecx , ecx
keluar saja
; int arrayLength2 = localRefToArray.Length;
mov eax , dword ptr [ edx + 4 ]
; if (i> = arrayLength2)
; melempar IndexOutOfRangeException baru ();
lingkaran :
cmp esi , eax
jae 056e2d31
; jumlah + = localRefToArray [i];
tambahkan edi , dword ptr [ edx + esi * 4 + 8 ]
; i ++;
termasuk esi
; if (i <arrayLength) goto loop
cmp ecx , esi
jg loop
; jumlah pengembalian;
keluar :
mov eax , edi
WithVariable ()
; jumlah int = 0;
xor esi , esi
; int [] localRefToArray = this.array;
mov edx , dword ptr [ ecx + 4 ]
; int arrayLength = localRefToArray.Length;
mov edi , dword ptr [ edx + 4 ]
; int i = 0;
xor eax , eax
; if (arrayLength == 0) mengembalikan jumlah;
menguji edi edi
keluar saja
; int arrayLength2 = localRefToArray.Length;
mov ecx , dword ptr [ edx + 4 ]
; if (i> = arrayLength2)
; melempar IndexOutOfRangeException baru ();
lingkaran :
cmp eax , ecx
jae 05902d31
; jumlah + = localRefToArray [i];
tambahkan esi , dword ptr [ edx + eax * 4 + 8 ]
; i ++;
inc eax
; if (i <arrayLength) goto loop
cmp eax edi
jl loop
; jumlah pengembalian;
keluar :
mov eax , esi

Perbandingan di Meld:


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:
Tanpa Variabel ()
jumlah int = 0 ;
xor edi , edi
int i = 0 ;
xor esi , esi
int [ ] localRefToArray = ini . array ;
mov edx , dword ptr [ ecx + 4 ]
int arrayLength = localRefToArray . Panjang ;
mov ebx , dword ptr [ edx + 4 ]
if ( arrayLength == 0 ) mengembalikan jumlah ;
tes ebx , ebx
keluar saja
int arrayLength2 = localRefToArray . Panjang ;
mov ecx , dword ptr [ edx + 4 ]
if ( i> = arrayLength2 )
melempar IndexOutOfRangeException baru ( ) ;
lingkaran :
cmp esi ecx
jae 05562d39
int t = array [ i ] ;
mov eax , dword ptr [ edx + esi * 4 + 8 ]
t + = jumlah ;
tambahkan eax , edi
t + = arrayLength ;
tambahkan eax , ebx
jumlah = t ;
mov edi , eax
i ++ ;
termasuk esi
if ( i <arrayLength ) goto loop
cmp ebx , esi
jg loop
jumlah pengembalian ;
keluar :
mov eax , edi
WithVariable ()
jumlah int = 0 ;
xor esi , esi
int [ ] localRefToArray = ini . array ;
mov edx , dword ptr [ ecx + 4 ]
int arrayLength = localRefToArray . Panjang ;
mov ebx , dword ptr [ edx + 4 ]
int i = 0 ;
xor ecx , ecx
if ( arrayLength == 0 ) ( jumlah pengembalian ;)
tes ebx , ebx
keluar saja
int arrayLength2 = localRefToArray . Panjang ;
mov edi , dword ptr [ edx + 4 ]
if ( i> = arrayLength2 )
melempar IndexOutOfRangeException baru ( ) ;
lingkaran :
cmp ecx edi
jae 04b12d39
int t = array [ i ] ;
mov eax , dword ptr [ edx + ecx * 4 + 8 ]
t + = jumlah ;
tambahkan eax , esi
t + = arrayLength ;
tambahkan eax , ebx
jumlah = t ;
mov esi , eax
i ++ ;
inc ecx
if ( i <arrayLength ) goto loop
cmp ecx , ebx
jl loop
jumlah pengembalian ;
keluar :
mov eax , esi

Perbandingan di Meld:


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 ).
Perbandingan


Secara keseluruhan, berikut adalah hasil dari to foreach benchmark untuk array 1 juta elemen:
 sum+=array[i]; 
Metode
ItemCount
Berarti
Kesalahan
Stddev
Median
Rasio
Rasio SD
Foreach
1.000.000
1,401 ms
0,2691 ms
0,7935 ms
1,694 ms
1,00
0,00
Untuk
1.000.000
1,586 ms
0,3204 ms
0,9447 ms
1,740 ms
1.23
0,65
Dan untuk
 sum+=array[i] + array.Length; 
Metode
ItemCount
Berarti
Kesalahan
Stddev
Median
Rasio
Rasio SD
Foreach
1.000.000
1,703 ms
0,3010 ms
0,8874 ms
1,726 ms
1,00
0,00
Untuk
1.000.000
1,715 ms
0,2859 ms
0,8430 ms
1,956 ms
1.13
0,56

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 .

Source: https://habr.com/ru/post/id454582/


All Articles