Membaca file teks besar dengan aliran di C #

96

Saya mendapat tugas yang bagus untuk mengetahui cara menangani file besar yang dimuat ke editor skrip aplikasi kami (ini seperti VBA untuk produk internal kami untuk makro cepat). Sebagian besar file berukuran sekitar 300-400 KB yang dapat dimuat dengan baik. Tetapi ketika mereka melampaui 100 MB, prosesnya mengalami kesulitan (seperti yang Anda harapkan).

Apa yang terjadi adalah bahwa file tersebut dibaca dan dimasukkan ke dalam RichTextBox yang kemudian dinavigasi - jangan terlalu khawatir tentang bagian ini.

Pengembang yang menulis kode awal hanya menggunakan StreamReader dan melakukan

[Reader].ReadToEnd()

yang bisa memakan waktu cukup lama untuk menyelesaikannya.

Tugas saya adalah memecah sedikit kode ini, membacanya dalam potongan menjadi buffer dan menampilkan bilah kemajuan dengan opsi untuk membatalkannya.

Beberapa asumsi:

  • Kebanyakan file berukuran 30-40 MB
  • Isi filenya adalah teks (bukan biner), ada yang berformat Unix, ada pula yang DOS.
  • Setelah konten diambil, kami mencari tahu terminator apa yang digunakan.
  • Tidak ada yang peduli setelah dimuat tentang waktu yang diperlukan untuk merender di kotak teks kaya. Ini hanya pemuatan awal teks.

Sekarang untuk pertanyaannya:

  • Bisakah saya menggunakan StreamReader, lalu memeriksa properti Length (jadi ProgressMax) dan mengeluarkan Read untuk ukuran buffer yang ditetapkan dan mengulang-ulang sementara WHILST di dalam pekerja latar belakang, sehingga tidak memblokir thread UI utama? Kemudian kembalikan pembuat string ke utas utama setelah selesai.
  • Isinya akan dikirim ke StringBuilder. dapatkah saya menginisialisasi StringBuilder dengan ukuran aliran jika panjangnya tersedia?

Apakah ini (menurut pendapat profesional Anda) ide bagus? Saya pernah mengalami beberapa masalah di masa lalu dengan membaca konten dari Streams, karena akan selalu melewatkan beberapa byte terakhir atau sesuatu, tetapi saya akan mengajukan pertanyaan lain jika ini masalahnya.

Nicole Lee
sumber
29
30-40MB file skrip? Makarel suci! Aku benci harus meninjau kode bahwa ...
dthorpe
Saya tahu pertanyaan ini agak lama tetapi saya menemukannya beberapa hari yang lalu dan telah menguji rekomendasi untuk MemoryMappedFile dan ini adalah metode tercepat. Perbandingan membaca file 7.616.939 baris 345MB melalui metode readline membutuhkan 12+ jam di mesin saya saat melakukan beban yang sama dan membaca melalui MemoryMappedFile membutuhkan waktu 3 detik.
csonon
Itu hanya beberapa baris kode. Lihat perpustakaan ini yang saya gunakan untuk membaca file berukuran 25gb dan lebih besar juga. github.com/Agenty/FileReader
Vikash Rathee

Jawaban:

175

Anda dapat meningkatkan kecepatan baca dengan menggunakan BufferedStream, seperti ini:

using (FileStream fs = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
using (BufferedStream bs = new BufferedStream(fs))
using (StreamReader sr = new StreamReader(bs))
{
    string line;
    while ((line = sr.ReadLine()) != null)
    {

    }
}

PEMBARUAN Maret 2013

Saya baru-baru ini menulis kode untuk membaca dan memproses (mencari teks dalam) file teks 1 GB-ish (jauh lebih besar daripada file yang terlibat di sini) dan mencapai peningkatan kinerja yang signifikan dengan menggunakan pola produsen / konsumen. Tugas produser membaca baris teks menggunakan the BufferedStreamdan menyerahkannya ke tugas konsumen terpisah yang melakukan pencarian.

Saya menggunakan ini sebagai kesempatan untuk mempelajari TPL Dataflow, yang sangat cocok untuk mengkodekan pola ini dengan cepat.

Mengapa BufferedStream lebih cepat

Buffer adalah blok byte dalam memori yang digunakan untuk menyimpan data dalam cache, sehingga mengurangi jumlah panggilan ke sistem operasi. Buffer meningkatkan kinerja baca dan tulis. Buffer dapat digunakan untuk membaca atau menulis, tetapi tidak pernah keduanya secara bersamaan. Metode Baca dan Tulis BufferedStream secara otomatis mempertahankan buffer.

PEMBARUAN Desember 2014: Jarak Tempuh Anda Dapat Bervariasi

Berdasarkan komentar, FileStream harus menggunakan BufferedStream secara internal. Pada saat jawaban ini pertama kali diberikan, saya mengukur peningkatan kinerja yang signifikan dengan menambahkan BufferedStream. Saat itu saya menargetkan .NET 3.x pada platform 32-bit. Hari ini, menargetkan .NET 4.5 pada platform 64-bit, saya tidak melihat adanya peningkatan.

Terkait

Saya menemukan kasus di mana streaming file CSV yang besar dan dihasilkan ke aliran Respons dari tindakan ASP.Net MVC sangat lambat. Menambahkan BufferedStream meningkatkan kinerja sebesar 100x dalam contoh ini. Untuk lebih lanjut, lihat Output Tidak Disangga Sangat Lambat

Eric J.
sumber
12
Bung, BufferedStream membuat semua perbedaan. +1 :)
Marcus
2
Ada biaya untuk meminta data dari subsistem IO. Dalam kasus disk yang berputar, Anda mungkin harus menunggu piring berputar ke posisinya untuk membaca potongan data berikutnya, atau lebih buruk lagi, menunggu kepala disk bergerak. Meskipun SSD tidak memiliki bagian mekanis untuk memperlambat segalanya, masih ada biaya per-IO-operasi untuk mengaksesnya. Aliran yang disangga membaca lebih dari sekadar apa yang diminta StreamReader, mengurangi jumlah panggilan ke OS dan pada akhirnya jumlah permintaan IO terpisah.
Eric J.
4
Betulkah? Ini tidak membuat perbedaan dalam skenario pengujian saya. Menurut Brad Abrams, tidak ada manfaat menggunakan BufferedStream melalui FileStream.
Nick Cox
2
@NickCox: Hasil Anda dapat bervariasi berdasarkan subsistem IO yang mendasarinya. Pada disk yang berputar dan pengontrol disk yang tidak memiliki data dalam cache-nya (dan juga data yang tidak di-cache oleh Windows), kecepatannya sangat besar. Kolom Brad ditulis pada tahun 2004. Saya mengukur peningkatan yang nyata dan drastis baru-baru ini.
Eric J.
3
Ini tidak berguna menurut: stackoverflow.com/questions/492283/… FileStream sudah menggunakan buffer secara internal.
Erwin Mayer
21

Jika Anda membaca statistik kinerja dan tolok ukur di situs web ini , Anda akan melihat bahwa cara tercepat untuk membaca (karena membaca, menulis, dan memproses semuanya berbeda) file teks adalah cuplikan kode berikut:

using (StreamReader sr = File.OpenText(fileName))
{
    string s = String.Empty;
    while ((s = sr.ReadLine()) != null)
    {
        //do your stuff here
    }
}

Semuanya sekitar 9 metode yang berbeda telah ditandai, tetapi yang satu tampaknya keluar di sebagian besar waktu, bahkan keluar melakukan buffered reader seperti yang disebutkan oleh pembaca lain.


sumber
2
Ini bekerja dengan baik untuk memisahkan file postgres 19GB untuk menerjemahkannya ke dalam sintaks sql dalam banyak file. Terima kasih postgres guy yang tidak pernah mengeksekusi parameter saya dengan benar. / menghela napas
Damon Drake
Perbedaan kinerja di sini tampaknya membayar untuk file yang sangat besar, seperti lebih besar dari 150MB (juga Anda benar-benar harus menggunakan a StringBuilderuntuk memuatnya ke memori, memuat lebih cepat karena tidak membuat string baru setiap kali Anda menambahkan karakter)
Joshua G
15

Anda mengatakan Anda telah diminta untuk menunjukkan bilah kemajuan saat file besar sedang dimuat. Apakah itu karena pengguna benar-benar ingin melihat% pemuatan file yang tepat, atau hanya karena mereka menginginkan umpan balik visual bahwa ada sesuatu yang terjadi?

Jika yang terakhir benar, maka solusinya menjadi lebih sederhana. Lakukan saja reader.ReadToEnd()pada utas latar belakang, dan tampilkan bilah kemajuan tipe marquee alih-alih bilah yang tepat.

Saya mengangkat poin ini karena menurut pengalaman saya hal ini sering terjadi. Saat Anda menulis program pemrosesan data, maka pengguna pasti akan tertarik dengan angka% lengkap, tetapi untuk pembaruan UI yang sederhana namun lambat, mereka lebih cenderung hanya ingin tahu bahwa komputer tidak macet. :-)

Christian Hayter
sumber
2
Tetapi dapatkah pengguna membatalkan panggilan ReadToEnd?
Tim Scarborough
@Tim, terlihat bagus. Dalam hal ini, kita kembali ke StreamReaderloop. Namun, ini tetap akan lebih sederhana karena tidak perlu membaca dulu untuk menghitung indikator kemajuan.
Christian Hayter
8

Untuk file biner, cara tercepat untuk membacanya yang saya temukan adalah ini.

 MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(file);
 MemoryMappedViewStream mms = mmf.CreateViewStream();
 using (BinaryReader b = new BinaryReader(mms))
 {
 }

Dalam pengujian saya, ini ratusan kali lebih cepat.

StainlessBeer
sumber
2
Apakah Anda punya bukti kuat tentang ini? Mengapa OP menggunakan ini di atas jawaban lain? Tolong gali lebih dalam dan berikan sedikit lebih banyak detail
Dylan Corriveau
7

Gunakan pekerja latar belakang dan baca baris dalam jumlah terbatas. Baca lebih lanjut hanya saat pengguna menggulir.

Dan cobalah untuk tidak pernah menggunakan ReadToEnd (). Itu salah satu fungsi yang menurut Anda "mengapa mereka membuatnya?"; itu adalah pembantu script kiddies yang bekerja dengan baik dengan hal-hal kecil, tetapi seperti yang Anda lihat, itu menyebalkan untuk file besar ...

Orang-orang yang memberi tahu Anda untuk menggunakan StringBuilder perlu membaca MSDN lebih sering:

Pertimbangan Performa
Metode Concat dan AppendFormat menggabungkan data baru ke objek String atau StringBuilder yang sudah ada. Operasi penggabungan objek String selalu membuat objek baru dari string yang ada dan data baru. Objek StringBuilder memelihara buffer untuk mengakomodasi penggabungan data baru. Data baru ditambahkan ke ujung buffer jika ruang tersedia; jika tidak, buffer baru yang lebih besar dialokasikan, data dari buffer asli disalin ke buffer baru, lalu data baru ditambahkan ke buffer baru. Kinerja operasi penggabungan untuk objek String atau StringBuilder bergantung pada seberapa sering alokasi memori terjadi.
Operasi penggabungan String selalu mengalokasikan memori, sedangkan operasi penggabungan StringBuilder hanya mengalokasikan memori jika buffer objek StringBuilder terlalu kecil untuk menampung data baru. Akibatnya, kelas String lebih disukai untuk operasi penggabungan jika sejumlah objek String digabungkan. Dalam hal ini, operasi penggabungan individu bahkan dapat digabungkan menjadi satu operasi oleh kompilator. Objek StringBuilder lebih disukai untuk operasi penggabungan jika sejumlah string digabungkan; misalnya, jika sebuah loop menggabungkan sejumlah string input pengguna secara acak.

Itu berarti besar alokasi memori, apa yang menjadi besar penggunaan sistem file swap, yang mensimulasikan bagian dari hard disk drive Anda untuk bertindak seperti memori RAM, tapi hard disk drive sangat lambat.

Opsi StringBuilder terlihat bagus untuk siapa yang menggunakan sistem sebagai pengguna tunggal, tetapi ketika Anda memiliki dua atau lebih pengguna yang membaca file besar pada saat yang sama, Anda mengalami masalah.

Tufo
sumber
jauh kalian super cepat! sayangnya karena cara kerja makro, seluruh aliran perlu dimuat. Seperti yang saya sebutkan jangan khawatir tentang bagian richtext. Ini pemuatan awal yang ingin kami tingkatkan.
Nicole Lee
sehingga Anda dapat bekerja dalam beberapa bagian, membaca baris X pertama, menerapkan makro, membaca baris X kedua, menerapkan makro, dan seterusnya ... jika Anda menjelaskan apa yang dilakukan makro ini, kami dapat membantu Anda dengan lebih presisi
Tufo
5

Ini seharusnya cukup untuk membantu Anda memulai.

class Program
{        
    static void Main(String[] args)
    {
        const int bufferSize = 1024;

        var sb = new StringBuilder();
        var buffer = new Char[bufferSize];
        var length = 0L;
        var totalRead = 0L;
        var count = bufferSize; 

        using (var sr = new StreamReader(@"C:\Temp\file.txt"))
        {
            length = sr.BaseStream.Length;               
            while (count > 0)
            {                    
                count = sr.Read(buffer, 0, bufferSize);
                sb.Append(buffer, 0, count);
                totalRead += count;
            }                
        }

        Console.ReadKey();
    }
}
ChaosPandion
sumber
4
Saya akan memindahkan "var buffer = new char [1024]" keluar dari loop: tidak perlu membuat buffer baru setiap kali. Letakkan saja sebelum "sementara (hitung> 0)".
Tommy Carlier
4

Lihat cuplikan kode berikut. Anda telah menyebutkan Most files will be 30-40 MB. Ini mengklaim membaca 180 MB dalam 1,4 detik pada Intel Quad Core:

private int _bufferSize = 16384;

private void ReadFile(string filename)
{
    StringBuilder stringBuilder = new StringBuilder();
    FileStream fileStream = new FileStream(filename, FileMode.Open, FileAccess.Read);

    using (StreamReader streamReader = new StreamReader(fileStream))
    {
        char[] fileContents = new char[_bufferSize];
        int charsRead = streamReader.Read(fileContents, 0, _bufferSize);

        // Can't do much with 0 bytes
        if (charsRead == 0)
            throw new Exception("File is 0 bytes");

        while (charsRead > 0)
        {
            stringBuilder.Append(fileContents);
            charsRead = streamReader.Read(fileContents, 0, _bufferSize);
        }
    }
}

Artikel asli

James
sumber
3
Jenis tes ini sangat tidak bisa diandalkan. Anda akan membaca data dari cache sistem file saat Anda mengulangi pengujian. Itu setidaknya satu kali lipat lebih cepat dari tes nyata yang membaca data dari disk. File 180 MB tidak mungkin memakan waktu kurang dari 3 detik. Nyalakan ulang mesin Anda, jalankan tes sekali untuk bilangan real.
Hans Passant
7
baris stringBuilder.Append berpotensi berbahaya, Anda harus menggantinya dengan stringBuilder.Append (fileContents, 0, charsRead); untuk memastikan Anda tidak menambahkan 1024 karakter penuh bahkan ketika streaming telah berakhir lebih awal.
Johannes Rudolph
@JohannesRudolph, komentar Anda baru saja membuat saya bug. Bagaimana Anda mendapatkan angka 1024?
OfirD
3

Anda mungkin lebih baik menggunakan penanganan file yang dipetakan memori di sini .. Dukungan file yang dipetakan memori akan ada di .NET 4 (Saya pikir ... Saya mendengarnya melalui orang lain yang membicarakannya), maka pembungkus ini yang menggunakan p / meminta untuk melakukan pekerjaan yang sama ..

Sunting: Lihat di sini di MSDN untuk cara kerjanya, berikut adalah entri blog yang menunjukkan bagaimana hal itu dilakukan di .NET 4 mendatang ketika keluar sebagai rilis. Tautan yang saya berikan sebelumnya adalah pembungkus di sekitar pinvoke untuk mencapai ini. Anda dapat memetakan seluruh file ke dalam memori, dan melihatnya seperti jendela geser saat menggulir file.

t0mm13b
sumber
3

Semua jawaban luar biasa! namun, untuk seseorang yang mencari jawaban, ini tampaknya tidak lengkap.

Karena String standar hanya dapat berukuran X, 2Gb hingga 4Gb tergantung pada konfigurasi Anda, jawaban ini tidak benar-benar memenuhi pertanyaan OP. Salah satu caranya adalah bekerja dengan List of Strings:

List<string> Words = new List<string>();

using (StreamReader sr = new StreamReader(@"C:\Temp\file.txt"))
{

string line = string.Empty;

while ((line = sr.ReadLine()) != null)
{
    Words.Add(line);
}
}

Beberapa mungkin ingin Tokenise dan membagi garis saat memproses. Daftar String sekarang dapat berisi teks dalam volume yang sangat besar.

paku berkarat
sumber
1

Sebuah iterator mungkin cocok untuk jenis pekerjaan ini:

public static IEnumerable<int> LoadFileWithProgress(string filename, StringBuilder stringData)
{
    const int charBufferSize = 4096;
    using (FileStream fs = File.OpenRead(filename))
    {
        using (BinaryReader br = new BinaryReader(fs))
        {
            long length = fs.Length;
            int numberOfChunks = Convert.ToInt32((length / charBufferSize)) + 1;
            double iter = 100 / Convert.ToDouble(numberOfChunks);
            double currentIter = 0;
            yield return Convert.ToInt32(currentIter);
            while (true)
            {
                char[] buffer = br.ReadChars(charBufferSize);
                if (buffer.Length == 0) break;
                stringData.Append(buffer);
                currentIter += iter;
                yield return Convert.ToInt32(currentIter);
            }
        }
    }
}

Anda dapat menyebutnya menggunakan yang berikut ini:

string filename = "C:\\myfile.txt";
StringBuilder sb = new StringBuilder();
foreach (int progress in LoadFileWithProgress(filename, sb))
{
    // Update your progress counter here!
}
string fileData = sb.ToString();

Saat file dimuat, iterator akan mengembalikan nomor kemajuan dari 0 hingga 100, yang dapat Anda gunakan untuk memperbarui bilah kemajuan Anda. Setelah loop selesai, StringBuilder akan berisi konten file teks.

Selain itu, karena Anda menginginkan teks, kita cukup menggunakan BinaryReader untuk membaca karakter, yang akan memastikan bahwa buffer Anda berbaris dengan benar saat membaca karakter multi-byte ( UTF-8 , UTF-16 , dll.).

Ini semua dilakukan tanpa menggunakan tugas latar belakang, utas, atau mesin status kustom yang rumit.

Extremeswank
sumber