Saya baru saja merevisi bab 4 dari C # di Kedalaman yang berkaitan dengan jenis nullable, dan saya menambahkan bagian tentang menggunakan operator "sebagai", yang memungkinkan Anda untuk menulis:
object o = ...;
int? x = o as int?;
if (x.HasValue)
{
... // Use x.Value in here
}
Saya pikir ini benar-benar rapi, dan itu bisa meningkatkan kinerja lebih dari setara C # 1, menggunakan "is" diikuti oleh pemain - setelah semua, dengan cara ini kita hanya perlu meminta pemeriksaan tipe dinamis sekali, dan kemudian pemeriksaan nilai sederhana .
Namun, ini tampaknya bukan masalahnya. Saya telah memasukkan contoh aplikasi uji di bawah ini, yang pada dasarnya menjumlahkan semua bilangan bulat dalam sebuah array objek - tetapi array tersebut berisi banyak referensi nol dan referensi string serta bilangan bulat kotak. Tolok ukur mengukur kode yang harus Anda gunakan dalam C # 1, kode menggunakan operator "sebagai", dan hanya untuk menendang solusi LINQ. Yang mengherankan saya, kode C # 1 adalah 20 kali lebih cepat dalam kasus ini - dan bahkan kode LINQ (yang saya harapkan akan lebih lambat, mengingat iterator yang terlibat) mengalahkan kode "sebagai".
Apakah implementasi .NET isinst
untuk tipe nullable benar-benar lambat? Apakah ini tambahanunbox.any
yang menyebabkan masalah? Apakah ada penjelasan lain untuk ini? Saat ini rasanya saya harus memasukkan peringatan untuk tidak menggunakan ini dalam situasi sensitif kinerja ...
Hasil:
Cast: 10000000: 121
As: 10000000: 2211
LINQ: 10000000: 2143
Kode:
using System;
using System.Diagnostics;
using System.Linq;
class Test
{
const int Size = 30000000;
static void Main()
{
object[] values = new object[Size];
for (int i = 0; i < Size - 2; i += 3)
{
values[i] = null;
values[i+1] = "";
values[i+2] = 1;
}
FindSumWithCast(values);
FindSumWithAs(values);
FindSumWithLinq(values);
}
static void FindSumWithCast(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values)
{
if (o is int)
{
int x = (int) o;
sum += x;
}
}
sw.Stop();
Console.WriteLine("Cast: {0} : {1}", sum,
(long) sw.ElapsedMilliseconds);
}
static void FindSumWithAs(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values)
{
int? x = o as int?;
if (x.HasValue)
{
sum += x.Value;
}
}
sw.Stop();
Console.WriteLine("As: {0} : {1}", sum,
(long) sw.ElapsedMilliseconds);
}
static void FindSumWithLinq(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = values.OfType<int>().Sum();
sw.Stop();
Console.WriteLine("LINQ: {0} : {1}", sum,
(long) sw.ElapsedMilliseconds);
}
}
as
jenis nullable. Menarik, karena tidak dapat digunakan pada tipe nilai lainnya. Sebenarnya, lebih mengejutkan.as
upaya untuk melakukan cast ke suatu tipe dan jika gagal maka ia mengembalikan nol. Anda tidak dapat mengatur tipe nilai menjadi nullJawaban:
Jelas kode mesin yang dapat dihasilkan oleh kompiler JIT untuk kasus pertama jauh lebih efisien. Satu aturan yang benar-benar membantu adalah bahwa suatu objek hanya dapat dibuka kotaknya ke variabel yang memiliki tipe yang sama dengan nilai kotak. Itu memungkinkan kompiler JIT untuk menghasilkan kode yang sangat efisien, tidak ada konversi nilai yang harus dipertimbangkan.
The adalah tes operator mudah, hanya memeriksa apakah objek tersebut tidak nol dan dari tipe yang diharapkan, dibutuhkan tetapi beberapa instruksi kode mesin. Para pemain juga mudah, kompiler JIT tahu lokasi bit nilai dalam objek dan menggunakannya secara langsung. Tidak ada penyalinan atau konversi yang terjadi, semua kode mesin sebaris dan mengambil tetapi sekitar selusin instruksi. Ini harus benar-benar efisien kembali. NET 1.0 ketika tinju adalah umum.
Casting ke int? Dibutuhkan lebih banyak pekerjaan. Representasi nilai integer kotak tidak kompatibel dengan tata letak memori
Nullable<int>
. Konversi diperlukan dan kode ini rumit karena kemungkinan tipe enum kotak. Kompiler JIT menghasilkan panggilan ke fungsi pembantu CLR bernama JIT_Unbox_Nullable untuk menyelesaikan pekerjaan. Ini adalah fungsi tujuan umum untuk semua tipe nilai, banyak kode di sana untuk memeriksa tipe. Dan nilainya disalin. Sulit memperkirakan biaya karena kode ini dikunci di dalam mscorwks.dll, tetapi ratusan instruksi kode mesin kemungkinan besar.Metode ekstensi Linq OfType () juga menggunakan operator is dan pemain. Namun ini gips untuk tipe generik. Kompiler JIT menghasilkan panggilan ke fungsi pembantu, JIT_Unbox () yang dapat melakukan gips ke tipe nilai sewenang-wenang. Saya tidak memiliki penjelasan yang bagus mengapa ini selambat para pemain
Nullable<int>
, mengingat bahwa lebih sedikit pekerjaan yang harus dilakukan. Saya menduga bahwa ngen.exe dapat menyebabkan masalah di sini.sumber
Menurut saya itu
isinst
hanya sangat lambat pada jenis nullable. Dalam metodeFindSumWithCast
saya berubahuntuk
yang juga secara signifikan memperlambat eksekusi. Satu-satunya perbedaan dalam IL yang bisa saya lihat adalah itu
diubah menjadi
sumber
isinst
diikuti oleh tes untuk pembatalan dan kemudian kondisional sebuahunbox.any
. Dalam kasus nullable ada yang tanpa syaratunbox.any
.isinst
danunbox.any
lebih lambat pada jenis nullable.Ini awalnya dimulai sebagai Komentar untuk jawaban yang sangat baik dari Hans Passant, tetapi terlalu lama jadi saya ingin menambahkan beberapa bit di sini:
Pertama,
as
operator C # akan memancarkanisinst
instruksi IL (begitu jugais
operator). (Instruksi lain yang menarik adalahcastclass
, dipancarkan ketika Anda melakukan cast langsung dan kompiler tahu bahwa pengecekan runtime tidak dapat dihentikan).Inilah yang
isinst
dilakukan ( ECMA 335 Partition III, 4.6 ):Yang paling penting:
Jadi, pembunuh kinerja tidak
isinst
dalam kasus ini, tetapi tambahanunbox.any
. Ini tidak jelas dari jawaban Hans, karena dia hanya melihat kode JIT. Secara umum, kompiler C # akan memancarkanunbox.any
setelahisinst T?
(tetapi akan menghilangkannya jika Anda melakukannyaisinst T
, kapanT
adalah jenis referensi).Kenapa bisa begitu?
isinst T?
tidak pernah memiliki efek yang jelas, yaitu Anda mendapatkan kembaliT?
. Alih-alih, semua instruksi ini memastikan bahwa Anda memiliki"boxed T"
yang dapat dibuka kotaknyaT?
. Untuk mendapatkan yang sebenarnyaT?
, kita masih perlu unbox kami"boxed T"
untukT?
, yang mengapa compiler memancarkanunbox.any
setelahisinst
. Jika Anda memikirkannya, ini masuk akal karena "format kotak" untukT?
hanya"boxed T"
dan membuatcastclass
danisinst
melakukan unbox akan tidak konsisten.Mencadangkan temuan Hans dengan beberapa informasi dari standar , ini dia:
(ECMA 335 Partisi III, 4.33):
unbox.any
(ECMA 335 Partisi III, 4.32):
unbox
sumber
Menariknya, saya menyampaikan umpan balik tentang dukungan operator melalui
dynamic
menjadi urutan-of-magnitude lebih lambat untukNullable<T>
(mirip dengan tes awal ini ) - Saya menduga karena alasan yang sangat mirip.Harus cinta
Nullable<T>
. Satu lagi yang menyenangkan adalah bahwa meskipun JIT melihat (dan menghapus)null
untuk struct yang tidak dapat dibatalkan, ia membuatnya untukNullable<T>
:sumber
null
untuk struct yang tidak dapat dibatalkan"? Apakah maksud Anda menggantinull
dengan nilai default atau sesuatu selama runtime?T
dll). Persyaratan stack dll bergantung pada args (jumlah ruang stack untuk lokal, dll), sehingga Anda mendapatkan satu JIT untuk permutasi unik yang melibatkan tipe nilai. Namun, semua referensi memiliki ukuran yang sama sehingga berbagi JIT. Saat melakukan JIT per nilai, ia dapat memeriksa beberapa skenario yang jelas, dan mencoba mengeluarkan kode yang tidak dapat dijangkau karena hal-hal seperti null yang tidak mungkin. Tidak sempurna, perhatikan. Juga, saya mengabaikan AOT untuk hal di atas.count
variabel. MenambahkanConsole.Write(count.ToString()+" ");
setelahwatch.Stop();
dalam kedua kasus memperlambat tes lain dengan hanya di bawah urutan besarnya, tetapi tes nullable tidak terbatas tidak berubah. Catatan ada juga perubahan ketika Anda menguji kasus ketikanull
dilewatkan, mengkonfirmasikan kode asli tidak benar-benar melakukan pemeriksaan nol dan kenaikan untuk tes lainnya. LinqpadIni adalah hasil dari FindSumWithAsAndHas di atas:
Ini adalah hasil dari FindSumWithCast:
Temuan:
Menggunakan
as
, itu menguji terlebih dahulu jika suatu objek adalah instance dari Int32; di bawah tenda yang digunakanisinst Int32
(yang mirip dengan kode tulisan tangan: if (o int)). Dan menggunakanas
, itu juga tanpa syarat membuka kotak objek. Dan itu adalah pembunuh kinerja nyata untuk memanggil properti (itu masih fungsi di bawah tenda), IL_0027Menggunakan gips, Anda menguji terlebih dahulu jika objek adalah
int
if (o is int)
; di bawah tenda ini menggunakanisinst Int32
. Jika ini adalah instance dari int, maka Anda dapat dengan aman membuka kotak nilainya, IL_002DSederhananya, ini adalah pseudo-code menggunakan
as
pendekatan:Dan ini adalah pseudo-code menggunakan pendekatan cast:
Jadi para pemain (
(int)a[i]
, yah sintaksnya terlihat seperti pemain, tetapi sebenarnya unboxing, cast dan unboxing berbagi sintaks yang sama, lain kali saya akan menjadi jago dengan terminologi yang tepat) pendekatannya benar-benar lebih cepat, Anda hanya perlu membuka kotak nilai ketika sebuah objek jelas merupakanint
. Hal yang sama tidak bisa dikatakan menggunakanas
pendekatan.sumber
Untuk menjaga agar jawaban ini tetap mutakhir, perlu disebutkan bahwa sebagian besar diskusi di halaman ini sekarang dapat diperdebatkan sekarang dengan C # 7.1 dan .NET 4.7 yang mendukung sintaksis ramping yang juga menghasilkan kode IL terbaik.
Contoh asli OP ...
menjadi hanya ...
Saya telah menemukan bahwa salah satu penggunaan umum untuk sintaks baru adalah ketika Anda menulis tipe nilai .NET (yaitu
struct
dalam C # ) yang mengimplementasikanIEquatable<MyStruct>
(seperti kebanyakan seharusnya). Setelah menerapkan metode yang sangat diketikEquals(MyStruct other)
, kini Anda dapat dengan anggun mengarahkan ulangEquals(Object obj)
timpa yang tidak diketik (diwarisi dariObject
) ke metode tersebut sebagai berikut:Lampiran: Kode
Release
bangun IL untuk dua contoh fungsi pertama yang ditunjukkan di atas dalam jawaban ini (masing-masing) diberikan di sini. Sementara kode IL untuk sintaks baru memang 1 byte lebih kecil, sebagian besar menang besar dengan membuat nol panggilan (vs dua) dan menghindariunbox
operasi sama sekali bila memungkinkan.Untuk pengujian lebih lanjut yang memperkuat komentar saya tentang kinerja sintaks C # 7 baru yang melampaui opsi yang tersedia sebelumnya, lihat di sini (khususnya, contoh 'D').
sumber
Profiling lebih lanjut:
Keluaran:
Apa yang bisa kita simpulkan dari angka-angka ini?
sumber
Saya tidak punya waktu untuk mencobanya, tetapi Anda mungkin ingin:
sebagai
Anda membuat objek baru setiap kali, yang tidak akan sepenuhnya menjelaskan masalahnya, tetapi dapat berkontribusi.
sumber
int?
di stack menggunakanunbox.any
. Saya menduga itulah masalahnya - tebakan saya adalah bahwa IL buatan tangan bisa mengalahkan kedua opsi di sini ... meskipun juga mungkin bahwa JIT dioptimalkan untuk mengenali kasus is / cast dan hanya memeriksa sekali.Saya mencoba jenis cek konstruksi yang tepat
typeof(int) == item.GetType()
, yang berkinerja secepatitem is int
versi, dan selalu mengembalikan nomor (penekanan: bahkan jika Anda menulis aNullable<int>
ke array, Anda harus menggunakantypeof(int)
). Anda juga perlunull != item
cek tambahan di sini.Namun
typeof(int?) == item.GetType()
tetap cepat (berbeda denganitem is int?
), tetapi selalu mengembalikan false.Tipe-konstruksi di mata saya adalah cara tercepat untuk memeriksa tipe yang tepat , karena menggunakan RuntimeTypeHandle. Karena tipe yang tepat dalam kasus ini tidak cocok dengan nullable, tebakan saya adalah,
is/as
harus melakukan pengangkatan beban tambahan di sini untuk memastikan bahwa itu sebenarnya adalah instance dari tipe Nullable.Dan jujur: apa yang kamu
is Nullable<xxx> plus HasValue
beli? Tidak ada. Anda selalu bisa langsung ke tipe (nilai) yang mendasarinya (dalam hal ini). Anda mendapatkan nilai atau "tidak, bukan turunan dari tipe yang Anda minta". Bahkan jika Anda menulis(int?)null
ke array, jenis cek akan mengembalikan false.sumber
int?
- jika Anda memberiint?
nilai pada kotak, nilai itu berakhir dengan int kotak ataunull
referensi.Keluaran:
[EDIT: 2010-06-19]
Catatan: Tes sebelumnya dilakukan di dalam VS, konfigurasi debug, menggunakan VS2009, menggunakan Core i7 (mesin pengembangan perusahaan).
Berikut ini dilakukan pada mesin saya menggunakan Core 2 Duo, menggunakan VS2010
sumber