C ++ 표준 라이브러리 iostream의 성능 저하에 대해 언급 할 때마다 불신의 물결에 부딪칩니다. 그러나 나는 iostream 라이브러리 코드 (전체 컴파일러 최적화)에 소비 된 많은 시간을 보여주는 프로파일 러 결과를 가지고 있으며 iostream에서 OS 특정 I / O API 및 사용자 정의 버퍼 관리로 전환하면 순서가 크게 향상됩니다.
C ++ 표준 라이브러리는 어떤 추가 작업을 수행하며 표준에 필요하며 실제로 유용합니까? 아니면 일부 컴파일러는 수동 버퍼 관리와 경쟁하는 iostream 구현을 제공합니까?
벤치 마크
문제를 해결하기 위해, 나는 iostreams 내부 버퍼링을 실행하는 몇 가지 짧은 프로그램을 작성했습니다.
- 바이너리 데이터를 http://ideone.com/2PPYw에 넣기
ostringstream
- 바이너리 데이터를
char[]
버퍼에 넣기 http://ideone.com/Ni5ct - http://ideone.com/Mj2Fi를
vector<char>
사용하여 이진 데이터 넣기back_inserter
- 새로운 기능 :
vector<char>
간단한 반복자 http://ideone.com/9iitv - 새로운 기능 : 바이너리 데이터를 http://ideone.com/qc9QA에 직접 삽입
stringbuf
- 새로운 기능 :
vector<char>
간단한 반복자 플러스 경계 확인 http://ideone.com/YyrKy
점을 유의 ostringstream
하고 stringbuf
그들이 너무 느립니다 때문에 버전이 적은 반복을 실행합니다.
이데온에서는 + + ostringstream
보다 약 3 배 느리고 원시 버퍼 보다 약 15 배 느립니다 . 실제 응용 프로그램을 사용자 지정 버퍼링으로 전환했을 때 프로파일 링 전후에 일관성이 있다고 생각합니다.std:copy
back_inserter
std::vector
memcpy
이들은 모두 메모리 내 버퍼이므로 느린 디스크 I / O, 너무 많은 플러시, stdio와의 동기화 또는 사람들이 C ++ 표준 라이브러리의 느려진 관찰을 변명하기 위해 사용하는 다른 것들에서 iostream의 느림을 비난 할 수 없습니다 요오드.
다른 시스템의 벤치 마크와 일반적인 구현이 수행하는 작업 (gcc의 libc ++, Visual C ++, Intel C ++ 등)과 표준에서 요구하는 오버 헤드의 양에 대한 주석을 보는 것이 좋을 것입니다.
이 테스트의 근거
많은 사람들이 iostream이 포맷 된 출력에 더 일반적으로 사용된다고 올바르게 지적했습니다. 그러나 이진 파일 액세스를 위해 C ++ 표준에서 제공하는 유일한 최신 API이기도합니다. 그러나 내부 버퍼링에서 성능 테스트를 수행하는 실제 이유는 일반적인 형식의 I / O에 적용됩니다. iostream이 디스크 컨트롤러에 원시 데이터를 제공 할 수없는 경우 포맷을 담당 할 때 어떻게 유지할 수 있습니까?
벤치 마크 타이밍
이것들은 모두 외부 ( k
) 루프의 반복입니다.
ideone (gcc-4.3.4, 알려지지 않은 OS 및 하드웨어) :
ostringstream
: 53 밀리 초stringbuf
: 27msvector<char>
과back_inserter
: 17.6 MSvector<char>
일반 반복자와 함께 : 10.6 msvector<char>
반복자와 범위 검사 : 11.4mschar[]
: 3.7ms
내 랩톱 (Visual C ++ 2010 x86, cl /Ox /EHsc
Windows 7 Ultimate 64 비트, Intel Core i7, 8GB RAM)에서 :
ostringstream
: 73.4 밀리 초, 71.6msstringbuf
: 21.7ms, 21.3msvector<char>
및back_inserter
: 34.6ms, 34.4msvector<char>
일반 반복자 사용시 : 1.10ms, 1.04msvector<char>
반복자와 경계 검사 : 1.11ms, 0.87ms, 1.12ms, 0.89ms, 1.02ms, 1.14mschar[]
: 1.48ms, 1.57ms
프로파일 활용 최적화와 비주얼 C ++ 2010 86, cl /Ox /EHsc /GL /c
, link /ltcg:pgi
, 실행 link /ltcg:pgo
, 측정 :
ostringstream
: 61.2ms, 60.5msvector<char>
일반 반복기 사용시 : 1.04ms, 1.03ms
cygwin gcc 4.3.4를 사용하는 동일한 노트북, 동일한 OS g++ -O3
:
ostringstream
: 62.7ms, 60.5msstringbuf
: 44.4ms, 44.5msvector<char>
및back_inserter
: 13.5ms, 13.6msvector<char>
일반 반복자와 함께 : 4.1ms, 3.9msvector<char>
반복자와 범위 검사 : 4.0ms, 4.0mschar[]
: 3.57ms, 3.75ms
동일한 랩톱, Visual C ++ 2008 SP1, cl /Ox /EHsc
:
ostringstream
: 88.7ms, 87.6msstringbuf
: 23.3ms, 23.4msvector<char>
및back_inserter
: 26.1 ms, 24.5 msvector<char>
일반 반복기 사용시 : 3.13ms, 2.48msvector<char>
반복자와 범위 검사 : 2.97ms, 2.53mschar[]
: 1.52ms, 1.25ms
동일한 랩톱, Visual C ++ 2010 64 비트 컴파일러 :
ostringstream
: 48.6ms, 45.0msstringbuf
: 16.2ms, 16.0msvector<char>
및back_inserter
: 26.3 ms, 26.5 msvector<char>
일반 반복자 포함 : 0.87ms, 0.89msvector<char>
반복자 및 범위 검사 : 0.99ms, 0.99mschar[]
: 1.25ms, 1.24ms
편집 : 결과가 얼마나 일관성이 있는지 두 번 모두 실행했습니다. 꽤 일관된 IMO.
참고 : 랩탑에서, ideone이 허용하는 것보다 더 많은 CPU 시간을 절약 할 수 있으므로 모든 방법에 대해 반복 횟수를 1000으로 설정했습니다. 이 의미 ostringstream
와 vector
첫 번째 패스에 일어난 재 할당, 최종 결과에 거의 영향이 있어야합니다.
편집 : 죄송합니다. vector
-with-ordinary-iterator 에서 버그를 발견했습니다 . 반복자가 진행되지 않았으므로 캐시 적중이 너무 많습니다. 나는 어떻게 vector<char>
성과 가 좋은지 궁금했다 char[]
. VC ++ 2010 vector<char>
보다 훨씬 빠르지 만 여전히 큰 차이는 없었습니다 char[]
.
결론
출력 스트림 버퍼링에는 데이터가 추가 될 때마다 3 단계가 필요합니다.
- 들어오는 블록이 사용 가능한 버퍼 공간에 맞는지 확인하십시오.
- 들어오는 블록을 복사하십시오.
- 데이터 끝 포인터를 업데이트하십시오.
내가 게시 한 최신 코드 스 니펫 인 ” vector<char>
simple iterator plus bounds check”는이를 수행 할뿐만 아니라 추가 공간을 할당하고 들어오는 블록이 맞지 않을 때 기존 데이터를 이동시킵니다. Clifford가 지적했듯이 파일 I / O 클래스의 버퍼링은 그렇게 할 필요가 없으며 현재 버퍼를 플러시하고 재사용합니다. 따라서 이것은 버퍼링 출력 비용의 상한이되어야합니다. 그리고 제대로 작동하는 인 메모리 버퍼를 만드는 데 필요한 것입니다.
그렇다면 왜 stringbuf
iideone에서 2.5 배가 느려지고 테스트 할 때 10 배 이상 느려 집니까? 이 간단한 마이크로 벤치 마크에서는 다형성으로 사용되지 않으므로 설명하지 않습니다.
답변
C ++ 성능에 관한 2006 기술 보고서 에는 IOStream에 대한 흥미로운 섹션이 있습니다 (p.68). 질문과 가장 관련이있는 부분은 6.1.2 절 ( “실행 속도”)에 있습니다.
IOStream 처리의 특정 측면이 여러 측면에 분산되어 있기 때문에 표준이 비효율적 인 구현을 요구하는 것으로 보입니다. 그러나 이것은 아닙니다. 어떤 형태의 전처리를 사용하면 많은 작업을 피할 수 있습니다. 일반적으로 사용되는 것보다 약간 더 똑똑한 링커를 사용하면 이러한 비 효율성을 제거 할 수 있습니다. 이것은 §6.2.3 및 §6.2.5에서 논의됩니다.
이 보고서는 2006 년에 작성된 이래로 많은 권장 사항이 현재 컴파일러에 통합되기를 희망하지만 실제로는 그렇지 않습니다.
언급했듯이 패싯이 등장하지 않을 수도 write()
있지만 맹목적으로 가정하지는 않습니다. 그렇다면 기능은 무엇입니까? ostringstream
GCC로 컴파일 된 코드 에서 GProf를 실행 하면 다음과 같은 고장이 발생합니다.
- 44.23 %에서
std::basic_streambuf<char>::xsputn(char const*, int)
- 34.62 %
std::ostream::write(char const*, int)
- 12.50 %
main
- 6.73 %에서
std::ostream::sentry::sentry(std::ostream&)
- 0.96 %에서
std::string::_M_replace_safe(unsigned int, unsigned int, char const*, unsigned int)
- 0.96 %에서
std::basic_ostringstream<char>::basic_ostringstream(std::_Ios_Openmode)
- 0.00 %에서
std::fpos<int>::fpos(long long)
따라서 많은 시간이 소요됩니다 xsputn
. 결국 std::copy()
커서 위치와 버퍼를 많이 확인하고 업데이트 한 후에 호출 c++\bits\streambuf.tcc
합니다 (자세한 내용을 살펴보십시오 ).
이것에 대한 나의 취지는 최악의 상황에 집중했다는 것입니다. 합리적으로 많은 양의 데이터를 처리하는 경우 수행되는 모든 검사는 전체 작업의 일부에 불과합니다. 그러나 코드는 한 번에 4 바이트 단위로 데이터를 이동하고 있으며 매번 추가 비용이 발생합니다. 실제 상황에서는 그렇게하지 않는 것이 분명합니다 write
. 1 int에서 1m 번이 아닌 1m int의 배열 에서 페널티 가 호출 된 경우 페널티가 얼마나 무시할 수 있을지 고려하십시오 . 그리고 실제 상황에서 IOStream의 중요한 기능, 즉 메모리 안전 및 유형 안전 설계에 진심으로 감사합니다. 이러한 이점은 대가를 치르며 이러한 비용이 실행 시간을 지배하게하는 테스트를 작성했습니다.
답변
오히려 Visual Studio 사용자에게 실망했습니다.
- 의 Visual Studio 구현
ostream
에서sentry
객체 (표준에streambuf
필요함)는 (필수 아님)을 보호하는 중요한 섹션으로 들어갑니다 . 이것은 선택 사항이 아닌 것처럼 보이므로 단일 스레드에서 사용하는 로컬 스트림에 대해서도 스레드 동기화 비용을 지불하므로 동기화가 필요하지 않습니다.
ostringstream
메시지를 매우 심각하게 형식화 하는 데 사용 되는 코드가 손상됩니다. stringbuf
직접을 사용 하면의 사용을 피할 수 sentry
있지만 형식이 지정된 삽입 연산자는에서 직접 작업 할 수 없습니다 streambuf
. Visual C ++ 2010의 경우 중요한 섹션이 ostringstream::write
기본 stringbuf::sputn
호출에 비해 3 배 느려 집니다.
보면 newlib에에 beldaz의 프로파일 데이터 , GCC의는 분명 것 sentry
같은 미친 아무것도하지 않습니다. ostringstream::write
gcc에서는보다 약 50 % 더 오래 걸리지 stringbuf::sputn
만 stringbuf
자체는 VC ++보다 훨씬 느립니다. 그리고 둘 다 여전히 vector<char>
VC ++에서와 같은 마진은 아니지만 I / O 버퍼링 을 사용하는 것과 비교하여 매우 바람직하지 않습니다.
답변
문제는 write ()를 호출 할 때마다 오버 헤드가 발생한다는 것입니다. 추가하는 각 추상화 레벨 (char []-> vector-> string-> ostringstream)은 함수 호출 / 반환과 백만 번 호출하면 추가하는 기타 관리 용 guff를 추가합니다.
한 번에 10 개의 정수를 쓰도록 ideone의 두 가지 예를 수정했습니다. ostringstream 시간은 53ms에서 6ms (거의 10 배 향상)가되었지만 char 루프는 개선 되었으나 (3.7에서 1.5) 유용하지만 2 배만 향상되었습니다.
성능이 염려된다면 작업에 적합한 도구를 선택해야합니다. ostringstream은 유용하고 유연하지만 원하는 방식으로 사용하면 위약금이 부과됩니다. char []는 더 어려운 작업이지만 성능 향상은 클 수 있습니다 (gcc가 아마도 memcpys를 인라인 할 것임을 기억하십시오).
요컨대 ostringstream은 깨지지 않지만 금속에 가까울수록 코드가 더 빨리 실행됩니다. 어셈블러는 여전히 일부 사람들에게 이점이 있습니다.
답변
더 나은 성능을 얻으려면 사용중인 컨테이너의 작동 방식을 이해해야합니다. char [] 배열 예제에서 필요한 크기의 배열이 미리 할당됩니다. 벡터 및 ostringstream 예제에서는 객체가 증가함에 따라 객체를 반복적으로 할당 및 재 할당하고 데이터를 여러 번 복사하도록합니다.
std :: vector를 사용하면 char 배열을 수행했을 때 벡터 크기를 최종 크기로 초기화하여 쉽게 해결할 수 있습니다. 대신 0으로 크기를 조정하여 불공평하게 성능을 저하시킵니다! 이것은 공정한 비교가 아닙니다.
ostringstream과 관련하여 공간을 사전 할당하는 것은 불가능합니다. 나는 그것이 부적절한 사용이라고 제안합니다. 클래스는 간단한 char 배열보다 훨씬 큰 유틸리티를 가지고 있지만, 그 유틸리티가 필요하지 않으면 사용하지 마십시오. 어쨌든 오버 헤드가 발생하기 때문입니다. 대신 데이터를 문자열로 형식화하는 것이 좋은 용도로 사용해야합니다. C ++은 광범위한 컨테이너를 제공하며 ostringstram은이 목적에 가장 적합하지 않습니다.
벡터 및 ostringstream의 경우 버퍼 오버런으로부터 보호를 받고 char 배열로는이를 얻지 못하며 보호 기능은 무료로 제공되지 않습니다.