Apakah linq lebih efisien daripada yang terlihat di permukaan?

13

Jika saya menulis sesuatu seperti ini:

var things = mythings
    .Where(x => x.IsSomeValue)
    .Where(y => y.IsSomeOtherValue)

Apakah ini sama dengan:

var results1 = new List<Thing>();
foreach(var t in mythings)
    if(t.IsSomeValue)
        results1.Add(t);

var results2 = new List<Thing>();
foreach(var t in results1)
    if(t.IsSomeOtherValue)
        results2.Add(t);

Atau ada sihir di balik selimut yang berfungsi lebih seperti ini:

var results = new List<Thing>();
foreach(var t in mythings)
    if(t.IsSomeValue && t.IsSomeOtherValue)
        results.Add(t);

Atau apakah itu sesuatu yang sama sekali berbeda?

ConditionRacer
sumber
4
Anda dapat melihatnya di ILSpy.
ChaosPandion
1
Ini lebih seperti contoh kedua daripada yang pertama tetapi jawaban ChaosPandion kedua bahwa ILSpy adalah teman Anda.
Michael
2
Lihat juga Mengapa Di mana dan Pilih mengungguli Pilih saja?
BlueRaja - Danny Pflughoeft

Jawaban:

27

Pertanyaan LINQ malas . Itu berarti kode:

var things = mythings
    .Where(x => x.IsSomeValue)
    .Where(y => y.IsSomeOtherValue);

sangat sedikit. Enumerable asli ( mythings) hanya disebutkan ketika enumerable yang dihasilkan ( things) dikonsumsi, misalnya dengan foreachloop .ToList(),, atau .ToArray().

Jika Anda menelepon things.ToList(), ini kira-kira setara dengan kode terakhir Anda, dengan mungkin beberapa (biasanya tidak signifikan) overhead dari enumerator.

Demikian juga, jika Anda menggunakan loop foreach:

foreach (var t in things)
    DoSomething(t);

Ini serupa dalam kinerja dengan:

foreach (var t in mythings)
    if (t.IsSomeValue && t.IsSomeOtherValue)
        DoSomething(t);

Beberapa keuntungan kinerja dari pendekatan kemalasan untuk enumerables (sebagai lawan menghitung semua hasil dan menyimpannya dalam daftar) adalah bahwa ia menggunakan memori yang sangat sedikit (karena hanya satu hasil disimpan pada satu waktu) dan bahwa tidak ada peningkatan signifikan - biaya awal.

Jika enumerable hanya disebutkan sebagian, ini sangat penting. Pertimbangkan kode ini:

things.First();

Cara LINQ diimplementasikan, mythingshanya akan disebutkan hingga elemen pertama yang cocok dengan kondisi tempat Anda. Jika elemen tersebut berada di awal daftar, ini bisa menjadi peningkatan kinerja yang sangat besar (misalnya O (1), bukan O (n)).

Cyanfish
sumber
1
Satu perbedaan kinerja antara LINQ dan penggunaan kode setara foreachadalah bahwa LINQ menggunakan permintaan delegasi, yang memiliki beberapa overhead. Ini bisa menjadi signifikan ketika kondisi mengeksekusi sangat cepat (yang sering mereka lakukan).
svick
2
Itulah yang saya maksud dengan overhead enumerator. Ini bisa menjadi masalah dalam beberapa kasus (jarang), tetapi dalam pengalaman saya yang tidak terlalu sering - biasanya waktu yang dibutuhkan sangat kecil untuk memulai, atau jauh melebihi oleh operasi lain yang Anda lakukan.
Cyanfish
Keterbatasan evaluasi malas Linq adalah bahwa tidak ada cara untuk mengambil "snapshot" dari enumerasi kecuali melalui metode seperti ToListatau ToArray. Jika hal seperti itu telah dibangun dengan benar IEnumerable, akan mungkin untuk meminta daftar untuk "memotret" segala aspek yang mungkin berubah di masa depan tanpa harus menghasilkan semuanya.
supercat
7

Kode berikut:

var things = mythings
    .Where(x => x.IsSomeValue)
    .Where(y => y.IsSomeOtherValue);

Setara dengan tidak ada, karena evaluasi malas, tidak ada yang akan terjadi.

var things = mythings
    .Where(x => x.IsSomeValue)
    .Where(y => y.IsSomeOtherValue)
    .ToList();

Berbeda, karena evaluasi akan diluncurkan.

Setiap item mythingsakan diberikan kepada yang pertama Where. Jika lewat, itu akan diberikan kepada yang kedua Where. Jika lewat, itu akan menjadi bagian dari output.

Jadi ini terlihat seperti ini:

var results = new List<Thing>();
foreach(var t in mythings)
{
    if(t.IsSomeValue)
    {
        if(t.IsSomeOtherValue)
        {
            results.Add(t);
        }
    }
}
Cyril Gandon
sumber
7

Di samping eksekusi yang ditangguhkan (yang sudah dijelaskan oleh jawaban lain, saya hanya akan menunjukkan detail lain), lebih seperti pada contoh kedua Anda.

Mari kita bayangkan Anda menelepon ToListdi things.

Implementasi Enumerable.Wherepengembalian a Enumerable.WhereListIterator. Ketika Anda memanggil Whereitu WhereListIterator(alias chaining Where-calls), Anda tidak lagi menelepon Enumerable.Where, tetapi Enumerable.WhereListIterator.Where, yang sebenarnya menggabungkan predikat (menggunakan Enumerable.CombinePredicates).

Jadi lebih seperti if(t.IsSomeValue && t.IsSomeOtherValue).

kemalasan
sumber
"mengembalikan Enumerable.WhereListIterator" membuatnya klik untuk saya. Mungkin konsep yang sangat sederhana, tapi itulah yang saya hadapi dengan ILSpy. Terima kasih
ConditionRacer
Lihat implementasi ulang pengoptimalan Jon Skeet ini jika Anda lebih tertarik pada analisis mendalam.
Servy
1

Tidak itu tidak sama. Dalam contoh Anda thingsadalah IEnumerable, yang pada saat ini masih hanya iterator, bukan array atau daftar aktual. Apalagi karena thingstidak digunakan, loop bahkan tidak pernah dievaluasi. Jenis ini IEnumerablememungkinkan untuk beralih melalui elemen-elemen yieldoleh instruksi Linq dan memprosesnya lebih lanjut dengan lebih banyak instruksi, yang berarti pada akhirnya Anda hanya memiliki satu loop.

Tetapi segera setelah Anda menambahkan instruksi seperti .ToArray()atau .ToList(), Anda memesan pembuatan struktur data aktual, sehingga menempatkan batasan pada rantai Anda.

Lihat pertanyaan SO terkait ini: /programming/2789389/how-do-i-implement-ienumerable

Julien Guertault
sumber