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:
- menempatkan data biner ke dalam
ostringstream
http://ideone.com/2PPYw - menempatkan data biner ke dalam
char[]
buffer http://ideone.com/Ni5ct - memasukkan data biner ke dalam http://ideone.com/Mj2Fi
vector<char>
menggunakanback_inserter
- BARU :
vector<char>
iterator sederhana http://ideone.com/9iitv - BARU : menempatkan data biner langsung ke
stringbuf
http://ideone.com/qc9QA - BARU :
vector<char>
iterator sederhana plus batas periksa http://ideone.com/YyrKy
Perhatikan bahwa ostringstream
dan stringbuf
versi menjalankan lebih sedikit iterasi karena mereka jauh lebih lambat.
Pada ideone, ostringstream
ini sekitar 3 kali lebih lambat dari std:copy
+ back_inserter
+ std::vector
, dan sekitar 15 kali lebih lambat daripada memcpy
menjadi 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 k
loop luar ( ).
Pada ideone (gcc-4.3.4, OS dan perangkat keras yang tidak dikenal):
ostringstream
: 53 milidetikstringbuf
: 27 msvector<char>
danback_inserter
: 17,6 msvector<char>
dengan iterator biasa: 10,6 msvector<char>
iterator dan batas memeriksa: 11,4 mschar[]
: 3,7 ms
Di laptop saya (Visual C ++ 2010 x86,, cl /Ox /EHsc
Windows 7 Ultimate 64-bit, Intel Core i7, 8 GB RAM):
ostringstream
: 73,4 milidetik, 71,6 msstringbuf
: 21,7 ms, 21,3 msvector<char>
danback_inserter
: 34,6 ms, 34,4 msvector<char>
dengan iterator biasa: 1,10 ms, 1,04 msvector<char>
iterator dan batas memeriksa: 1,11 ms, 0,87 ms, 1,12 ms, 0,89 ms, 1,02 ms, 1,14 mschar[]
: 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 msvector<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 msstringbuf
: 44,4 ms, 44,5 msvector<char>
danback_inserter
: 13,5 ms, 13,6 msvector<char>
dengan iterator biasa: 4,1 ms, 3,9 msvector<char>
iterator dan batas memeriksa: 4.0 ms, 4.0 mschar[]
: 3,57 ms, 3,75 ms
Laptop yang sama, Visual C ++ 2008 SP1, cl /Ox /EHsc
:
ostringstream
: 88,7 ms, 87,6 msstringbuf
: 23,3 ms, 23,4 msvector<char>
danback_inserter
: 26.1 ms, 24.5 msvector<char>
dengan iterator biasa: 3,13 ms, 2,48 msvector<char>
iterator dan batas memeriksa: 2,97 ms, 2,53 mschar[]
: 1,52 ms, 1,25 ms
Laptop yang sama, kompiler Visual C ++ 2010 64-bit:
ostringstream
: 48,6 ms, 45,0 msstringbuf
: 16.2 ms, 16.0 msvector<char>
danback_inserter
: 26,3 ms, 26,5 msvector<char>
dengan iterator biasa: 0,87 ms, 0,89 msvector<char>
iterator dan batas memeriksa: 0,99 ms, 0,99 mschar[]
: 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 ostringstream
dan vector
realokasi, yang hanya terjadi pada pass pertama, harus memiliki dampak kecil pada hasil akhir.
EDIT: Ups, menemukan bug di- vector
dengan-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 stringbuf
2.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.
sumber
std::ostringstream
tidak cukup pintar untuk secara eksponensial meningkatkan ukuranstd::vector
buffernya, itu (A) bodoh dan (B) sesuatu yang dipikirkan orang tentang kinerja I / O. Bagaimanapun, buffer akan digunakan kembali, itu tidak dialokasikan kembali setiap waktu. Danstd::vector
juga menggunakan buffer yang tumbuh secara dinamis. Saya mencoba bersikap adil di sini.ostringstream
dan Anda ingin kinerja secepat mungkin maka Anda harus mempertimbangkan untuk langsung melakukannyastringbuf
. Theostream
kelas kira untuk mengikat fungsi format bersama-sama lokal menyadari dengan pilihan fleksibel penyangga (file, string, dll) melaluirdbuf()
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.ofstream
kefprintf
ketika keluaran Info logging yang melibatkan ganda. MSVC 2008 di WinXPsp3. iostreams hanya lambat anjing.Jawaban:
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 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 padaostringstream
kode Anda yang dikompilasi dengan GCC memberikan uraian berikut:std::basic_streambuf<char>::xsputn(char const*, int)
std::ostream::write(char const*, int)
main
std::ostream::sentry::sentry(std::ostream&)
std::string::_M_replace_safe(unsigned int, unsigned int, char const*, unsigned int)
std::basic_ostringstream<char>::basic_ostringstream(std::_Ios_Openmode)
std::fpos<int>::fpos(long long)
Jadi sebagian besar waktu dihabiskan
xsputn
, yang akhirnya memanggilstd::copy()
setelah banyak memeriksa dan memperbarui posisi kursor dan buffer (lihatc++\bits\streambuf.tcc
detailnya).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
write
dipanggil 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.sumber
ostream::write()
.sizeof i
, tetapi semua kompiler yang saya uji memiliki 4-byteint
). Dan itu tampaknya tidak terlalu realistis bagi saya, menurut Anda, ukuran potongan apa yang dilewati dalam setiap panggilan kexsputn
dalam kode yang khasstream << "VAR: " << var.x << ", " << var.y << endl;
.xsputn
lima 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 memanggiloperator <<
masing-masing.Saya agak kecewa dengan pengguna Visual Studio di luar sana, yang lebih suka beri satu ini:
ostream
,sentry
objek (yang diperlukan oleh standar) memasuki bagian kritis yang melindungistreambuf
(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
ostringstream
untuk memformat pesan dengan cukup parah. Menggunakanstringbuf
secara langsung menghindari penggunaansentry
, tetapi operator penyisipan yang diformat tidak dapat bekerja secara langsung padastreambuf
s. Untuk Visual C ++ 2010, bagian kritis melambatostringstream::write
dengan faktor tiga vsstringbuf::sputn
panggilan yang mendasarinya .Melihat data profiler beldaz di newlib , tampak jelas bahwa gcc
sentry
tidak melakukan hal gila seperti ini.ostringstream::write
di bawah gcc hanya membutuhkan waktu sekitar 50% lebih lama daristringbuf::sputn
, tetapistringbuf
itu sendiri jauh lebih lambat daripada di bawah VC ++. Dan keduanya masih membandingkan sangat tidak menguntungkan untuk menggunakanvector<char>
buffering I / O, meskipun tidak dengan margin yang sama seperti di bawah VC ++.sumber
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.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.
sumber
ostringstream::write()
harus dilakukanvector::push_back()
tetapi tidak? Jika ada, itu harus lebih cepat karena itu menyerahkan blok bukannya empat elemen individu. Jikaostringstream
lebih lambat daripadastd::vector
tanpa menyediakan fitur tambahan, maka ya saya akan menyebutnya rusak.stringbuf
secara langsung tidak akan menghapus semua panggilan fungsi karenastringbuf
antarmuka publik terdiri dari fungsi non-virtual publik di kelas dasar yang kemudian dikirim ke fungsi virtual yang dilindungi di kelas turunan.sputn
fungsi publik yang memanggil virtual protectedxsputn
, akan diuraikan. Sekalipunxsputn
tidak digarisbawahi, kompiler dapat, sambil inliningsputn
, menentukanxsputn
override yang tepat dibutuhkan dan menghasilkan panggilan langsung tanpa melalui vtable.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.
sumber
ostringstream.str.reserve(4000000)
dan tidak ada bedanya.ostringstream
, Anda bisa "memesan" dengan mengirimkan string tiruan, yaitu:ostringstream str(string(1000000 * sizeof(int), '\0'));
Denganvector
,resize
tidak membatalkan alokasi ruang apa pun, itu hanya diperluas jika perlu.vector[]
operator biasanya TIDAK diperiksa untuk kesalahan batas secara default.vector.at()
Namun demikian.vector<T>::resize(0)
biasanya tidak mengalokasikan kembali memorioperator[]
, tetapipush_back()
(dengan caraback_inserter
), yang pasti TIDAK menguji meluap. Menambahkan versi lain yang tidak digunakanpush_back
.