Apakah LINQ membutuhkan siklus pemrosesan dan memori yang jauh lebih banyak daripada teknik iterasi data tingkat rendah?

8

Latar Belakang

Saya baru-baru ini dalam proses bertahan wawancara teknologi melelahkan untuk posisi yang menggunakan .NET stack, beberapa di antaranya termasuk pertanyaan konyol seperti ini , dan beberapa pertanyaan yang lebih valid. Saya baru-baru ini menemukan masalah yang mungkin valid tetapi saya ingin memastikan dengan komunitas di sini.

Ketika ditanya oleh pewawancara bagaimana saya akan menghitung frekuensi kata-kata dalam dokumen teks dan memberi peringkat hasilnya, saya menjawab bahwa saya akan menghitungnya

  1. Gunakan objek aliran, letakkan file teks dalam memori sebagai string.
  2. Pisahkan string menjadi sebuah array pada spasi sambil mengabaikan tanda baca.
  3. Gunakan LINQ terhadap array ke .GroupBy()dan .Count(), lalu OrderBy()ucapkan hitungan.

Saya salah menjawab karena dua alasan:

  1. Streaming seluruh file teks ke dalam memori bisa menjadi bencana. Bagaimana jika itu seluruh ensiklopedia? Alih-alih, saya harus mengalirkan satu blok pada satu waktu dan mulai membangun tabel hash.
  2. LINQ terlalu mahal dan membutuhkan terlalu banyak siklus pemrosesan. Saya seharusnya membangun tabel hash dan, untuk setiap iterasi, hanya menambahkan kata ke tabel hash jika tidak ada dan kemudian menambah itu dihitung.

Alasan pertama tampaknya, well, masuk akal. Tapi yang kedua memberi saya lebih banyak jeda. Saya berpikir bahwa salah satu nilai jual LINQ adalah bahwa ia hanya mengabstraksi operasi tingkat rendah seperti tabel hash tetapi bahwa, di bawah tabir, masih implementasi yang sama.

Pertanyaan

Selain dari beberapa siklus pemrosesan tambahan untuk memanggil metode abstrak apa pun, apakah LINQ membutuhkan siklus pemrosesan yang lebih signifikan untuk menyelesaikan tugas iterasi data yang diberikan daripada tugas tingkat rendah (seperti membuat tabel hash)?

Matt Cashatt
sumber
2
Tanyakan padanya, tolol apa yang meletakkan seluruh ensiklopedia dalam satu file teks?
JeffO
4
Itu salah satu hal yang harus diukur. Bangun 2 atau 3 implementasi dan catat kinerja. Generalisasi tentang LINQ atau teknik X tidak berguna. Saya akan mengatakan bahwa pewawancara yang buruk menyatakan bahwa menggunakan LINQ sebagai jawaban "salah". Meskipun dalam pemrosesan sisi server beban berat setiap milidetik penting.
Lord Tydus
1
Google cepat untuk "pengujian linq kinerja untuk objek dan loop" menemukan beberapa hit. Beberapa menyertakan kode sumber yang dapat Anda gunakan untuk menguji sendiri. Lihat ini , ini dan ini .
Oded
1
Sedangkan untuk wawancara, ingatlah ada beberapa programer C ++ "old school" yang berpikir Anda harus menemukan kembali roda daripada menggunakan pustaka .NET. Anda juga akan mengalami VBer sekolah lama yang ingin melakukan semua kode akses data dengan tangan daripada menggunakan LINQ dan EF.
jfrankcarr
1
Oded, contoh-contoh di tautan yang Anda berikan sangat salah. Saya tidak bisa masuk ke semua detail dalam komentar, tetapi ambil tautan kedua. Ini membandingkan "foreach x jika x = toFind stop" dengan kueri linq yang sama dengan "select * from list di mana x suka toFind" Perbedaannya adalah yang pertama berhenti ketika menemukan instance pertama, permintaan linq selalu beralih lebih dari setiap entri dan akan mengembalikan koleksi SEMUA item yang cocok dengan pola pencarian. Sangat berbeda. Itu bukan karena LINQ rusak, itu karena dia menggunakan kueri yang salah.
Ian

Jawaban:

9

Saya akan mengatakan kelemahan utama dari jawaban ini adalah kurang penggunaan Linq dan lebih banyak operator tertentu yang dipilih. GroupBymengambil setiap elemen dan memproyeksikannya ke kunci dan nilai yang masuk ke pencarian. Dengan kata lain, setiap kata akan menambahkan sesuatu ke pencarian.

Implementasi naif .GroupBy(e => e)akan menyimpan salinan setiap kata dalam materi sumber, membuat pencarian terakhir hampir sebesar bahan sumber asli. Bahkan jika kami memproyeksikan nilainya dengan .GroupBy(e => e, e => null)kami menciptakan pencarian besar nilai-nilai kecil.

Yang kami inginkan adalah operator yang hanya menyimpan informasi yang diperlukan, yaitu satu salinan dari setiap kata dan jumlah kata itu sejauh ini. Untuk itu, kita bisa menggunakan Aggregate:

words.Aggregate(new Dictionary<string, int>(), (counts, word) => 
{
    int currentCount;
    counts.TryGetValue(word, currentCount);
    counts[word] = currentCount + 1;
    return counts;
} 

Dari sini, ada beberapa cara yang dapat kami lakukan untuk mempercepat ini:

  1. Alih-alih membuat banyak string saat memisahkan, kita bisa mengedarkan struct yang mereferensikan string asli dan segmen yang berisi kata, dan hanya menyalin segmen ketika ternyata menjadi kunci unik
  2. Gunakan Parallel Linq untuk menggabungkan beberapa core kemudian menggabungkan hasilnya . Ini sepele dibandingkan dengan pekerjaan kaki yang diperlukan untuk melakukan ini dengan tangan.
Chris Pitman
sumber
Semua poin bagus, Chris, terima kasih. Saya akan menahan diri untuk tidak menerima sedikit pun karena pertanyaannya lebih umum dan pada dasarnya dijawab oleh Oded dalam komentar di atas. Saya hanya ingin memberinya kesempatan untuk memberikan jawaban terlebih dahulu. Terima kasih sekali lagi atas wawasan Anda, yang sangat bagus.
Matt Cashatt
6

Saya pikir Anda memiliki jalan keluar yang sempit, pewawancara tidak benar-benar tahu apa yang dia bicarakan. Lebih buruk lagi, dia percaya ada jawaban yang "benar". Jika dia adalah seseorang yang Anda ingin bekerja, saya berharap dia mengambil jawaban awal Anda, mencari tahu mengapa Anda memilihnya, dan kemudian menantang Anda untuk membuatnya lebih baik jika dia dapat menemukan masalah dengannya.

LINQ membuat orang takut karena terlihat seperti sulap. Ini sebenarnya sangat sederhana (Begitu sederhana, Anda harus menjadi jenius untuk membuatnya)

var result = from item in collection where item=>item.Property > 3 select item;

Dikompilasi menjadi:

IEnumerable<itemType> result = collection.Where(item=>item.property >3);

(Tolong jangan berteriak jika aku salah sintaks, sudah lewat tengah malam dan aku di tempat tidur :))

Di mana ada metode ekstensi pada IEnumerable yang mengambil lambda. Lambda hanya (dalam hal ini) dikompilasi untuk delegasi:

bool AMethod(ItemType item)
{
    return item.property >3;
}

Metode Where hanya menambahkan SEMUA instance item di mana AMethod mengembalikan true ke koleksi yang dikembalikan.

Tidak ada alasan yang lebih lambat daripada melakukan foreach dan menambahkan semua item yang cocok ke koleksi di loop itu. Bahkan metode ekstensi Where mungkin melakukan hal itu. Keajaiban nyata datang ketika Anda harus menyuntikkan kriteria alternatif di mana.

Seperti yang saya sebutkan di atas dalam komentar saya, beberapa contoh terkait sangat salah. Dan informasi yang salah inilah yang menyebabkan masalah.

Akhirnya, jika wawancara memberi Anda kesempatan, Anda bisa mengatakan bahwa:

  • LINQ mudah dibaca, terutama ketika Anda mulai memperkenalkan proyeksi dan pengelompokan yang menarik. Kode yang mudah dibaca adalah mudah [y | ier] untuk mempertahankan kode yang merupakan kemenangan.

  • Akan sangat mudah untuk mengukur kinerja jika itu benar-benar hambatan dan menggantinya dengan yang lain.

Ian
sumber
Secara keseluruhan saya setuju dengan Anda tetapi perilaku metode Where - Where method tidak menambahkan semua item yang cocok ke koleksi. Ini menyimpan informasi yang diperlukan untuk memfilter item di pohon ekspresi. Jika iterator yang dikembalikan tidak benar-benar digunakan, penyaringan tidak akan terjadi sama sekali.
Codism
Poin yang luar biasa, saya seharusnya menyebutkan itu. Dalam contoh mereka, mereka menggunakan iterator yang dikembalikan. Ini adalah kegilaan dari ujian mereka. Untuk mengekstraksi nilai yang ditemukan (semua item dalam data uji mereka unik) mereka memiliki pendahuluan yang mengulangi hasil yang dapat dihitung untuk menampilkan hasilnya. Tentu saja hanya ada satu hasil sehingga hanya dicetak satu jawaban. Kegilaan :)
Ian
Walaupun saya belum pernah menggunakan LINQ, satu hal yang saya rasa tidak menyenangkan adalah mengoptimalkan hal-hal seperti Countuntuk beberapa skenario sempit yang bekerja buruk dengan enkapsulasi. Menggabungkan daftar sejuta item dan iterator empat item, dan Countharus membutuhkan sekitar 5 operasi, tetapi sebaliknya akan membutuhkan satu juta. Saya berharap MS akan mendefinisikan IEnhancedEnumeratordengan int Move(int)metode yang akan mengembalikan 0 jika sukses, atau mengembalikan jumlah kekurangan jika terjadi kegagalan (jadi melakukan Move(1000003)pada yang baru dibuat List<T>.Enumeratordari daftar jutaan item akan mengembalikan 3). Implementasi apa pun ...
supercat
... dari IEnumerable<T>dapat dibungkus dalam implementasi IEnhancedEnumerator, tetapi jenis yang mengimplementasikan IEnhancedEnumeratorsecara langsung dapat memungkinkan peningkatan pesanan yang sangat besar pada banyak operasi, dan bahkan hal-hal seperti pengembalian dari Appenddapat memaparkan kemampuan pencarian konstituen yang cepat dicari.
supercat