Saya punya kelas, seperti ini:
public class MyClass
{
public int Value { get; set; }
public bool IsValid { get; set; }
}
Sebenarnya itu jauh lebih besar, tetapi ini menciptakan kembali masalah (keanehan).
Saya ingin mendapatkan jumlah dari Value
, di mana instance itu valid. Sejauh ini, saya telah menemukan dua solusi untuk ini.
Yang pertama adalah ini:
int result = myCollection.Where(mc => mc.IsValid).Select(mc => mc.Value).Sum();
Namun, yang kedua adalah ini:
int result = myCollection.Select(mc => mc.IsValid ? mc.Value : 0).Sum();
Saya ingin mendapatkan metode yang paling efisien. Saya, pada awalnya, berpikir bahwa yang kedua akan lebih efisien. Kemudian bagian teoretis saya mulai berjalan "Ya, satu adalah O (n + m + m), yang lain adalah O (n + n). Yang pertama harus bekerja lebih baik dengan lebih banyak cacat, sedangkan yang kedua harus berkinerja lebih baik dengan kurang ". Saya pikir mereka akan tampil setara. EDIT: Dan kemudian @Martin menunjukkan bahwa Di mana dan Pilih digabungkan, jadi itu seharusnya benar-benar O (m + n). Namun, jika Anda melihat di bawah, sepertinya ini tidak terkait.
Jadi saya mengujinya.
(Lebih dari 100 baris, jadi saya pikir lebih baik mempostingnya sebagai Intisari.)
Hasilnya ... menarik.
Dengan toleransi dasi 0%:
Timbangan ini mendukung Select
dan Where
, sekitar ~ 30 poin.
How much do you want to be the disambiguation percentage?
0
Starting benchmarking.
Ties: 0
Where + Select: 65
Select: 36
Dengan toleransi dasi 2%:
Itu sama, kecuali bahwa untuk beberapa mereka berada dalam 2%. Saya akan mengatakan itu margin kesalahan minimum. Select
dan Where
sekarang hanya memiliki ~ 20 poin.
How much do you want to be the disambiguation percentage?
2
Starting benchmarking.
Ties: 6
Where + Select: 58
Select: 37
Dengan toleransi dasi 5%:
Inilah yang saya katakan sebagai margin of error maksimum saya. Itu membuatnya sedikit lebih baik untuk Select
, tetapi tidak banyak.
How much do you want to be the disambiguation percentage?
5
Starting benchmarking.
Ties: 17
Where + Select: 53
Select: 31
Dengan toleransi dasi 10%:
Ini jalan keluar dari batas kesalahan saya, tetapi saya masih tertarik dengan hasilnya. Karena itu memberi Select
dan Where
dua puluh poin memimpin itu untuk sementara waktu sekarang.
How much do you want to be the disambiguation percentage?
10
Starting benchmarking.
Ties: 36
Where + Select: 44
Select: 21
Dengan toleransi dasi 25%:
Ini cara, jalan keluar dari batas kesalahan saya, tetapi saya masih tertarik dengan hasilnya, karena Select
dan Where
masih (hampir) mempertahankan keunggulan 20 poin mereka. Sepertinya itu mengungguli dalam beberapa yang berbeda, dan itulah yang memberinya keunggulan.
How much do you want to be the disambiguation percentage?
25
Starting benchmarking.
Ties: 85
Where + Select: 16
Select: 0
Sekarang, aku menebak bahwa memimpin 20 poin berasal dari tengah-tengah, di mana mereka berdua terikat untuk mendapatkan sekitar kinerja yang sama. Saya bisa mencoba dan mencatatnya, tetapi itu akan menjadi seluruh informasi untuk diterima. Grafik akan lebih baik, saya kira.
Jadi itulah yang saya lakukan.
Ini menunjukkan bahwa Select
garis tetap stabil (diharapkan) dan Select + Where
garis naik (diharapkan). Namun, apa yang membingungkan saya adalah mengapa itu tidak sesuai dengan Select
pada 50 atau lebih awal: sebenarnya saya mengharapkan lebih awal dari 50, karena pencacah ekstra harus dibuat untuk Select
dan Where
. Maksud saya, ini menunjukkan 20 poin memimpin, tetapi itu tidak menjelaskan mengapa. Ini, saya kira, adalah poin utama dari pertanyaan saya.
Kenapa berperilaku seperti ini? Haruskah saya mempercayainya? Jika tidak, haruskah saya menggunakan yang lain atau yang ini?
Seperti @KingKong disebutkan dalam komentar, Anda juga dapat menggunakan Sum
kelebihan yang membutuhkan lambda. Jadi dua opsi saya sekarang diubah menjadi ini:
Pertama:
int result = myCollection.Where(mc => mc.IsValid).Sum(mc => mc.Value);
Kedua:
int result = myCollection.Sum(mc => mc.IsValid ? mc.Value : 0);
Saya akan membuatnya sedikit lebih pendek, tetapi:
How much do you want to be the disambiguation percentage?
0
Starting benchmarking.
Ties: 0
Where: 60
Sum: 41
How much do you want to be the disambiguation percentage?
2
Starting benchmarking.
Ties: 8
Where: 55
Sum: 38
How much do you want to be the disambiguation percentage?
5
Starting benchmarking.
Ties: 21
Where: 49
Sum: 31
How much do you want to be the disambiguation percentage?
10
Starting benchmarking.
Ties: 39
Where: 41
Sum: 21
How much do you want to be the disambiguation percentage?
25
Starting benchmarking.
Ties: 85
Where: 16
Sum: 0
Keunggulan dua puluh poin masih ada, artinya itu tidak ada hubungannya dengan Where
dan Select
kombinasi yang ditunjukkan oleh @Marcin dalam komentar.
Terima kasih telah membaca dinding teks saya! Juga, jika Anda tertarik, ini versi modifikasi yang mencatat CSV yang digunakan Excel.
mc.Value
.Where
+Select
tidak menyebabkan dua iterasi terpisah pada pengumpulan input. LINQ untuk Objek mengoptimalkannya menjadi satu iterasi. Baca Lebih Lanjut di posting blogJawaban:
Select
iterates sekali atas seluruh set dan, untuk setiap item, melakukan cabang bersyarat (memeriksa validitas) dan+
operasi.Where+Select
menciptakan iterator yang melompati elemen yang tidak valid (bukankahyield
mereka), melakukan+
hanya pada item yang valid.Jadi, biaya untuk
Select
adalah:Dan untuk
Where+Select
:Dimana:
p(valid)
adalah probabilitas suatu item dalam daftar valid.cost(check valid)
adalah biaya cabang yang memeriksa validitascost(yield)
adalah biaya membangun keadaan baruwhere
iterator, yang lebih kompleks daripada iterator sederhana yangSelect
digunakan versi.Seperti yang Anda lihat, untuk yang diberikan
n
,Select
versi adalah konstanta, sedangkanWhere+Select
versi adalah persamaan linear denganp(valid)
sebagai variabel. Nilai aktual dari biaya menentukan titik persimpangan dari dua garis, dan karenacost(yield)
dapat berbeda daricost(+)
, mereka tidak harus berpotongan padap(valid)
= 0,5.sumber
cost(append)
? Jawaban yang benar-benar bagus, lihat dari sudut yang berbeda dan bukan hanya statistik.Where
tidak membuat apa-apa, hanya mengembalikan satu elemen pada saat itu darisource
urutan jika hanya memenuhi predikat.IEnumerable.Select(IEnumerable, Func)
danIQueryable.Select(IQueryable, Expression<Func>)
. Anda benar bahwa LINQ tidak melakukan "apa-apa" sampai Anda mengulangi koleksi tersebut, yang mungkin itulah yang Anda maksud.Berikut adalah penjelasan mendalam tentang apa yang menyebabkan perbedaan waktu.
The
Sum()
fungsi untukIEnumerable<int>
terlihat seperti ini:Dalam C #,
foreach
itu hanya gula sintaksis untuk versi iterator .Net, (jangan dikelirukan dengan ) . Jadi kode di atas sebenarnya diterjemahkan ke ini:IEnumerator<T>
IEnumerable<T>
Ingat, dua baris kode yang Anda bandingkan adalah sebagai berikut
Sekarang inilah kickernya:
LINQ menggunakan eksekusi yang ditangguhkan . Dengan demikian, sementara itu mungkin tampak bahwa
result1
iterates atas koleksi dua kali, itu sebenarnya hanya iterates atas sekali. TheWhere()
kondisi sebenarnya diterapkan selamaSum()
, dalam panggilan untukMoveNext()
(Hal ini dimungkinkan berkat keajaibanyield return
) .Ini berarti, untuk
result1
, kode di dalamwhile
loop,hanya dieksekusi satu kali untuk setiap item dengan
mc.IsValid == true
. Sebagai perbandingan,result2
akan menjalankan kode itu untuk setiap item dalam koleksi. Itu sebabnyaresult1
umumnya lebih cepat.(Meskipun, perhatikan bahwa memanggil
Where()
kondisi di dalamMoveNext()
masih memiliki beberapa overhead kecil, jadi jika sebagian besar / semua item memilikimc.IsValid == true
,result2
sebenarnya akan lebih cepat!)Semoga sekarang sudah jelas kenapa
result2
biasanya lebih lambat. Sekarang saya ingin menjelaskan mengapa saya menyatakan dalam komentar bahwa perbandingan kinerja LINQ ini tidak masalah .Membuat ekspresi LINQ itu murah. Memanggil fungsi delegasi itu murah. Mengalokasikan dan mengulangi iterator itu murah. Tetapi bahkan lebih murah untuk tidak melakukan hal-hal ini. Jadi, jika Anda menemukan bahwa pernyataan LINQ adalah hambatan dalam program Anda, menurut pengalaman saya menulis ulang tanpa LINQ akan selalu membuatnya lebih cepat daripada berbagai metode LINQ lainnya.
Jadi, alur kerja LINQ Anda akan terlihat seperti ini:
Untungnya, kemacetan LINQ jarang terjadi. Heck, bottleneck jarang terjadi. Saya telah menulis ratusan pernyataan LINQ dalam beberapa tahun terakhir, dan akhirnya mengganti <1%. Dan kebanyakan dari mereka adalah karena LINQ2EF optimasi SQL 's miskin, bukannya kesalahan LINQ ini.
Jadi, seperti biasa, tulis kode yang jelas dan masuk akal terlebih dahulu, dan tunggu sampai setelah Anda diprofilkan untuk mengkhawatirkan optimasi mikro.
sumber
Hal yang lucu. Apakah Anda tahu bagaimana
Sum(this IEnumerable<TSource> source, Func<TSource, int> selector)
didefinisikan? Itu menggunakanSelect
metode!Jadi sebenarnya, semuanya harus bekerja hampir sama. Saya melakukan riset cepat sendiri, dan berikut hasilnya:
Untuk implementasi berikut:
mod
berarti: setiap 1 darimod
item tidak valid: untukmod == 1
setiap item tidak valid, untukmod == 2
item aneh tidak valid, dll. Koleksi berisi10000000
item.Dan hasil untuk koleksi dengan
100000000
item:Seperti yang Anda lihat,
Select
danSum
hasilnya cukup konsisten di semuamod
nilai. Namunwhere
danwhere
+select
tidak.sumber
Dugaan saya adalah bahwa versi dengan Di mana menyaring 0s dan mereka bukan subjek untuk Sum (yaitu Anda tidak menjalankan penambahan). Ini tentu saja suatu dugaan karena saya tidak dapat menjelaskan bagaimana mengeksekusi ekspresi lambda tambahan dan memanggil beberapa metode mengungguli penambahan sederhana dari 0.
Seorang teman saya menyarankan bahwa fakta bahwa 0 dalam penjumlahan dapat menyebabkan penalti kinerja yang parah karena pemeriksaan melimpah. Akan menarik untuk melihat bagaimana ini akan tampil dalam konteks yang tidak dicentang.
sumber
unchecked
membuatnya menjadi, sangat kecil sedikit lebih baik untukSelect
.Menjalankan sampel berikut, menjadi jelas bagi saya bahwa satu-satunya waktu Di mana + Pilih dapat mengungguli Pilih sebenarnya ketika itu membuang jumlah yang baik (kira-kira setengah dalam tes informal saya) dari item potensial dalam daftar. Dalam contoh kecil di bawah ini, saya mendapatkan kira-kira angka yang sama dari kedua sampel ketika Di mana melompat kira-kira 4mil item dari 10mil. Saya berlari dalam rilis, dan memesan kembali pelaksanaan di mana + pilih vs pilih dengan hasil yang sama.
sumber
Select
?Jika Anda membutuhkan kecepatan, hanya melakukan perulangan lurus mungkin merupakan taruhan terbaik Anda. Dan melakukan
for
cenderung lebih baik daripadaforeach
(dengan asumsi koleksi Anda adalah akses acak tentu saja).Berikut adalah timing yang saya dapatkan dengan 10% elemen tidak valid:
Dan dengan 90% elemen tidak valid:
Dan ini kode tolok ukur saya ...
BTW, saya setuju dengan dugaan Stilgar : kecepatan relatif dari dua kasus Anda bervariasi tergantung pada persentase item yang tidak valid, hanya karena jumlah pekerjaan yang
Sum
perlu dilakukan bervariasi dalam kasus "Di mana".sumber
Daripada mencoba menjelaskan melalui deskripsi, saya akan mengambil pendekatan yang lebih matematis.
Dengan kode di bawah ini yang seharusnya mendekati apa yang dilakukan LINQ secara internal, biaya relatifnya adalah sebagai berikut:
Pilih saja:
Nd + Na
Di mana + Pilih:
Nd + Md + Ma
Untuk mengetahui titik di mana mereka akan menyeberang, kita perlu melakukan aljabar kecil:
Nd + Md + Ma = Nd + Na => M(d + a) = Na => (M/N) = a/(d+a)
Ini artinya bahwa agar titik belok mencapai 50%, biaya pemanggilan seorang delegasi harus kira-kira sama dengan biaya penambahan. Karena kita tahu bahwa titik belok yang sebenarnya adalah sekitar 60%, kita dapat bekerja mundur dan menentukan bahwa biaya permohonan delegasi untuk @ It'sNotALie sebenarnya sekitar 2/3 biaya tambahan yang mengejutkan, tapi itulah yang angka-angkanya mengatakan.
sumber
Saya pikir ini menarik bahwa hasil MarcinJuraszek berbeda dari It'sNotALie. Secara khusus, hasil MarcinJuraszek dimulai dengan keempat implementasi di tempat yang sama, sementara hasil It'sNotALie melintasi tengah. Saya akan menjelaskan bagaimana ini bekerja dari sumbernya.
Mari kita asumsikan ada
n
elemen total, danm
elemen valid.The
Sum
Fungsi ini cukup sederhana. Itu hanya loop melalui enumerator: http://typedescriptor.net/browse/members/367300-System.Linq.Enumerable.Sum(IEnumerable%601)Untuk kesederhanaan, anggap koleksi tersebut adalah daftar. Kedua Pilih dan WhereSelect akan membuat
WhereSelectListIterator
. Ini berarti iterator aktual yang dihasilkan adalah sama. Dalam kedua kasus, adaSum
yang loop di atas iterator, theWhereSelectListIterator
. Bagian yang paling menarik dari iterator adalah metode MoveNext .Karena iteratornya sama, loopnya sama. Satu-satunya perbedaan adalah di tubuh loop.
Tubuh lambda ini memiliki biaya yang sangat mirip. Klausa mana mengembalikan nilai bidang, dan predikat ternary juga mengembalikan nilai bidang. Klausa pilih mengembalikan nilai bidang, dan dua cabang operator ternary mengembalikan nilai bidang atau konstanta. Klausa pilih gabungan memiliki cabang sebagai operator ternary, tetapi WhereSelect menggunakan cabang di
MoveNext
.Namun, semua operasi ini cukup murah. Operasi yang paling mahal sejauh ini adalah cabang, di mana prediksi yang salah akan merugikan kita.
Operasi mahal lainnya di sini adalah
Invoke
. Meminta fungsi membutuhkan waktu lebih lama daripada menambahkan nilai, seperti yang ditunjukkan oleh Branko Dimitrijevic.Juga menimbang adalah akumulasi diperiksa di
Sum
. Jika prosesor tidak memiliki flag overflow aritmatika, maka ini bisa mahal untuk diperiksa juga.Karenanya, biaya yang menarik adalah: adalah:
n
+m
) * Aktifkan +m
*checked+=
n
* Ajukan +n
*checked+=
Jadi, jika biaya Invoke jauh lebih tinggi daripada biaya akumulasi yang diperiksa, maka kasing 2 selalu lebih baik. Jika mereka tentang genap, maka kita akan melihat keseimbangan ketika sekitar setengah dari elemen valid.
Sepertinya pada sistem MarcinJuraszek, dicentang + = memiliki biaya yang dapat diabaikan, tetapi pada sistem It'sNotALie dan Branko Dimitrijevic, dicentang + = memiliki biaya yang signifikan. Sepertinya itu yang paling mahal pada sistem It'sNotALie karena titik impas jauh lebih tinggi. Sepertinya tidak ada yang memposting hasil dari sistem di mana akumulasi biayanya jauh lebih tinggi daripada Invoke.
sumber