Performa mengejutkan dengan tipe "as" dan nullable

330

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 isinstuntuk 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);
    }
}
Jon Skeet
sumber
8
Mengapa tidak melihat kode yang di-jet-kan? Bahkan VS debugger dapat menunjukkannya.
Anton Tykhyy
2
Saya hanya ingin tahu, apakah Anda menguji dengan CLR 4.0 juga?
Dirk Vollmar
1
@Anton: Poin bagus. Akan melakukan pada titik tertentu (meskipun ini tidak ada di VS saat ini :) @divo: Ya, dan itu lebih buruk lagi. Tapi kemudian itu dalam versi beta, jadi mungkin ada banyak kode debug di sana.
Jon Skeet
1
Hari ini saya belajar bahwa Anda dapat menggunakan asjenis nullable. Menarik, karena tidak dapat digunakan pada tipe nilai lainnya. Sebenarnya, lebih mengejutkan.
leppie
3
@ Lepp, sangat masuk akal untuk tidak bekerja pada tipe nilai. Pikirkan tentang hal ini, asupaya untuk melakukan cast ke suatu tipe dan jika gagal maka ia mengembalikan nol. Anda tidak dapat mengatur tipe nilai menjadi null
Earlz

Jawaban:

209

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.

Hans Passant
sumber
16
Oke, saya yakin. Saya kira saya terbiasa berpikir "adalah" sebagai berpotensi mahal karena kemungkinan berjalan hierarki warisan - tetapi dalam kasus tipe nilai, tidak ada kemungkinan hierarki, sehingga dapat menjadi perbandingan bitwise sederhana . Saya masih berpikir kode JIT untuk kasus nullable dapat dioptimalkan oleh JIT jauh lebih berat daripada itu.
Jon Skeet
26

Menurut saya itu isinsthanya sangat lambat pada jenis nullable. Dalam metode FindSumWithCastsaya berubah

if (o is int)

untuk

if (o is int?)

yang juga secara signifikan memperlambat eksekusi. Satu-satunya perbedaan dalam IL yang bisa saya lihat adalah itu

isinst     [mscorlib]System.Int32

diubah menjadi

isinst     valuetype [mscorlib]System.Nullable`1<int32>
Dirk Vollmar
sumber
1
Lebih dari itu; dalam kasus "pemain" yang isinstdiikuti oleh tes untuk pembatalan dan kemudian kondisional sebuah unbox.any. Dalam kasus nullable ada yang tanpa syarat unbox.any .
Jon Skeet
Ya, ternyata keduanya isinst dan unbox.anylebih lambat pada jenis nullable.
Dirk Vollmar
@ Jon: Anda dapat meninjau jawaban saya tentang mengapa para pemain diperlukan. (Saya tahu ini sudah tua, tetapi saya baru saja menemukan q ini dan berpikir saya harus memberikan 2c saya tentang apa yang saya ketahui tentang CLR).
Johannes Rudolph
22

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, asoperator C # akan memancarkan isinstinstruksi IL (begitu juga isoperator). (Instruksi lain yang menarik adalah castclass, dipancarkan ketika Anda melakukan cast langsung dan kompiler tahu bahwa pengecekan runtime tidak dapat dihentikan).

Inilah yang isinstdilakukan ( ECMA 335 Partition III, 4.6 ):

Format: isinst typeTok

typeTok adalah token metadata (a typeref, typedefatau typespec), yang menunjukkan kelas yang diinginkan.

Jika typeTok adalah tipe nilai yang tidak dapat dibatalkan atau tipe parameter generik, itu ditafsirkan sebagai typeTok "kotak" .

Jika typeTok adalah tipe nullable Nullable<T>, itu ditafsirkan sebagai "kotak"T

Yang paling penting:

Jika tipe aktual (bukan tipe verifier yang dilacak) dari obj adalah verifier-assignable-ke tipe typeTok kemudian isinstberhasil dan obj (sebagai hasilnya ) dikembalikan tidak berubah sementara verifikasi melacak tipenya sebagai typeTok .Tidak seperti paksaan (§1.6) dan konversi (§3.27), isinsttidak pernah mengubah tipe objek yang sebenarnya dan mempertahankan identitas objek (lihat Partisi I).

Jadi, pembunuh kinerja tidak isinstdalam kasus ini, tetapi tambahan unbox.any. Ini tidak jelas dari jawaban Hans, karena dia hanya melihat kode JIT. Secara umum, kompiler C # akan memancarkan unbox.anysetelah isinst T?(tetapi akan menghilangkannya jika Anda melakukannya isinst T, kapan Tadalah jenis referensi).

Kenapa bisa begitu? isinst T?tidak pernah memiliki efek yang jelas, yaitu Anda mendapatkan kembali T?. Alih-alih, semua instruksi ini memastikan bahwa Anda memiliki "boxed T"yang dapat dibuka kotaknya T?. Untuk mendapatkan yang sebenarnya T?, kita masih perlu unbox kami "boxed T"untuk T?, yang mengapa compiler memancarkan unbox.anysetelah isinst. Jika Anda memikirkannya, ini masuk akal karena "format kotak" untuk T?hanya "boxed T"dan membuat castclassdan isinstmelakukan unbox akan tidak konsisten.

Mencadangkan temuan Hans dengan beberapa informasi dari standar , ini dia:

(ECMA 335 Partisi III, 4.33): unbox.any

Ketika diterapkan pada bentuk kotak dari tipe nilai, unbox.anyinstruksi mengekstraksi nilai yang terkandung dalam obj (tipe O). (Ini sama dengan unboxdiikuti oleh ldobj.) Ketika diterapkan ke tipe referensi, theunbox.any instruksi memiliki efek yang sama dengan castclasstypeTok.

(ECMA 335 Partisi III, 4.32): unbox

Biasanya, unboxcukup hitung alamat tipe nilai yang sudah ada di dalam objek kotak. Pendekatan ini tidak mungkin dilakukan saat unboxing jenis nilai yang dapat dibatalkan. Karena Nullable<T>nilai dikonversi ke dalam kotak Tsselama operasi kotak, sebuah implementasi sering harus membuat yang baru Nullable<T>di heap dan menghitung alamat ke objek yang baru dialokasikan.

Johannes Rudolph
sumber
Saya pikir kalimat yang dikutip terakhir mungkin memiliki kesalahan ketik; bukankah seharusnya "... di heap ..." menjadi "di tumpukan eksekusi ?" Sepertinya membuka kotak kembali ke beberapa instance heap GC baru menukar masalah asli untuk yang baru hampir identik.
Glenn Slayden
19

Menariknya, saya menyampaikan umpan balik tentang dukungan operator melalui dynamicmenjadi urutan-of-magnitude lebih lambat untuk Nullable<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) nulluntuk struct yang tidak dapat dibatalkan, ia membuatnya untuk Nullable<T>:

using System;
using System.Diagnostics;
static class Program {
    static void Main() { 
        // JIT
        TestUnrestricted<int>(1,5);
        TestUnrestricted<string>("abc",5);
        TestUnrestricted<int?>(1,5);
        TestNullable<int>(1, 5);

        const int LOOP = 100000000;
        Console.WriteLine(TestUnrestricted<int>(1, LOOP));
        Console.WriteLine(TestUnrestricted<string>("abc", LOOP));
        Console.WriteLine(TestUnrestricted<int?>(1, LOOP));
        Console.WriteLine(TestNullable<int>(1, LOOP));

    }
    static long TestUnrestricted<T>(T x, int loop) {
        Stopwatch watch = Stopwatch.StartNew();
        int count = 0;
        for (int i = 0; i < loop; i++) {
            if (x != null) count++;
        }
        watch.Stop();
        return watch.ElapsedMilliseconds;
    }
    static long TestNullable<T>(T? x, int loop) where T : struct {
        Stopwatch watch = Stopwatch.StartNew();
        int count = 0;
        for (int i = 0; i < loop; i++) {
            if (x != null) count++;
        }
        watch.Stop();
        return watch.ElapsedMilliseconds;
    }
}
Marc Gravell
sumber
Yowser. Itu perbedaan yang sangat menyakitkan. Eek.
Jon Skeet
Jika tidak baik lainnya telah keluar dari semua ini, itu membuat saya menyertakan peringatan untuk kedua kode asli saya dan ini :)
Jon Skeet
Saya tahu ini adalah pertanyaan lama, tetapi bisakah Anda menjelaskan apa yang Anda maksud dengan "tempat JIT (dan menghapus) nulluntuk struct yang tidak dapat dibatalkan"? Apakah maksud Anda mengganti nulldengan nilai default atau sesuatu selama runtime?
Justin Morgan
2
@Justin - metode generik dapat digunakan saat runtime dengan sejumlah permutasi parameter-generik ( Tdll). 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.
Marc Gravell
Tes nullable yang tidak dibatasi masih lebih rendah 2,5 kali lipat, tetapi ada beberapa pengoptimalan yang terjadi ketika Anda tidak menggunakan countvariabel. Menambahkan Console.Write(count.ToString()+" ");setelah watch.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 ketika nulldilewatkan, mengkonfirmasikan kode asli tidak benar-benar melakukan pemeriksaan nol dan kenaikan untuk tes lainnya. Linqpad
Mark Hurd
12

Ini adalah hasil dari FindSumWithAsAndHas di atas: teks alternatif

Ini adalah hasil dari FindSumWithCast: teks alternatif

Temuan:

  • Menggunakan as, itu menguji terlebih dahulu jika suatu objek adalah instance dari Int32; di bawah tenda yang digunakan isinst Int32(yang mirip dengan kode tulisan tangan: if (o int)). Dan menggunakan as, itu juga tanpa syarat membuka kotak objek. Dan itu adalah pembunuh kinerja nyata untuk memanggil properti (itu masih fungsi di bawah tenda), IL_0027

  • Menggunakan gips, Anda menguji terlebih dahulu jika objek adalah int if (o is int); di bawah tenda ini menggunakan isinst Int32. Jika ini adalah instance dari int, maka Anda dapat dengan aman membuka kotak nilainya, IL_002D

Sederhananya, ini adalah pseudo-code menggunakan aspendekatan:

int? x;

(x.HasValue, x.Value) = (o isinst Int32, o unbox Int32)

if (x.HasValue)
    sum += x.Value;    

Dan ini adalah pseudo-code menggunakan pendekatan cast:

if (o isinst Int32)
    sum += (o unbox Int32)

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 merupakan int. Hal yang sama tidak bisa dikatakan menggunakan aspendekatan.

Michael Buen
sumber
11

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

object o = ...;
int? x = o as int?;
if (x.HasValue)
{
    // ...use x.Value in here
}

menjadi hanya ...

if (o is int x)
{
    // ...use x in here
}

Saya telah menemukan bahwa salah satu penggunaan umum untuk sintaks baru adalah ketika Anda menulis tipe nilai .NET (yaitu structdalam C # ) yang mengimplementasikan IEquatable<MyStruct>(seperti kebanyakan seharusnya). Setelah menerapkan metode yang sangat diketik Equals(MyStruct other), kini Anda dapat dengan anggun mengarahkan ulang Equals(Object obj)timpa yang tidak diketik (diwarisi dari Object) ke metode tersebut sebagai berikut:

public override bool Equals(Object obj) => obj is MyStruct o && Equals(o);

 


Lampiran: Kode Releasebangun 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 menghindari unboxoperasi sama sekali bila memungkinkan.

// static void test1(Object o, ref int y)
// {
//     int? x = o as int?;
//     if (x.HasValue)
//         y = x.Value;
// }

[0] valuetype [mscorlib]Nullable`1<int32> x
        ldarg.0
        isinst [mscorlib]Nullable`1<int32>
        unbox.any [mscorlib]Nullable`1<int32>
        stloc.0
        ldloca.s x
        call instance bool [mscorlib]Nullable`1<int32>::get_HasValue()
        brfalse.s L_001e
        ldarg.1
        ldloca.s x
        call instance !0 [mscorlib]Nullable`1<int32>::get_Value()
        stind.i4
L_001e: ret

// static void test2(Object o, ref int y)
// {
//     if (o is int x)
//         y = x;
// }

[0] int32 x,
[1] object obj2
        ldarg.0
        stloc.1
        ldloc.1
        isinst int32
        ldnull
        cgt.un
        dup
        brtrue.s L_0011
        ldc.i4.0
        br.s L_0017
L_0011: ldloc.1
        unbox.any int32
L_0017: stloc.0
        brfalse.s L_001d
        ldarg.1
        ldloc.0
        stind.i4
L_001d: ret

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

Glenn Slayden
sumber
9

Profiling lebih lanjut:

using System;
using System.Diagnostics;

class Program
{
    const int Size = 30000000;

    static void Main(string[] args)
    {
        object[] values = new object[Size];
        for (int i = 0; i < Size - 2; i += 3)
        {
            values[i] = null;
            values[i + 1] = "";
            values[i + 2] = 1;
        }

        FindSumWithIsThenCast(values);

        FindSumWithAsThenHasThenValue(values);
        FindSumWithAsThenHasThenCast(values);

        FindSumWithManualAs(values);
        FindSumWithAsThenManualHasThenValue(values);



        Console.ReadLine();
    }

    static void FindSumWithIsThenCast(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("Is then Cast: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithAsThenHasThenValue(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 then Has then Value: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithAsThenHasThenCast(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            int? x = o as int?;

            if (x.HasValue)
            {
                sum += (int)o;
            }
        }
        sw.Stop();
        Console.WriteLine("As then Has then Cast: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithManualAs(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            bool hasValue = o is int;
            int x = hasValue ? (int)o : 0;

            if (hasValue)
            {
                sum += x;
            }
        }
        sw.Stop();
        Console.WriteLine("Manual As: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithAsThenManualHasThenValue(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            int? x = o as int?;

            if (o is int)
            {
                sum += x.Value;
            }
        }
        sw.Stop();
        Console.WriteLine("As then Manual Has then Value: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

}

Keluaran:

Is then Cast: 10000000 : 303
As then Has then Value: 10000000 : 3524
As then Has then Cast: 10000000 : 3272
Manual As: 10000000 : 395
As then Manual Has then Value: 10000000 : 3282

Apa yang bisa kita simpulkan dari angka-angka ini?

  • Pertama, adalah-maka-cast pendekatan secara signifikan lebih cepat daripada sebagai pendekatan. 303 vs 3524
  • Kedua, .Nilai sedikit lebih lambat dari casting. 3524 vs 3272
  • Ketiga, .HasValue sedikit lebih lambat daripada menggunakan manual (yaitu menggunakan is ). 3524 vs 3282
  • Keempat, melakukan perbandingan apple-to-apple (yaitu menetapkan HasValue yang disimulasikan dan mengkonversi Nilai yang disimulasikan terjadi bersama-sama) antara simulasi sebagai dan nyata sebagai pendekatan, kita dapat melihat bahwa simulasi masih jauh lebih cepat daripada yang sebenarnya . 395 vs 3524
  • Terakhir, berdasarkan kesimpulan pertama dan keempat, ada sesuatu yang salah dengan sebagai implementasi ^ _ ^
Michael Buen
sumber
8

Saya tidak punya waktu untuk mencobanya, tetapi Anda mungkin ingin:

foreach (object o in values)
        {
            int? x = o as int?;

sebagai

int? x;
foreach (object o in values)
        {
            x = o as int?;

Anda membuat objek baru setiap kali, yang tidak akan sepenuhnya menjelaskan masalahnya, tetapi dapat berkontribusi.

James Black
sumber
1
Tidak, saya menjalankannya dan ini sedikit lebih lambat.
Henk Holterman
2
Mendeklarasikan suatu variabel di tempat yang berbeda hanya memengaruhi kode yang dihasilkan secara signifikan ketika variabel tersebut ditangkap (pada titik itu memengaruhi semantik aktual) dalam pengalaman saya. Perhatikan bahwa itu tidak membuat objek baru di heap, meskipun itu pasti membuat contoh baru int?di stack menggunakan unbox.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.
Jon Skeet
Saya berpikir bahwa para pemain mungkin dioptimalkan karena sudah ada begitu lama.
James Black
1
is / cast adalah target yang mudah untuk optimasi, itu adalah ungkapan yang sangat umum.
Anton Tykhyy
4
Variabel lokal dialokasikan pada stack ketika frame stack untuk metode ini dibuat, jadi di mana Anda mendeklarasikan variabel dalam metode tidak membuat perbedaan sama sekali. (Kecuali itu dalam penutupan tentu saja, tapi bukan itu yang terjadi di sini.)
Guffa
8

Saya mencoba jenis cek konstruksi yang tepat

typeof(int) == item.GetType(), yang berkinerja secepat item is intversi, dan selalu mengembalikan nomor (penekanan: bahkan jika Anda menulis a Nullable<int>ke array, Anda harus menggunakantypeof(int) ). Anda juga perlu null != itemcek tambahan di sini.

Namun

typeof(int?) == item.GetType()tetap cepat (berbeda dengan item 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 HasValuebeli? 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?)nullke array, jenis cek akan mengembalikan false.

dalo
sumber
Menarik ... ide menggunakan "sebagai" + HasValue (tidak adalah ditambah HasValue, catatan) adalah bahwa itu hanya melakukan cek tipe sekali bukan dua kali. Itu melakukan "centang dan hapus kotak" dalam satu langkah. Rasanya seperti itu harus lebih cepat ... tetapi jelas tidak. Saya tidak yakin apa yang Anda maksud dengan kalimat terakhir, tetapi tidak ada yang namanya kotak int?- jika Anda memberi int?nilai pada kotak, nilai itu berakhir dengan int kotak atau nullreferensi.
Jon Skeet
7
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);
        FindSumWithAsAndHas(values);
        FindSumWithAsAndIs(values);


        FindSumWithIsThenAs(values);
        FindSumWithIsThenConvert(values);

        FindSumWithLinq(values);



        Console.ReadLine();
    }

    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 FindSumWithAsAndHas(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 and Has: {0} : {1}", sum,
                          (long)sw.ElapsedMilliseconds);
    }


    static void FindSumWithAsAndIs(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            int? x = o as int?;
            if (o is int)
            {
                sum += x.Value;
            }
        }
        sw.Stop();
        Console.WriteLine("As and Is: {0} : {1}", sum,
                          (long)sw.ElapsedMilliseconds);
    }







    static void FindSumWithIsThenAs(object[] values)
    {
        // Apple-to-apple comparison with Cast routine above.
        // Using the similar steps in Cast routine above,
        // the AS here cannot be slower than Linq.



        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {

            if (o is int)
            {
                int? x = o as int?;
                sum += x.Value;
            }
        }
        sw.Stop();
        Console.WriteLine("Is then As: {0} : {1}", sum,
                          (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithIsThenConvert(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {            
            if (o is int)
            {
                int x = Convert.ToInt32(o);
                sum += x;
            }
        }
        sw.Stop();
        Console.WriteLine("Is then Convert: {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);
    }
}

Keluaran:

Cast: 10000000 : 456
As and Has: 10000000 : 2103
As and Is: 10000000 : 2029
Is then As: 10000000 : 1376
Is then Convert: 10000000 : 566
LINQ: 10000000 : 1811

[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

Inside VS, Configuration: Debug

Cast: 10000000 : 309
As and Has: 10000000 : 3322
As and Is: 10000000 : 3249
Is then As: 10000000 : 1926
Is then Convert: 10000000 : 410
LINQ: 10000000 : 2018




Outside VS, Configuration: Debug

Cast: 10000000 : 303
As and Has: 10000000 : 3314
As and Is: 10000000 : 3230
Is then As: 10000000 : 1942
Is then Convert: 10000000 : 418
LINQ: 10000000 : 1944




Inside VS, Configuration: Release

Cast: 10000000 : 305
As and Has: 10000000 : 3327
As and Is: 10000000 : 3265
Is then As: 10000000 : 1942
Is then Convert: 10000000 : 414
LINQ: 10000000 : 1932




Outside VS, Configuration: Release

Cast: 10000000 : 301
As and Has: 10000000 : 3274
As and Is: 10000000 : 3240
Is then As: 10000000 : 1904
Is then Convert: 10000000 : 414
LINQ: 10000000 : 1936
Michael Buen
sumber
Versi kerangka mana yang Anda gunakan, karena minat? Hasil di netbook saya (menggunakan .NET 4RC) bahkan lebih dramatis - versi menggunakan As jauh lebih buruk daripada hasil Anda. Mungkin mereka sudah memperbaikinya untuk .NET 4 RTM? Saya masih berpikir itu bisa lebih cepat ...
Jon Skeet
@Michael: Apakah Anda menjalankan bangunan yang tidak dioptimalkan, atau berjalan di debugger?
Jon Skeet
@ Jon: build tidak dioptimalkan, di bawah debugger
Michael Buen
1
@Michael: Benar - Saya cenderung melihat hasil kinerja di bawah debugger sebagai sangat tidak relevan :)
Jon Skeet
@ Jon: Jika di bawah debugger, artinya di dalam VS; ya patokan sebelumnya dilakukan di bawah debugger. Saya melakukan benchmark lagi, di dalam VS dan di luarnya, dan dikompilasi sebagai debug dan dikompilasi sebagai rilis. Periksa hasil edit
Michael Buen