Za każdym razem, gdy wspominam powolne działanie standardowych bibliotek i +++ C ++, spotykam się z falą niedowierzania. Mam jednak wyniki profilowania pokazujące dużą ilość czasu spędzonego na kodzie biblioteki iostream (pełne optymalizacje kompilatora), a przełączanie z iostreams na interfejsy API I / O specyficzne dla systemu operacyjnego i niestandardowe zarządzanie buforami daje pewien wzrost wielkości.
Jaką dodatkową pracę wykonuje standardowa biblioteka C ++, czy jest wymagana przez standard i czy jest przydatna w praktyce? Czy też niektóre kompilatory zapewniają implementacje iostreams, które są konkurencyjne w stosunku do ręcznego zarządzania buforami?
Benchmarki
Aby poruszyć sprawy, napisałem kilka krótkich programów do wykonywania wewnętrznego buforowania iostreams:
- umieszczanie danych binarnych w
ostringstream
http://ideone.com/2PPYw - umieszczanie danych binarnych w
char[]
buforze http://ideone.com/Ni5ct - wprowadzanie danych binarnych za
vector<char>
pomocąback_inserter
http://ideone.com/Mj2Fi - NOWOŚĆ :
vector<char>
prosty iterator http://ideone.com/9iitv - NOWOŚĆ : umieszczanie danych binarnych bezpośrednio na stronie
stringbuf
http://ideone.com/qc9QA - NOWOŚĆ :
vector<char>
prosty iterator plus granice sprawdź http://ideone.com/YyrKy
Zauważ, że wersje ostringstream
i stringbuf
uruchamiają mniej iteracji, ponieważ są one znacznie wolniejsze.
Na ideonie ostringstream
jest około 3 razy wolniejszy niż std:copy
+ back_inserter
+ std::vector
i około 15 razy wolniejszy niż memcpy
w surowym buforze. Jest to spójne z profilowaniem przed i po, kiedy zmieniłem moją prawdziwą aplikację na niestandardowe buforowanie.
Są to wszystkie bufory pamięci, więc powolności iostreamów nie można winić za powolne operacje we / wy dysku, zbyt duże opróżnianie, synchronizację ze standardem lub jakiekolwiek inne rzeczy, których ludzie używają, aby usprawiedliwić spowolnienie standardowej biblioteki C ++ iostream.
Byłoby miło zobaczyć wyniki testów porównawczych na innych systemach i komentarze na temat czynności wykonywanych przez popularne implementacje (takich jak libc ++ gcc, Visual C ++, Intel C ++) oraz na ile narzutu narzuca norma.
Uzasadnienie tego testu
Wiele osób słusznie zauważyło, że iostreamy są częściej używane do sformatowanego wyjścia. Są to jednak jedyne nowoczesne interfejsy API zapewniane przez standard C ++ do dostępu do plików binarnych. Ale prawdziwy powód przeprowadzania testów wydajności na wewnętrznym buforowaniu dotyczy typowych sformatowanych operacji we / wy: jeśli iostreams nie może zapewnić, że kontroler dysku jest dostarczany z surowymi danymi, to jak mogą nadążyć, gdy są również odpowiedzialni za formatowanie?
Benchmark Timing
Wszystko to na iterację zewnętrznej ( k
) pętli.
W systemie ideone (gcc-4.3.4, nieznany system operacyjny i sprzęt):
ostringstream
: 53 milisekundystringbuf
: 27 msvector<char>
iback_inserter
: 17,6 msvector<char>
ze zwykłym iteratorem: 10,6 msvector<char>
sprawdzanie iteratora i granic: 11,4 mschar[]
: 3,7 ms
Na moim laptopie (Visual C ++ 2010 x86, cl /Ox /EHsc
Windows 7 Ultimate 64-bit, Intel Core i7, 8 GB RAM):
ostringstream
: 73,4 milisekund, 71,6 msstringbuf
: 21,7 ms, 21,3 msvector<char>
iback_inserter
: 34,6 ms, 34,4 msvector<char>
ze zwykłym iteratorem: 1,10 ms, 1,04 msvector<char>
iterator i sprawdzanie granic: 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, z profilu Guided Optimization cl /Ox /EHsc /GL /c
, link /ltcg:pgi
, biegać, link /ltcg:pgo
zmierz:
ostringstream
: 61,2 ms, 60,5 msvector<char>
ze zwykłym iteratorem: 1,04 ms, 1,03 ms
Ten sam laptop, ten sam system operacyjny, używając cygwin gcc 4.3.4 g++ -O3
:
ostringstream
: 62,7 ms, 60,5 msstringbuf
: 44,4 ms, 44,5 msvector<char>
iback_inserter
: 13,5 ms, 13,6 msvector<char>
ze zwykłym iteratorem: 4,1 ms, 3,9 msvector<char>
sprawdzanie iteratora i granic: 4,0 ms, 4,0 mschar[]
: 3,57 ms, 3,75 ms
Sam laptop, Visual C ++ 2008 SP1 cl /Ox /EHsc
:
ostringstream
: 88,7 ms, 87,6 msstringbuf
: 23,3 ms, 23,4 msvector<char>
iback_inserter
: 26,1 ms, 24,5 msvector<char>
ze zwykłym iteratorem: 3,13 ms, 2,48 msvector<char>
sprawdzanie iteratora i granic: 2,97 ms, 2,53 mschar[]
: 1,52 ms, 1,25 ms
Ten sam laptop, 64-bitowy kompilator Visual C ++ 2010:
ostringstream
: 48,6 ms, 45,0 msstringbuf
: 16,2 ms, 16,0 msvector<char>
iback_inserter
: 26,3 ms, 26,5 msvector<char>
ze zwykłym iteratorem: 0,87 ms, 0,89 msvector<char>
sprawdzanie iteratora i granic: 0,99 ms, 0,99 mschar[]
: 1,25 ms, 1,24 ms
EDYCJA: Przebiegł wszystko dwa razy, aby zobaczyć, jak spójne były wyniki. Dość spójny IMO.
UWAGA: Na moim laptopie, ponieważ mogę zaoszczędzić więcej czasu procesora niż pozwala ideone, ustawiłem liczbę iteracji na 1000 dla wszystkich metod. Oznacza to, że ostringstream
i vector
realokacja, która odbywa się tylko na pierwszym przejeździe, powinny mieć niewielki wpływ na końcowe wyniki.
EDYCJA: Ups, znaleziono błąd w vector
-w zwykłym-iteratorze, iterator nie był zaawansowany i dlatego było zbyt wiele trafień w pamięci podręcznej. Zastanawiałem się, jak radził sobie vector<char>
lepiej char[]
. Nie miało to jednak większego znaczenia, vector<char>
wciąż jest szybsze niż char[]
w VC ++ 2010.
Wnioski
Buforowanie strumieni wyjściowych wymaga trzech kroków przy każdym dodawaniu danych:
- Sprawdź, czy przychodzący blok pasuje do dostępnej przestrzeni bufora.
- Skopiuj przychodzący blok.
- Zaktualizuj wskaźnik końca danych.
Najnowszy fragment kodu, który opublikowałem, „ vector<char>
prosty iterator plus sprawdzanie granic” nie tylko robi to, ale także przydziela dodatkową przestrzeń i przenosi istniejące dane, gdy nadchodzący blok nie pasuje. Jak zauważył Clifford, buforowanie w klasie I / O pliku nie musiałoby tego robić, wystarczyło opróżnić bieżący bufor i użyć go ponownie. Powinno to stanowić górną granicę kosztu buforowania danych wyjściowych. I to jest dokładnie to, czego potrzeba, aby stworzyć działający bufor w pamięci.
Dlaczego więc stringbuf
2,5 razy wolniej działa na ideone, a co najmniej 10 razy wolniej, gdy go testuję? Nie jest on używany polimorficznie w tym prostym mikroprocesorze, więc to nie wyjaśnia.
źródło
std::ostringstream
nie jest wystarczająco inteligentny, aby wykładniczo zwiększyć rozmiar bufora,std::vector
robi to (A) głupie i (B) coś, co ludzie myślą o wydajności I / O, powinni pomyśleć. W każdym razie bufor jest ponownie wykorzystywany, nie jest ponownie przydzielany za każdym razem. Istd::vector
używa również dynamicznie rosnącego bufora. Staram się być tutaj sprawiedliwy.ostringstream
i chcesz uzyskać jak najszybszą wydajność, powinieneś rozważyć przejście od razu dostringbuf
. Teostream
zajęcia są przypuszczać, aby związać ze sobą narodowe świadomy funkcjonalność formatowania z elastycznego wyboru bufora (plik, łańcuch, itp) przezrdbuf()
a jego interfejs funkcji wirtualnej. Jeśli nie wykonujesz żadnego formatowania, ten dodatkowy poziom pośredni z pewnością będzie wyglądał proporcjonalnie drogo w porównaniu z innymi podejściami.ofstream
dofprintf
podczas wysyłania informacji rejestracyjnych dotyczących podwójnych. MSVC 2008 na WinXPsp3. iostreams jest po prostu psie.Odpowiedzi:
Nie odpowiadając na specyfikę twojego pytania tak bardzo jak tytuł: Raport techniczny z 2006 roku na temat wydajności C ++ zawiera interesującą sekcję dotyczącą IOStreams (str. 68). Najbardziej odpowiedni dla twojego pytania znajduje się w Rozdziale 6.1.2 („Szybkość wykonania”):
Ponieważ raport został napisany w 2006 roku, można mieć nadzieję, że wiele zaleceń zostanie uwzględnionych w obecnych kompilatorach, ale być może tak nie jest.
Jak wspomniałeś, aspekty mogą się nie pojawiać
write()
(ale nie zakładam tego na ślepo). Więc co zawiera? Uruchomienie GProf naostringstream
kodzie skompilowanym za pomocą GCC daje następujący podział: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)
Spędza się więc większość czasu
xsputn
, który ostatecznie wywołujestd::copy()
po wielu sprawdzeniach i aktualizacjach pozycji kursora i buforów (sprawdźc++\bits\streambuf.tcc
szczegóły).Uważam, że skupiłeś się na najgorszej sytuacji. Wszystkie przeprowadzone kontrole byłyby niewielkim ułamkiem całkowitej pracy wykonanej, gdybyś miał do czynienia z dość dużymi fragmentami danych. Ale twój kod przesuwa dane o cztery bajty na raz i za każdym razem ponosi dodatkowe koszty. Najwyraźniej należałoby tego uniknąć w rzeczywistej sytuacji - zastanów się, jak mało znacząca byłaby kara, gdyby
write
został wywołany na tablicy 1 mln int zamiast na 1 mln razy na jedną int. A w rzeczywistej sytuacji naprawdę docenilibyśmy ważne cechy IOStreams, a mianowicie bezpieczną pamięć i konstrukcję. Takie korzyści mają swoją cenę, a ty napisałeś test, który sprawia, że koszty te dominują w czasie realizacji.źródło
ostream::write()
.sizeof i
, ale wszystkie kompilatory, z którymi testuję, mają 4 bajtyint
). I nie wydaje mi się to wcale nierealistyczne, jak myślisz, jakie fragmenty wielkości są przekazywane w każdym wywołaniuxsputn
w typowym kodziestream << "VAR: " << var.x << ", " << var.y << endl;
.xsputn
pięciokrotnie, równie dobrze może znajdować się w pętli, która zapisuje 10 milionów plików linii. Przekazywanie danych do iostreams w dużych porcjach to o wiele mniej rzeczywisty scenariusz niż mój kod testowy. Dlaczego powinienem pisać do buforowanego strumienia przy minimalnej liczbie połączeń? Jeśli muszę zrobić własne buforowanie, jaki jest sens iostreams? A w przypadku danych binarnych mam opcję samodzielnego buforowania, gdy piszę miliony cyfr do pliku tekstowego, opcja zbiorcza po prostu nie istnieje, muszę zadzwonićoperator <<
do każdego z nich.Jestem raczej rozczarowany użytkownikami Visual Studio, którzy raczej się na tym dali:
ostream
Thesentry
obiektu (co jest wymagane przez standard) wchodzi do sekcji krytycznej chroniącstreambuf
(co nie jest wymagane). Nie wydaje się to opcjonalne, więc płacisz za synchronizację wątków nawet za strumień lokalny używany przez pojedynczy wątek, który nie wymaga synchronizacji.Uszkadza to kod, który
ostringstream
dość poważnie używa do formatowania wiadomości. Użyciestringbuf
bezpośredniego pozwala uniknąć użyciasentry
, ale sformatowane operatory wstawiania nie mogą działać bezpośrednio nastreambuf
s. W Visual C ++ 2010 sekcja krytyczna zwalniaostringstream::write
trzykrotnie w porównaniu dostringbuf::sputn
wywołania bazowego .Patrząc na dane profilera beldaz na newlib , wydaje się jasne, że gcc
sentry
nie robi nic takiego szalonego.ostringstream::write
pod gcc zajmuje tylko około 50% dłużej niżstringbuf::sputn
, alestringbuf
sam jest znacznie wolniejszy niż pod VC ++. I oba nadal bardzo niekorzystnie porównują się do korzystania zvector<char>
buforowania dla operacji we / wy, chociaż nie o taki sam margines jak w VC ++.źródło
sentry
... „Wartownik klasy definiuje klasę, która jest odpowiedzialna za wykonywanie wyjątkowo bezpiecznych operacji na prefiksach i sufiksach”. oraz notatkę „Konstruktor wartowników i destruktor mogą także wykonywać dodatkowe operacje zależne od implementacji”. Można również przypuszczać, że z zasady C ++ „nie płacisz za to, czego nie używasz”, komitet C ++ nigdy nie zatwierdzi tak marnotrawnego wymogu. Ale nie krępuj się zadać pytanie dotyczące bezpieczeństwa wątku iostream.Problem, który widzisz, jest narzutem wokół każdego wywołania write (). Każdy dodany poziom abstrakcji (char [] -> vector -> string -> ostringstream) dodaje jeszcze kilka wywołań funkcji / zwrotów i inne funkcje porządkowe, które - jeśli wywołasz to milion razy - sumują się.
Zmodyfikowałem dwa przykłady na ideone, aby napisać dziesięć liczb na raz. Czas ostringstream wyniósł od 53 do 6 ms (prawie 10-krotna poprawa), podczas gdy pętla char poprawiła się (3,7 do 1,5) - użyteczne, ale tylko dwa razy.
Jeśli obawiasz się o wydajność, musisz wybrać odpowiednie narzędzie do pracy. Ostringstream jest użyteczny i elastyczny, ale za używanie go w sposób, w jaki próbujesz, grozi kara. char [] to trudniejsza praca, ale wzrost wydajności może być świetny (pamiętaj, że gcc prawdopodobnie wstawi również memcpys dla ciebie).
Krótko mówiąc, ostringstream nie jest zepsuty, ale im bardziej zbliżasz się do metalu, tym szybciej działa Twój kod. Asembler nadal ma zalety dla niektórych ludzi.
źródło
ostringstream::write()
musi to zrobić, żevector::push_back()
nie? Jeśli już, to powinno być szybsze, ponieważ przekazał blok zamiast czterech pojedynczych elementów. Jeśliostringstream
jest wolniejszy niżstd::vector
bez żadnych dodatkowych funkcji, to tak nazwałbym to zepsute.stringbuf
Bezpośrednie użycie nie spowoduje usunięcia wszystkich wywołań funkcji, ponieważstringbuf
publiczny interfejs składa się z publicznych nie-wirtualnych funkcji w klasie bazowej, które następnie wysyłają do chronionej funkcji wirtualnej w klasie pochodnej.sputn
funkcja publiczna, która wywołuje funkcję wirtualnej ochronyxsputn
, zostanie podkreślona. Nawet jeślixsputn
nie jest wbudowanysputn
, kompilator może podczas wstawiania określić dokładnexsputn
wymagane zastąpienie i wygenerować bezpośrednie wywołanie bez przechodzenia przez vtable.Aby uzyskać lepszą wydajność, musisz zrozumieć, w jaki sposób działają używane pojemniki. W przykładzie tablicy char [] tablica o wymaganym rozmiarze jest przydzielana z góry. W przykładzie wektorowym i ostringstream wymuszasz na obiektach wielokrotne przydzielanie, realokację i ewentualnie kopiowanie danych wiele razy w miarę wzrostu obiektu.
W przypadku std :: vector można to łatwo rozwiązać, inicjując rozmiar wektora do ostatecznego rozmiaru, podobnie jak w przypadku tablicy znaków; zamiast tego raczej niesprawiedliwie paraliżujesz wydajność, zmieniając rozmiar na zero! To nie jest sprawiedliwe porównanie.
Jeśli chodzi o ostringstream, wstępna alokacja przestrzeni nie jest możliwa, sugerowałbym, że jest to niewłaściwe zastosowanie. Klasa ma znacznie większą użyteczność niż zwykła tablica znaków, ale jeśli nie potrzebujesz tego narzędzia, nie używaj go, ponieważ w każdym razie zapłacisz koszty ogólne. Zamiast tego należy go użyć do tego, do czego jest dobry - formatowanie danych w łańcuch. C ++ zapewnia szeroką gamę pojemników, a ostringstram należy do najmniej odpowiednich do tego celu.
W przypadku wektora i ostringstream otrzymujesz ochronę przed przepełnieniem bufora, nie dostajesz tego z tablicą char, a ta ochrona nie jest bezpłatna.
źródło
ostringstream.str.reserve(4000000)
i to nie miało znaczenia.ostringstream
można „zarezerwować”, przekazując fikcyjny ciąg, tzn .:ostringstream str(string(1000000 * sizeof(int), '\0'));
Za pomocąvector
,resize
nie zwalnia żadnej przestrzeni, rozszerza się tylko w razie potrzeby.vector[]
operator zazwyczaj NIE jest sprawdzany pod kątem błędów granic.vector.at()
jest jednak.vector<T>::resize(0)
zwykle nieoperator[]
, alepush_back()
(przy okazjiback_inserter
), które zdecydowanie testują przepełnienie. Dodano kolejną wersję, która nie używapush_back
.