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.
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. I std::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 do stringbuf
. Te ostream
zajęcia są przypuszczać, aby związać ze sobą narodowe świadomy funkcjonalność formatowania z elastycznego wyboru bufora (plik, łańcuch, itp) przez rdbuf()
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
do fprintf
podczas wysyłania informacji rejestracyjnych dotyczących podwójnych. MSVC 2008 na WinXPsp3. iostreams jest po prostu psie.