Apakah standar C ++ mandat kinerja yang buruk untuk iostreams, atau apakah saya hanya berurusan dengan implementasi yang buruk?

197

Setiap kali saya menyebutkan kinerja lambat C ++ iostreams perpustakaan standar, saya bertemu dengan gelombang ketidakpercayaan. Namun saya memiliki hasil profiler yang menunjukkan sejumlah besar waktu yang dihabiskan dalam kode pustaka iostream (optimisasi kompiler penuh), dan beralih dari iostreams ke I / O API khusus OS dan manajemen buffer kustom tidak memberikan urutan peningkatan yang besar.

Apa pekerjaan tambahan yang dilakukan pustaka standar C ++, apakah diperlukan oleh standar, dan apakah berguna dalam praktiknya? Atau apakah beberapa kompiler menyediakan implementasi iostreams yang kompetitif dengan manajemen buffer manual?

Tolak ukur

Untuk menyelesaikan masalah, saya telah menulis beberapa program singkat untuk melatih buffer internal iostreams:

Perhatikan bahwa ostringstreamdan stringbufversi menjalankan lebih sedikit iterasi karena mereka jauh lebih lambat.

Pada ideone, ostringstreamini sekitar 3 kali lebih lambat dari std:copy+ back_inserter+ std::vector, dan sekitar 15 kali lebih lambat daripada memcpymenjadi buffer mentah. Ini terasa konsisten dengan profil sebelum dan sesudah ketika saya mengganti aplikasi asli saya ke buffering kustom.

Ini semua adalah buffer dalam memori, sehingga lambatnya iostreams tidak dapat disalahkan pada I / O disk yang lambat, terlalu banyak pembilasan, sinkronisasi dengan stdio, atau hal lain apa pun yang digunakan orang untuk memaafkan kelambatan yang diamati dari perpustakaan standar C ++ iostream.

Akan menyenangkan untuk melihat tolok ukur pada sistem lain dan komentar tentang hal-hal implementasi yang umum dilakukan (seperti libc ++ gcc, Visual C ++, Intel C ++) dan berapa banyak overhead yang diamanatkan oleh standar.

Dasar pemikiran untuk tes ini

Sejumlah orang telah dengan benar menunjukkan bahwa iostreams lebih umum digunakan untuk output yang diformat. Namun, mereka juga satu-satunya API modern yang disediakan oleh standar C ++ untuk akses file biner. Tetapi alasan sebenarnya untuk melakukan tes kinerja pada buffering internal berlaku untuk I / O yang diformat secara khas: jika iostreams tidak dapat menyimpan pengontrol disk yang disertakan dengan data mentah, bagaimana mereka dapat mengikuti ketika mereka juga bertanggung jawab untuk memformat?

Timing Benchmark

Semua ini adalah per iterasi dari kloop luar ( ).

Pada ideone (gcc-4.3.4, OS dan perangkat keras yang tidak dikenal):

  • ostringstream: 53 milidetik
  • stringbuf: 27 ms
  • vector<char>dan back_inserter: 17,6 ms
  • vector<char> dengan iterator biasa: 10,6 ms
  • vector<char> iterator dan batas memeriksa: 11,4 ms
  • char[]: 3,7 ms

Di laptop saya (Visual C ++ 2010 x86,, cl /Ox /EHscWindows 7 Ultimate 64-bit, Intel Core i7, 8 GB RAM):

  • ostringstream: 73,4 milidetik, 71,6 ms
  • stringbuf: 21,7 ms, 21,3 ms
  • vector<char>dan back_inserter: 34,6 ms, 34,4 ms
  • vector<char> dengan iterator biasa: 1,10 ms, 1,04 ms
  • vector<char> iterator dan batas memeriksa: 1,11 ms, 0,87 ms, 1,12 ms, 0,89 ms, 1,02 ms, 1,14 ms
  • char[]: 1,48 ms, 1,57 ms

Visual C ++ 2010 x86, dengan Optimization Profil-Guided cl /Ox /EHsc /GL /c, link /ltcg:pgi, run, link /ltcg:pgo, ukuran:

  • ostringstream: 61,2 ms, 60,5 ms
  • vector<char> dengan iterator biasa: 1,04 ms, 1,03 ms

Laptop yang sama, OS yang sama, menggunakan cygwin gcc 4.3.4 g++ -O3:

  • ostringstream: 62,7 ms, 60,5 ms
  • stringbuf: 44,4 ms, 44,5 ms
  • vector<char>dan back_inserter: 13,5 ms, 13,6 ms
  • vector<char> dengan iterator biasa: 4,1 ms, 3,9 ms
  • vector<char> iterator dan batas memeriksa: 4.0 ms, 4.0 ms
  • char[]: 3,57 ms, 3,75 ms

Laptop yang sama, Visual C ++ 2008 SP1, cl /Ox /EHsc:

  • ostringstream: 88,7 ms, 87,6 ms
  • stringbuf: 23,3 ms, 23,4 ms
  • vector<char>dan back_inserter: 26.1 ms, 24.5 ms
  • vector<char> dengan iterator biasa: 3,13 ms, 2,48 ms
  • vector<char> iterator dan batas memeriksa: 2,97 ms, 2,53 ms
  • char[]: 1,52 ms, 1,25 ms

Laptop yang sama, kompiler Visual C ++ 2010 64-bit:

  • ostringstream: 48,6 ms, 45,0 ms
  • stringbuf: 16.2 ms, 16.0 ms
  • vector<char>dan back_inserter: 26,3 ms, 26,5 ms
  • vector<char> dengan iterator biasa: 0,87 ms, 0,89 ms
  • vector<char> iterator dan batas memeriksa: 0,99 ms, 0,99 ms
  • char[]: 1,25 ms, 1,24 ms

EDIT: Berlari semua dua kali untuk melihat seberapa konsisten hasilnya. IMO cukup konsisten.

CATATAN: Di laptop saya, karena saya bisa menghemat lebih banyak waktu CPU daripada yang diizinkan ideone, saya mengatur jumlah iterasi ke 1000 untuk semua metode. Ini berarti bahwa ostringstreamdan vectorrealokasi, yang hanya terjadi pada pass pertama, harus memiliki dampak kecil pada hasil akhir.

EDIT: Ups, menemukan bug di- vectordengan-iterator biasa, iterator tidak sedang maju dan karena itu ada terlalu banyak cache hit. Saya bertanya-tanya bagaimana vector<char>kinerjanya char[]. Itu tidak membuat banyak perbedaan, vector<char>masih lebih cepat daripada di char[]bawah VC ++ 2010.

Kesimpulan

Buffer aliran output membutuhkan tiga langkah setiap kali data ditambahkan:

  • Periksa apakah blok yang masuk sesuai dengan ruang buffer yang tersedia.
  • Salin blok masuk.
  • Perbarui pointer akhir data.

Cuplikan kode terbaru yang saya posting, " vector<char>iterator sederhana plus batas cek" tidak hanya melakukan ini, tetapi juga mengalokasikan ruang tambahan dan memindahkan data yang ada saat blok yang masuk tidak sesuai. Seperti Clifford tunjukkan, buffering di file I / O class tidak perlu melakukan itu, itu hanya akan menyiram buffer saat ini dan menggunakannya kembali. Jadi ini harus menjadi batas atas pada biaya buffering output. Dan itulah yang dibutuhkan untuk membuat buffer di dalam memori yang berfungsi.

Jadi mengapa stringbuf2.5x lebih lambat pada ideone, dan setidaknya 10 kali lebih lambat ketika saya mengujinya? Ini tidak digunakan secara polimorfis dalam patok ukur mikro sederhana ini, jadi itu tidak menjelaskannya.

Ben Voigt
sumber
24
Anda sedang menulis sejuta karakter satu per satu, dan bertanya-tanya mengapa lebih lambat daripada menyalin ke buffer yang dialokasikan sebelumnya?
Anon.
20
@ Anon: Saya buffering empat juta byte empat sekaligus, dan ya saya bertanya-tanya mengapa itu lambat. Jika std::ostringstreamtidak cukup pintar untuk secara eksponensial meningkatkan ukuran std::vectorbuffernya, itu (A) bodoh dan (B) sesuatu yang dipikirkan orang tentang kinerja I / O. Bagaimanapun, buffer akan digunakan kembali, itu tidak dialokasikan kembali setiap waktu. Dan std::vectorjuga menggunakan buffer yang tumbuh secara dinamis. Saya mencoba bersikap adil di sini.
Ben Voigt
14
Tugas apa yang sebenarnya Anda coba patok? Jika Anda tidak menggunakan salah satu fitur pemformatan ostringstreamdan Anda ingin kinerja secepat mungkin maka Anda harus mempertimbangkan untuk langsung melakukannya stringbuf. The ostreamkelas kira untuk mengikat fungsi format bersama-sama lokal menyadari dengan pilihan fleksibel penyangga (file, string, dll) melalui rdbuf()dan antarmuka fungsi virtual. Jika Anda tidak melakukan pemformatan apa pun, tingkat tipuan ekstra itu tentu akan terlihat mahal secara proporsional dibandingkan dengan pendekatan lain.
CB Bailey
5
+1 untuk kebenaran op. Kita sudah order atau kecepatan besarnya up dengan bergerak dari ofstreamke fprintfketika keluaran Info logging yang melibatkan ganda. MSVC 2008 di WinXPsp3. iostreams hanya lambat anjing.
KitsuneYMG
6
Berikut adalah beberapa tes di situs komite: open-std.org/jtc1/sc22/wg21/docs/D_5.cpp
Johannes Schaub - litb

Jawaban:

49

Tidak menjawab secara spesifik pertanyaan Anda seperti judulnya: Laporan Teknis 2006 tentang Kinerja C ++ memiliki bagian yang menarik tentang IOStreams (hal.68). Yang paling relevan dengan pertanyaan Anda ada di Bagian 6.1.2 ("Kecepatan Eksekusi"):

Karena aspek-aspek tertentu dari pemrosesan IOStreams didistribusikan ke berbagai sisi, tampaknya Standar mengamanatkan implementasi yang tidak efisien. Tapi ini tidak terjadi - dengan menggunakan beberapa bentuk preprocessing, banyak pekerjaan yang bisa dihindari. Dengan tautan yang sedikit lebih pintar daripada yang biasanya digunakan, adalah mungkin untuk menghapus beberapa inefisiensi ini. Ini dibahas dalam §6.2.3 dan §6.2.5.

Karena laporan ini ditulis pada tahun 2006, orang akan berharap bahwa banyak dari rekomendasi akan dimasukkan ke dalam kompiler saat ini, tetapi mungkin ini tidak terjadi.

Seperti yang Anda sebutkan, aspek mungkin tidak menampilkan write()(tapi saya tidak akan menganggap itu secara membabi buta). Jadi apa fitur? Menjalankan GProf pada ostringstreamkode Anda yang dikompilasi dengan GCC memberikan uraian berikut:

  • 44,23% dalam std::basic_streambuf<char>::xsputn(char const*, int)
  • 34,62% ​​dalam std::ostream::write(char const*, int)
  • 12,50% dalam main
  • 6,73% dalam std::ostream::sentry::sentry(std::ostream&)
  • 0,96% dalam std::string::_M_replace_safe(unsigned int, unsigned int, char const*, unsigned int)
  • 0,96% dalam std::basic_ostringstream<char>::basic_ostringstream(std::_Ios_Openmode)
  • 0,00% dalam std::fpos<int>::fpos(long long)

Jadi sebagian besar waktu dihabiskan xsputn, yang akhirnya memanggil std::copy()setelah banyak memeriksa dan memperbarui posisi kursor dan buffer (lihat c++\bits\streambuf.tccdetailnya).

Pendapat saya adalah Anda berfokus pada situasi terburuk. Semua pemeriksaan yang dilakukan akan menjadi sebagian kecil dari total pekerjaan yang dilakukan jika Anda berurusan dengan potongan data yang cukup besar. Tetapi kode Anda menggeser data dalam empat byte sekaligus, dan menimbulkan semua biaya tambahan setiap kali. Jelas seseorang akan menghindari melakukan hal itu dalam situasi kehidupan nyata - pertimbangkan betapa diabaikannya hukuman jika writedipanggil pada array 1m int bukannya pada 1m kali pada satu int. Dan dalam situasi kehidupan nyata seseorang akan sangat menghargai fitur-fitur penting dari iOStreams, yaitu desain yang aman-memori dan tipe-aman. Keuntungan semacam itu ada harganya, dan Anda telah menulis tes yang membuat biaya ini mendominasi waktu eksekusi.

beldaz
sumber
Kedengarannya seperti informasi yang bagus untuk pertanyaan di masa depan tentang kinerja penyisipan diformat / ekstraksi iostreams yang mungkin akan saya tanyakan segera. Tapi saya tidak percaya ada aspek yang terlibat ostream::write().
Ben Voigt
4
+1 untuk profil (itu saya kira mesin Linux?). Namun, saya sebenarnya menambahkan empat byte sekaligus (sebenarnya sizeof i, tetapi semua kompiler yang saya uji memiliki 4-byte int). Dan itu tampaknya tidak terlalu realistis bagi saya, menurut Anda, ukuran potongan apa yang dilewati dalam setiap panggilan ke xsputndalam kode yang khas stream << "VAR: " << var.x << ", " << var.y << endl;.
Ben Voigt
39
@ Beldaz: Contoh kode "khas" yang hanya memanggil xsputnlima kali bisa saja berada di dalam loop yang menulis 10 juta file baris. Mengirim data ke iostreams dalam potongan besar jauh lebih sedikit dari skenario kehidupan nyata daripada kode benchmark saya. Mengapa saya harus menulis ke aliran buffered dengan jumlah panggilan minimum? Jika saya harus melakukan buffering sendiri, apa gunanya iostreams? Dan dengan data biner, saya memiliki opsi untuk buffer sendiri, ketika menulis jutaan angka ke file teks, opsi massal tidak ada, saya HARUS memanggil operator <<masing-masing.
Ben Voigt
1
@ Beldaz: Orang dapat memperkirakan kapan I / O mulai mendominasi dengan perhitungan sederhana. Pada tingkat penulisan rata-rata 90 MB / s yang merupakan ciri khas hard disk tingkat konsumen saat ini, pembilasan buffer 4MB membutuhkan waktu <45ms (throughput, latensi tidak penting karena cache tulis OS). Jika menjalankan loop dalam membutuhkan waktu lebih lama dari itu untuk mengisi buffer, maka CPU akan menjadi faktor pembatas. Jika loop dalam berjalan lebih cepat, maka I / O akan menjadi faktor pembatas, atau setidaknya ada beberapa waktu CPU yang tersisa untuk melakukan pekerjaan nyata.
Ben Voigt
5
Tentu saja, itu tidak berarti bahwa menggunakan iostreams berarti program yang lambat. Jika I / O adalah bagian yang sangat kecil dari program, maka menggunakan perpustakaan I / O dengan kinerja yang buruk tidak akan memiliki banyak dampak secara keseluruhan. Tetapi tidak cukup sering dipanggil untuk masalah tidak sama dengan kinerja yang baik, dan dalam aplikasi I / O yang berat, itu penting.
Ben Voigt
27

Saya agak kecewa dengan pengguna Visual Studio di luar sana, yang lebih suka beri satu ini:

  • Dalam implementasi Visual Studio ostream, sentryobjek (yang diperlukan oleh standar) memasuki bagian kritis yang melindungi streambuf(yang tidak diperlukan). Ini sepertinya tidak opsional, jadi Anda membayar biaya sinkronisasi utas bahkan untuk streaming lokal yang digunakan oleh utas tunggal, yang tidak perlu sinkronisasi.

Ini menyakitkan kode yang digunakan ostringstreamuntuk memformat pesan dengan cukup parah. Menggunakan stringbufsecara langsung menghindari penggunaan sentry, tetapi operator penyisipan yang diformat tidak dapat bekerja secara langsung pada streambufs. Untuk Visual C ++ 2010, bagian kritis melambat ostringstream::writedengan faktor tiga vs stringbuf::sputnpanggilan yang mendasarinya .

Melihat data profiler beldaz di newlib , tampak jelas bahwa gcc sentrytidak melakukan hal gila seperti ini. ostringstream::writedi bawah gcc hanya membutuhkan waktu sekitar 50% lebih lama dari stringbuf::sputn, tetapi stringbufitu sendiri jauh lebih lambat daripada di bawah VC ++. Dan keduanya masih membandingkan sangat tidak menguntungkan untuk menggunakan vector<char>buffering I / O, meskipun tidak dengan margin yang sama seperti di bawah VC ++.

Ben Voigt
sumber
Apakah informasi ini masih terkini? AFAIK, implementasi C ++ 11 yang dikirimkan bersama GCC melakukan kunci 'gila' ini. Tentu saja, VS2010 masih melakukannya juga. Adakah yang bisa memperjelas perilaku ini dan jika 'yang tidak diperlukan' masih berlaku di C ++ 11?
mloskot
2
@mloskot: Saya melihat tidak ada persyaratan keamanan thread di sentry... "Kelas penjaga mendefinisikan kelas yang bertanggung jawab untuk melakukan pengecualian awalan aman dan operasi sufiks." dan sebuah catatan "Konstruktor penjaga dan destruktor juga dapat melakukan operasi tergantung pada implementasi tambahan." Seseorang juga dapat menduga dari prinsip C ++ dari "Anda tidak membayar apa yang tidak Anda gunakan" bahwa komite C ++ tidak akan pernah menyetujui persyaratan boros seperti itu. Tapi jangan ragu untuk bertanya tentang keamanan thread iostream.
Ben Voigt
8

Masalah yang Anda lihat adalah semua di overhead sekitar setiap panggilan untuk menulis (). Setiap level abstraksi yang Anda tambahkan (char [] -> vector -> string -> ostringstream) menambahkan beberapa fungsi lagi panggilan / pengembalian dan guff rumah tangga lain yang - jika Anda menyebutnya jutaan kali - bertambah.

Saya memodifikasi dua contoh pada ideone untuk menulis sepuluh int sekaligus. Waktu ostringstream meningkat dari 53 menjadi 6 ms (hampir 10 x peningkatan) sementara char loop meningkat (3,7 menjadi 1,5) - berguna, tetapi hanya dengan faktor dua.

Jika Anda peduli dengan kinerja maka Anda harus memilih alat yang tepat untuk pekerjaan itu. ostringstream berguna dan fleksibel, tetapi ada penalti untuk menggunakannya dengan cara yang Anda coba. char [] adalah pekerjaan yang lebih sulit, tetapi perolehan kinerja bisa sangat bagus (ingat gcc mungkin akan menyatukan memcpys untuk Anda juga).

Singkatnya, ostringstream tidak rusak, tetapi semakin dekat Anda dengan logam semakin cepat kode Anda akan berjalan. Assembler masih memiliki keunggulan bagi sebagian orang.

Roddy
sumber
8
Apa yang ostringstream::write()harus dilakukan vector::push_back()tetapi tidak? Jika ada, itu harus lebih cepat karena itu menyerahkan blok bukannya empat elemen individu. Jika ostringstreamlebih lambat daripada std::vectortanpa menyediakan fitur tambahan, maka ya saya akan menyebutnya rusak.
Ben Voigt
1
@Ben Voigt: Sebaliknya, vektor sesuatu harus melakukan itu atau tidak harus melakukan yang membuat vektor lebih berkinerja dalam kasus ini. Vektor dijamin bersebelahan dalam memori, sementara ostringstream tidak. Vektor adalah salah satu kelas yang dirancang untuk menjadi penampil, sedangkan ostringstream tidak.
Dragontamer5788
2
@Ben Voigt: Menggunakan stringbufsecara langsung tidak akan menghapus semua panggilan fungsi karena stringbufantarmuka publik terdiri dari fungsi non-virtual publik di kelas dasar yang kemudian dikirim ke fungsi virtual yang dilindungi di kelas turunan.
CB Bailey
2
@ Charles: Pada setiap kompiler yang layak seharusnya, karena panggilan fungsi publik akan dimasukkan ke dalam konteks di mana tipe dinamis diketahui oleh kompiler, ia dapat menghapus tipuan dan bahkan inline panggilan tersebut.
Ben Voigt
6
@ Raddy: Saya harus berpikir bahwa ini semua kode template inline, terlihat di setiap unit kompilasi. Tapi saya rasa itu bisa bervariasi tergantung implementasi. Yang pasti saya harapkan panggilan dalam diskusi, sputnfungsi publik yang memanggil virtual protected xsputn, akan diuraikan. Sekalipun xsputntidak digarisbawahi, kompiler dapat, sambil inlining sputn, menentukan xsputnoverride yang tepat dibutuhkan dan menghasilkan panggilan langsung tanpa melalui vtable.
Ben Voigt
1

Untuk mendapatkan kinerja yang lebih baik, Anda harus memahami cara kerja wadah yang Anda gunakan. Dalam contoh array [] array Anda, array dengan ukuran yang dibutuhkan dialokasikan terlebih dahulu. Dalam contoh vektor dan ostringstream Anda memaksa objek untuk berulang kali mengalokasikan dan merealokasi dan mungkin menyalin data berkali-kali saat objek tumbuh.

Dengan std :: vector, ini dengan mudah diselesaikan dengan menginisialisasi ukuran vektor ke ukuran akhir seperti yang Anda lakukan pada array char; alih-alih, Anda malah melumpuhkan kinerja dengan mengubah ukuran menjadi nol! Itu bukan perbandingan yang adil.

Sehubungan dengan ostringstream, preallocating ruang tidak mungkin, saya akan menyarankan bahwa itu adalah penggunaan yang tidak tepat. Kelas memiliki utilitas yang jauh lebih besar daripada array char sederhana, tetapi jika Anda tidak membutuhkan utilitas itu, maka jangan menggunakannya, karena Anda akan membayar biaya overhead dalam hal apa pun. Alih-alih itu harus digunakan untuk apa yang baik untuk - memformat data menjadi string. C ++ menyediakan berbagai macam wadah dan ostringstram adalah salah satu yang paling tidak sesuai untuk tujuan ini.

Dalam hal vektor dan ostringstream Anda mendapatkan perlindungan dari buffer overrun, Anda tidak mendapatkannya dengan array char, dan perlindungan itu tidak datang secara gratis.

Clifford
sumber
1
Alokasi tampaknya tidak menjadi masalah bagi ostringstream. Dia hanya mencari kembali ke nol untuk iterasi selanjutnya. Tidak ada pemotongan. Saya juga mencoba ostringstream.str.reserve(4000000)dan tidak ada bedanya.
Roddy
Saya pikir dengan ostringstream, Anda bisa "memesan" dengan mengirimkan string tiruan, yaitu: ostringstream str(string(1000000 * sizeof(int), '\0'));Dengan vector, resizetidak membatalkan alokasi ruang apa pun, itu hanya diperluas jika perlu.
Nim
1
"vektor .. perlindungan dari buffer overrun". Kesalahpahaman umum - vector[]operator biasanya TIDAK diperiksa untuk kesalahan batas secara default. vector.at()Namun demikian.
Roddy
2
vector<T>::resize(0)biasanya tidak mengalokasikan kembali memori
Niki Yoshiuchi
2
@ Raddy: Tidak menggunakan operator[], tetapi push_back()(dengan cara back_inserter), yang pasti TIDAK menguji meluap. Menambahkan versi lain yang tidak digunakan push_back.
Ben Voigt