C ++ standardı iostreams için düşük performans gerektiriyor mu yoksa sadece kötü bir uygulama ile mi uğraşıyorum?


197

C ++ standart kütüphane iostreams'in yavaş performansından her bahsettiğimde, bir güvensizlik dalgası ile karşılaşıyorum. Yine de, iostream kütüphane kodunda (tam derleyici optimizasyonları) harcanan büyük miktarlarda zaman gösteren profiller sonuçlarına sahibim ve iostreams'ten OS'ye özgü I / O API'larına ve özel tampon yönetimine geçiş, bir büyüklük iyileştirme sırası veriyor.

C ++ standart kütüphanesinin yaptığı ekstra iş nedir, standart için gerekli midir ve pratikte faydalı mıdır? Yoksa bazı derleyiciler manuel arabellek yönetimi ile rekabet edebilecek iostream uygulamaları sunuyor mu?

Deneyler

Konuları harekete geçirmek için, iostreams iç ara belleğini kullanmak için birkaç kısa program yazdım:

ostringstreamVe stringbufsürümlerinin çok daha yavaş olduğu için daha az yineleme kullandığını unutmayın .

İdeal olarak, + + ' ostringstreamdan yaklaşık 3 kat , ham bir tampondan yaklaşık 15 kat daha yavaştır . Gerçek uygulamamı özel arabelleğe geçirdiğimde, önceki ve sonraki profil oluşturma ile tutarlı hissediyor.std:copyback_inserterstd::vectormemcpy

Bunların hepsi bellek içi arabelleklerdir, bu nedenle iostreams'in yavaşlığı yavaş disk G / Ç, çok fazla yıkama, stdio ile senkronizasyon veya insanların C ++ standart kütüphanesinin gözlemlenen yavaşlığını mazur göstermek için kullandıkları diğer şeylerden sorumlu tutulamaz. iostream.

Diğer sistemlerde karşılaştırmalar ve yaygın uygulamaların yaptığı şeyler (gcc's libc ++, Visual C ++, Intel C ++ gibi) ve genel giderlerin ne kadarının standart tarafından zorunlu kılındığına dair yorumları görmek güzel olurdu.

Bu test için gerekçe

Bazı insanlar iostream'lerin biçimlendirilmiş çıktı için daha yaygın olarak kullanıldığını doğru bir şekilde belirtti. Ancak, ikili dosya erişimi için C ++ standardı tarafından sağlanan tek modern API'dir. Ancak dahili arabellek üzerinde performans testleri yapmanın gerçek nedeni, tipik biçimlendirilmiş G / Ç için geçerlidir: iostreams disk denetleyicisini ham verilerle birlikte tutamazsa, biçimlendirmeden de sorumlu olduklarında nasıl devam edebilirler?

Karşılaştırma Zamanlaması

Bütün bunlar dış ( k) döngüsünün tekrarı içindir.

İdeone'de (gcc-4.3.4, bilinmeyen işletim sistemi ve donanım):

  • ostringstream: 53 milisaniye
  • stringbuf: 27 ms
  • vector<char>ve back_inserter: 17,6 ms
  • vector<char> sıradan yineleyici ile: 10.6 ms
  • vector<char> yineleyici ve sınır kontrolü: 11.4 ms
  • char[]: 3,7 ms

Dizüstü bilgisayarımda (Visual C ++ 2010 x86,, cl /Ox /EHscWindows 7 Ultimate 64 bit, Intel Core i7, 8 GB RAM):

  • ostringstream: 73.4 milisaniye, 71.6 ms
  • stringbuf: 21,7 ms, 21,3 ms
  • vector<char>ve back_inserter: 34,6 ms, 34,4 ms
  • vector<char> sıradan yineleyici ile: 1.10 ms, 1.04 ms
  • vector<char> yineleyici ve sınır kontrolü: 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, Profil Kılavuzlu Optimizasyon cl /Ox /EHsc /GL /cile link /ltcg:pgi, çalıştırın link /ltcg:pgo, ölçün:

  • ostringstream: 61,2 ms, 60,5 ms
  • vector<char> sıradan yineleyici ile: 1.04 ms, 1.03 ms

Aynı dizüstü bilgisayar, aynı işletim sistemi, cygwin gcc 4.3.4 kullanarak g++ -O3:

  • ostringstream: 62,7 ms, 60,5 ms
  • stringbuf: 44,4 ms, 44,5 ms
  • vector<char>ve back_inserter: 13,5 ms, 13,6 ms
  • vector<char> sıradan yineleyici ile: 4.1 ms, 3.9 ms
  • vector<char> yineleyici ve sınır kontrolü: 4.0 ms, 4.0 ms
  • char[]: 3,57 ms, 3,75 ms

Aynı dizüstü bilgisayar, Visual C ++ 2008 SP1, cl /Ox /EHsc:

  • ostringstream: 88.7 ms, 87.6 ms
  • stringbuf: 23,3 ms, 23,4 ms
  • vector<char>ve back_inserter: 26,1 ms, 24,5 ms
  • vector<char> sıradan yineleyici ile: 3.13 ms, 2.48 ms
  • vector<char> yineleyici ve sınır kontrolü: 2,97 ms, 2,53 ms
  • char[]: 1,52 ms, 1,25 ms

Aynı dizüstü bilgisayar, Visual C ++ 2010 64 bit derleyici:

  • ostringstream: 48,6 ms, 45,0 ms
  • stringbuf: 16,2 ms, 16,0 ms
  • vector<char>ve back_inserter: 26,3 ms, 26,5 ms
  • vector<char> sıradan yineleyici ile: 0.87 ms, 0.89 ms
  • vector<char> yineleyici ve sınır kontrolü: 0.99 ms, 0.99 ms
  • char[]: 1,25 ms, 1,24 ms

EDIT: Sonuçların ne kadar tutarlı olduğunu görmek için hepsini iki kez koştu. Oldukça tutarlı IMO.

NOT: Dizüstü bilgisayarımda, ideone'nin izin verdiğinden daha fazla CPU zamanı ayırabildiğim için, tüm yöntemler için yineleme sayısını 1000'e ayarladım. Bu araçlar olduğunu ostringstreamve vectoryalnızca ilk geçişte gerçekleşir yeniden tahsis, nihai sonuçlar üzerinde çok az etkiye sahiptir.

DÜZENLEME: Hata, vectorsıradan yineleyici ile bir hata buldu , yineleyici gelişmiş değildi ve bu nedenle çok fazla önbellek isabet vardı. Nasıl vector<char>daha iyi performans gösterdiğini merak ediyordum char[]. Yine de çok fazla fark yaratmadı vector<char>, hala char[]VC ++ 2010'dan daha hızlı .

Sonuçlar

Çıktı akışlarının arabelleğe alınması, her veri eklendiğinde üç adım gerektirir:

  • Gelen bloğun kullanılabilir arabellek alanına sığdığını kontrol edin.
  • Gelen bloğu kopyalayın.
  • Veri sonu işaretçisini güncelleyin.

Gönderdiğim son kod snippet'i, " vector<char>basit yineleyici artı sınır kontrolü" sadece bunu yapmakla kalmaz, aynı zamanda ek alan ayırır ve gelen blok sığmadığında mevcut verileri taşır. Clifford'un işaret ettiği gibi, bir dosya G / Ç sınıfında arabelleğe almanın bunu yapmak zorunda kalmayacak, sadece mevcut arabelleği yıkayıp tekrar kullanacaktı. Bu nedenle, tamponlama çıktısının maliyetinde bir üst sınır olmalıdır. Ve tam olarak çalışan bir bellek içi tamponu yapmak için gereken şey budur.

Öyleyse stringbufideone'de neden 2,5 kat daha yavaş ve test ettiğimde en az 10 kat daha yavaş? Bu basit mikro-ölçütte polimorfik olarak kullanılmıyor, bu yüzden açıklamıyor.


24
Her seferinde bir milyon karakter yazıyorsunuz ve neden önceden yerleştirilmiş bir ara belleğe kopyalamaktan daha yavaş olduğunu mu merak ediyorsunuz?
Anon.

20
@Anon: Bir seferde dört milyon bayt arabelleğe veriyorum ve evet bunun neden yavaş olduğunu merak ediyorum. Eğer std::ostringstreamkatlanarak tampon boyutunun yolu artıracak etkili yeterli değildir std::vectorI düşünmeye en (A) aptal ve (B) bir şey insanlar / O performansı hakkında düşünmek gerektiğini yapar. Her neyse, arabellek yeniden kullanılır, her seferinde yeniden tahsis edilmez. Ayrıca std::vectordinamik olarak büyüyen bir tampon kullanıyor. Burada adil olmaya çalışıyorum.
Ben Voigt

14
Aslında hangi görevi kıyaslamaya çalışıyorsunuz? Biçimlendirme özelliklerinden herhangi birini kullanmıyorsanız ostringstreamve olabildiğince hızlı performans istiyorsanız doğrudan gitmeyi düşünmelisiniz stringbuf. ostreamSınıflar aracılığıyla esnek tampon seçimi (dosya dize, vb) ile birlikte yerel farkında biçimlendirme işlevini kravat varsayalım rdbuf()ve sanal fonksiyon arayüzünde. Herhangi bir biçimlendirme yapmıyorsanız, o zaman fazladan dolaylı aktarım seviyesi kesinlikle diğer yaklaşımlara kıyasla oransal olarak pahalı görünecektir.
CB Bailey

5
+1 doğruluk için op. İkiye katlama içeren günlük bilgisi çıktısından zamana ofstreamgeçiş fprintfyaparak düzen veya büyüklükteki hız artışlarını aldık . WinXPsp3 üzerinde MSVC 2008. iostreams sadece köpek yavaş.
KitsuneYMG

Yanıtlar:


49

Sorunuzun ayrıntılarını başlık kadar çok cevaplamamak: C ++ Performansı ile ilgili 2006 Teknik Raporu'nun IOStreams hakkında ilginç bir bölümü var (s.68). Sorunuzla en alakalı olan bölüm 6.1.2'de ("Yürütme Hızı"):

IOStreams işlemenin belirli yönleri birden fazla yöne dağıtıldığı için, Standardın verimsiz bir uygulamayı zorunlu kıldığı görülmektedir. Ancak durum böyle değil - bir tür önişlem kullanarak, işin çoğundan kaçınılabilir. Tipik olarak kullanılandan biraz daha akıllı bir bağlayıcı ile, bu verimsizliklerin bazılarının giderilmesi mümkündür. Bu, §6.2.3 ve §6.2.5'te tartışılmaktadır.

Rapor 2006 yılında yazıldığından beri, tavsiyelerin çoğunun mevcut derleyicilere dahil edilmesi umulmaktadır, ancak belki de durum böyle değildir.

Bahsettiğiniz gibi, fasetler öne çıkmayabilir write()(ama bunu körü körüne kabul etmem). Peki özellik ne? ostringstreamGCC ile derlenen kodunuzda GProf'u çalıştırmak aşağıdaki dökümü verir:

  • 44.23% inç std::basic_streambuf<char>::xsputn(char const*, int)
  • 34.62% inç std::ostream::write(char const*, int)
  • % 12,50 inç main
  • % 6.73 inç std::ostream::sentry::sentry(std::ostream&)
  • % 0.96 inç std::string::_M_replace_safe(unsigned int, unsigned int, char const*, unsigned int)
  • % 0.96 inç std::basic_ostringstream<char>::basic_ostringstream(std::_Ios_Openmode)
  • % 0.00 inç std::fpos<int>::fpos(long long)

Böylece zamanın büyük kısmı harcanır xsputn, sonunda std::copy()imleç konumlarının ve arabelleklerin çok sayıda kontrol edilmesinden ve güncellenmesinden sonra çağrı yapar ( c++\bits\streambuf.tccayrıntılara bir göz atın ).

Benim bu konudaki görüşüm, en kötü duruma odaklandığınızdır. Gerçekleştirilen tüm kontrol, makul miktarda büyük veri parçalarıyla uğraşıyorsanız, yapılan toplam işin küçük bir kısmı olacaktır. Ancak kodunuz, verileri bir seferde dört bayt olarak kaydırıyor ve her seferinde tüm ekstra maliyetlere neden oluyor. Açıkçası, bunu gerçek yaşamda yapmaktan kaçınabilirsiniz - bir writeint'te 1m yerine 1m'lik bir dizi üzerinde cezanın çağrılması durumunda cezanın ne kadar ihmal edilebilir olacağını düşünün . Ve gerçek hayattaki bir durumda, IOStreams'in önemli özellikleri olan bellek güvenliği ve tip güvenliği tasarımı gerçekten takdir edilecektir. Bu tür avantajların bir bedeli vardır ve bu maliyetleri yürütme süresine hakim kılan bir test yazdınız.


Muhtemelen yakında soracağım iostreams formatlanmış ekleme / çıkarma performansı hakkında gelecekteki bir soru için harika bir bilgi gibi görünüyor. Ama ben bununla ilgili herhangi bir yön olduğuna inanmıyorum ostream::write().
Ben Voigt

4
Profil oluşturma için +1 (bu benim tahmin ettiğim bir Linux makinesi mi?). Ancak, aslında bir seferde dört bayt ekliyorum (aslında sizeof i, ancak test ettiğim tüm derleyiciler 4 bayt var int). Ve bu benim için o kadar gerçekçi görünmüyor ki, her çağrıda hangi boyut parçalarına xsputnbenzer tipik kodlarla geçildiğini düşünüyorsunuz stream << "VAR: " << var.x << ", " << var.y << endl;.
Ben Voigt

39
@beldaz: xsputn10 milyon satırlık bir dosya yazan bir döngünün içinde sadece beş kez çağrı yapan bu "tipik" kod örneği olabilir. Büyük parçalarda iostreams'e veri aktarmak, karşılaştırma kodumdan çok daha az gerçek hayat senaryosudur. Neden minimum arama sayısıyla arabelleğe alınmış bir akışa yazmam gerekiyor? Kendi arabelleğimi yapmam gerekirse, zaten iostreams'in anlamı nedir? Ve ikili veri ile, kendim arabelleğe alma seçeneği var, bir metin dosyasına milyonlarca sayı yazarken, toplu seçenek yok, operator <<her biri için aramak zorunda .
Ben Voigt

1
@beldaz: G / Ç'nin basit bir hesaplama ile baskın olmaya başladığı tahmin edilebilir. Mevcut tüketici sınıfı sabit disklere özgü 90 MB / s ortalama yazma hızında, 4 MB arabelleğin yıkanması <45 ms sürer (işlem hacmi, işletim sistemi yazma önbelleği nedeniyle gecikme önemsizdir). İç halkayı çalıştırmak tamponu doldurmaktan daha uzun sürerse, CPU sınırlayıcı faktör olacaktır. İç döngü daha hızlı çalışırsa, G / Ç sınırlayıcı faktör olacaktır veya en azından gerçek işi yapmak için biraz CPU zamanı kaldı.
Ben Voigt

5
Tabii ki, bu iostreams kullanmak mutlaka yavaş bir program anlamına gelmez. G / Ç programın çok küçük bir parçasıysa, düşük performanslı bir G / Ç kitaplığı kullanmanın genel bir etkisi olmayacaktır. Ancak, önem arz edecek kadar sık ​​çağrılmaması, iyi performansla aynı şey değildir ve I / O ağır uygulamalarında önemlidir.
Ben Voigt

27

Oldukça hayal kırıklığına uğradım, bu konuda bir hile yapan Visual Studio kullanıcıları:

  • Visual Studio uygulamasında ostream, sentrynesne (standart tarafından zorunlu kılınan), streambuf(gerekli olmayan) koruyan kritik bir bölüme girer . Bu isteğe bağlı görünmüyor, bu nedenle senkronizasyon ihtiyacı olmayan tek bir iş parçacığı tarafından kullanılan yerel bir akış için bile iş parçacığı eşitleme maliyetini ödersiniz.

Bu, ostringstreammesajları oldukça ciddi bir şekilde biçimlendirmek için kullanılan kodu incitir . Kullanılması stringbufdoğrudan kullanımını önler sentry, ancak biçimlendirilmiş yerleştirme operatörleri doğrudan çalışamaz streambufs. Visual C ++ 2010 için kritik bölüm, ostringstream::writetemel stringbuf::sputnçağrıya göre üç kat daha yavaşlıyor .

Beldaz'ın newlib'deki profil verilerine bakıldığında, gcc'ninsentry böyle çılgın bir şey yapmadığı açık görünüyor . ostringstream::writegcc altında sadece% 50 daha uzun sürer stringbuf::sputn, ancak stringbufkendisi VC ++ 'dan çok daha yavaştır. Ve her ikisi de hala vector<char>VC ++ ile aynı marjla olmasa da, bir I / O tamponlaması için çok olumsuz bir şekilde karşılaştırır .


Bu bilgi hala güncel mi? AFAIK, GCC ile birlikte gelen C ++ 11 uygulaması bu 'çılgın' kilidi gerçekleştirir. Elbette, VS2010 hala bunu yapıyor. Herkes bu davranışı açıklığa kavuşturabilir ve eğer 'gerekli olmayan' hala C ++ 11 tutar?
mloskot

2
@mloskot: Üzerinde iş parçacığı güvenliği gereksinimi görmüyorum sentry... "Sınıf nöbetçi, özel güvenli önek ve sonek işlemleri yapmaktan sorumlu bir sınıf tanımlar." ve "Nöbetçi kurucu ve yıkıcı, uygulamaya bağlı ek işlemler de yapabilir." Bir de C ++ komitesinin böylesine savurgan bir şartı asla onaylamayacağı “kullanmadığınız şeyler için ödeme yapmazsınız” C ++ prensibinden de anlaşılabilir. Ancak iostream iplik güvenliği hakkında soru sormaktan çekinmeyin.
Ben Voigt

8

Gördüğünüz sorun, her write çağrısının etrafındaki tepegözde (). Eklediğiniz her soyutlama düzeyi (char [] -> vector -> string -> ostringstream), bir milyon kez daha çağırırsanız, toplanan birkaç işlev çağrısı / geri dönüşü ve diğer temizlik yardımı ekler.

İdeone'deki örneklerden iki tanesini bir seferde on int yazacak şekilde değiştirdim. Karakter geçişi süresi 53 ila 6 ms (neredeyse 10 kat iyileştirme) olurken, char döngüsü iyileşti (3.7 ila 1.5) - yararlı, ancak sadece iki faktörle.

Performans konusunda endişeleriniz varsa, iş için doğru aracı seçmeniz gerekir. ostringstream kullanışlı ve esnektir, ancak bunu denediğiniz şekilde kullanmanın bir cezası vardır. char [] daha zor bir iştir, ancak performans kazançları harika olabilir (gcc'nin muhtemelen sizin için memcpys'i satır içinde yapacağını unutmayın).

Kısacası, ostringstream kırılmaz, ancak metale yaklaştıkça kodunuz daha hızlı çalışır. Assembler hala bazı halk için avantajlara sahiptir.


8
Ne ostringstream::write()yapmak vector::push_back()zorunda değil mi? Bir şey varsa, dört ayrı eleman yerine bir blok verildiğinden daha hızlı olmalıdır. Eğer ostringstreamdaha yavaş olduğu std::vectorek özellikler sağlamadan, daha sonra evet ben kırık derim.
Ben Voigt

1
@Ben Voigt: Tam tersine, vektörü yapmak için bir şey yapmak zorunda, o da bu yönde vektörü daha performans gösteren yapar. Vektör, bellekte bitişik olduğu garanti edilirken, ostringstream değil. Vektör, performans göstermek için tasarlanmış sınıflardan biridir, ancak ostringstream değildir.
Dragontamer5788

2
@Ben Voigt: stringbufDoğrudan kullanmak tüm işlev çağrılarını kaldırmayacak çünkü stringbufgenel arabirimi temel sınıftaki ortak sanal olmayan işlevlerden oluşuyor ve daha sonra türetilmiş sınıftaki korumalı sanal işleve gönderiliyor.
CB Bailey

2
@Charles: İyi bir derleyicide, genel işlev çağrısı, dinamik türün derleyici tarafından bilindiği bir bağlam içine gireceğinden, dolaylılığı kaldırabilir ve hatta bu çağrıları satır içine alabilir.
Ben Voigt

6
@Roddy: Bunların tüm satır içi şablon kodu olduğunu ve her derleme biriminde görülebildiğini düşünmeliyim. Ama sanırım bu uygulamaya göre değişebilir. Kesinlikle tartışılan sputnçağrının, sanal xsputnkorumayı çağıran kamu işlevinin satır içi olmasını beklerim. xsputnSatır içi olmasa bile , derleyici, satır içi konumdayken gereken sputntam xsputngeçersiz kılmayı belirleyebilir ve vtable'dan geçmeden doğrudan çağrı oluşturabilir.
Ben Voigt

1

Daha iyi performans elde etmek için kullandığınız kapların nasıl çalıştığını anlamanız gerekir. Char [] dizisi örneğinizde, gerekli boyuttaki dizi önceden atanır. Vektör ve ostringstream örneğinizde, nesneleri, nesne büyüdükçe defalarca tahsis etmeye ve yeniden tahsis etmeye ve muhtemelen birçok kez kopyalamaya zorlarsınız.

Std :: vector ile bu, char dizisini yaptığınız gibi vektörün boyutunu son boyuta başlatarak kolayca çözülür; bunun yerine sıfıra yeniden boyutlandırarak performansı haksız yere sakat ediyorsunuz! Bu neredeyse adil bir karşılaştırma değil.

Ostringstream ile ilgili olarak, alanın önceden konumlandırılması mümkün değildir, bunun uygunsuz bir kullanım olduğunu öneririm. Sınıf, basit bir char dizisinden çok daha büyük bir yardımcı programa sahiptir, ancak bu yardımcı programa ihtiyacınız yoksa, bunu kullanmayın, çünkü her durumda ek yükü ödersiniz. Bunun yerine, veriyi bir dizeye biçimlendirmek için iyi olan şey için kullanılmalıdır. C ++ çok çeşitli kaplar sağlar ve bir ostringstram bu amaç için en az uygun olanlardan biridir.

Vektör ve ostringstream durumunda, arabellek taşmasına karşı koruma elde edersiniz, bunu bir char dizisiyle elde edemezsiniz ve bu koruma ücretsiz gelmez.


1
Tahsisat, ostringstream için sorun gibi görünmüyor. Daha sonraki tekrarlamalar için sıfıra geri döner. Kesilme yok. Ayrıca denedim ostringstream.str.reserve(4000000)ve hiçbir fark yaratmadı.
Roddy

Bence ostringstream, bir kukla dize geçirerek "rezerv" olabilir, yani: ostringstream str(string(1000000 * sizeof(int), '\0'));ile vector, resizeherhangi bir boşluk dağıtmak değil, sadece gerektiğinde genişler.
Nim

1
msgstr "vektör .. arabellek taşmasına karşı koruma". Yaygın bir yanlış anlama - vector[]operatör varsayılan olarak sınır hataları için KONTROL EDİLMEZ. vector.at()ancak.
Roddy

2
vector<T>::resize(0)genellikle belleği yeniden tahsis etmez
Niki Yoshiuchi

2
@Roddy: Kullanmıyor operator[], ancak push_back()(bu arada back_inserter), kesinlikle taşma testi yapıyor. Kullanmayan başka bir sürüm eklendi push_back.
Ben Voigt
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.