C ++ 표준은 iostream의 성능 저하를 요구합니까, 아니면 구현이 좋지 않은 경우에만 처리합니까? 컴파일러 최적화)에 소비 된

C ++ 표준 라이브러리 iostream의 성능 저하에 대해 언급 할 때마다 불신의 물결에 부딪칩니다. 그러나 나는 iostream 라이브러리 코드 (전체 컴파일러 최적화)에 소비 된 많은 시간을 보여주는 프로파일 러 결과를 가지고 있으며 iostream에서 OS 특정 I / O API 및 사용자 정의 버퍼 관리로 전환하면 순서가 크게 향상됩니다.

C ++ 표준 라이브러리는 어떤 추가 작업을 수행하며 표준에 필요하며 실제로 유용합니까? 아니면 일부 컴파일러는 수동 버퍼 관리와 경쟁하는 iostream 구현을 제공합니까?

벤치 마크

문제를 해결하기 위해, 나는 iostreams 내부 버퍼링을 실행하는 몇 가지 짧은 프로그램을 작성했습니다.

점을 유의 ostringstream하고 stringbuf그들이 너무 느립니다 때문에 버전이 적은 반복을 실행합니다.

이데온에서는 + + ostringstream보다 약 3 배 느리고 원시 버퍼 보다 약 15 배 느립니다 . 실제 응용 프로그램을 사용자 지정 버퍼링으로 전환했을 때 프로파일 링 전후에 일관성이 있다고 생각합니다.std:copyback_inserterstd::vectormemcpy

이들은 모두 메모리 내 버퍼이므로 느린 디스크 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: 27ms
  • vector<char>back_inserter: 17.6 MS
  • vector<char> 일반 반복자와 함께 : 10.6 ms
  • vector<char> 반복자와 범위 검사 : 11.4ms
  • char[]: 3.7ms

내 랩톱 (Visual C ++ 2010 x86, cl /Ox /EHscWindows 7 Ultimate 64 비트, Intel Core i7, 8GB RAM)에서 :

  • ostringstream: 73.4 밀리 초, 71.6ms
  • stringbuf: 21.7ms, 21.3ms
  • vector<char>back_inserter: 34.6ms, 34.4ms
  • vector<char> 일반 반복자 사용시 : 1.10ms, 1.04ms
  • vector<char> 반복자와 경계 검사 : 1.11ms, 0.87ms, 1.12ms, 0.89ms, 1.02ms, 1.14ms
  • char[]: 1.48ms, 1.57ms

프로파일 활용 최적화와 비주얼 C ++ 2010 86, cl /Ox /EHsc /GL /c, link /ltcg:pgi, 실행 link /ltcg:pgo, 측정 :

  • ostringstream: 61.2ms, 60.5ms
  • vector<char> 일반 반복기 사용시 : 1.04ms, 1.03ms

cygwin gcc 4.3.4를 사용하는 동일한 노트북, 동일한 OS g++ -O3:

  • ostringstream: 62.7ms, 60.5ms
  • stringbuf: 44.4ms, 44.5ms
  • vector<char>back_inserter: 13.5ms, 13.6ms
  • vector<char> 일반 반복자와 함께 : 4.1ms, 3.9ms
  • vector<char> 반복자와 범위 검사 : 4.0ms, 4.0ms
  • char[]: 3.57ms, 3.75ms

동일한 랩톱, Visual C ++ 2008 SP1, cl /Ox /EHsc:

  • ostringstream: 88.7ms, 87.6ms
  • stringbuf: 23.3ms, 23.4ms
  • vector<char>back_inserter: 26.1 ms, 24.5 ms
  • vector<char> 일반 반복기 사용시 : 3.13ms, 2.48ms
  • vector<char> 반복자와 범위 검사 : 2.97ms, 2.53ms
  • char[]: 1.52ms, 1.25ms

동일한 랩톱, Visual C ++ 2010 64 비트 컴파일러 :

  • ostringstream: 48.6ms, 45.0ms
  • stringbuf: 16.2ms, 16.0ms
  • vector<char>back_inserter: 26.3 ms, 26.5 ms
  • vector<char> 일반 반복자 포함 : 0.87ms, 0.89ms
  • vector<char> 반복자 및 범위 검사 : 0.99ms, 0.99ms
  • char[]: 1.25ms, 1.24ms

편집 : 결과가 얼마나 일관성이 있는지 두 번 모두 실행했습니다. 꽤 일관된 IMO.

참고 : 랩탑에서, ideone이 허용하는 것보다 더 많은 CPU 시간을 절약 할 수 있으므로 모든 방법에 대해 반복 횟수를 1000으로 설정했습니다. 이 의미 ostringstreamvector첫 번째 패스에 일어난 재 할당, 최종 결과에 거의 영향이 있어야합니다.

편집 : 죄송합니다. vector-with-ordinary-iterator 에서 버그를 발견했습니다 . 반복자가 진행되지 않았으므로 캐시 적중이 너무 많습니다. 나는 어떻게 vector<char>성과 가 좋은지 궁금했다 char[]. VC ++ 2010 vector<char>보다 훨씬 빠르지 만 여전히 큰 차이는 없었습니다 char[].

결론

출력 스트림 버퍼링에는 데이터가 추가 될 때마다 3 단계가 필요합니다.

  • 들어오는 블록이 사용 가능한 버퍼 공간에 맞는지 확인하십시오.
  • 들어오는 블록을 복사하십시오.
  • 데이터 끝 포인터를 업데이트하십시오.

내가 게시 한 최신 코드 스 니펫 인 ” vector<char>simple iterator plus bounds check”는이를 수행 할뿐만 아니라 추가 공간을 할당하고 들어오는 블록이 맞지 않을 때 기존 데이터를 이동시킵니다. Clifford가 지적했듯이 파일 I / O 클래스의 버퍼링은 그렇게 할 필요가 없으며 현재 버퍼를 플러시하고 재사용합니다. 따라서 이것은 버퍼링 출력 비용의 상한이되어야합니다. 그리고 제대로 작동하는 인 메모리 버퍼를 만드는 데 필요한 것입니다.

그렇다면 왜 stringbufiideone에서 2.5 배가 느려지고 테스트 할 때 10 배 이상 느려 집니까? 이 간단한 마이크로 벤치 마크에서는 다형성으로 사용되지 않으므로 설명하지 않습니다.



답변

C ++ 성능에 관한 2006 기술 보고서 에는 IOStream에 대한 흥미로운 섹션이 있습니다 (p.68). 질문과 가장 관련이있는 부분은 6.1.2 절 ( “실행 속도”)에 있습니다.

IOStream 처리의 특정 측면이 여러 측면에 분산되어 있기 때문에 표준이 비효율적 인 구현을 요구하는 것으로 보입니다. 그러나 이것은 아닙니다. 어떤 형태의 전처리를 사용하면 많은 작업을 피할 수 있습니다. 일반적으로 사용되는 것보다 약간 더 똑똑한 링커를 사용하면 이러한 비 효율성을 제거 할 수 있습니다. 이것은 §6.2.3 및 §6.2.5에서 논의됩니다.

이 보고서는 2006 년에 작성된 이래로 많은 권장 사항이 현재 컴파일러에 통합되기를 희망하지만 실제로는 그렇지 않습니다.

언급했듯이 패싯이 등장하지 않을 수도 write()있지만 맹목적으로 가정하지는 않습니다. 그렇다면 기능은 무엇입니까? ostringstreamGCC로 컴파일 된 코드 에서 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::writegcc에서는보다 약 50 % 더 오래 걸리지 stringbuf::sputnstringbuf자체는 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 배열로는이를 얻지 못하며 보호 기능은 무료로 제공되지 않습니다.