C ++ 표준은 iostream의 성능 저하를 요구합니까, 아니면 구현이 좋지 않은 경우에만 처리합니까?


197

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 배 이상 느려 집니까? 이 간단한 마이크로 벤치 마크에서는 다형성으로 사용되지 않으므로 설명하지 않습니다.


24
한 번에 백만 개의 문자를 쓰고 사전 할당 된 버퍼에 복사하는 것보다 왜 느린 지 궁금하십니까?
아논.

20
@Anon : 한 번에 4 백만 바이트를 버퍼링하고 있는데 왜 느린 지 궁금합니다. 경우 std::ostringstream기하 급수적으로 버퍼 크기 길 증가에 스마트 것만으로는 충분하지 않습니다 std::vectorI 생각의 (A) 바보와 (B) 어떤 사람들은 / O 성능에 대해 생각해야하지가. 어쨌든 버퍼는 재사용되며 매번 재 할당되지는 않습니다. 또한 std::vector동적으로 증가하는 버퍼를 사용하고 있습니다. 나는 여기서 공정하게 노력하고 있습니다.
Ben Voigt

14
실제로 어떤 작업을 벤치마킹하려고합니까? 의 서식 기능을 사용하지 않고 ostringstream최대한 빠른 성능을 원한다면로 이동하는 것이 stringbuf좋습니다. ostream클래스를 통해 유연한 버퍼의 선택 (파일, 캐릭터 등)와 함께 로케일 포맷인지 기능을 묶어 생각되어 rdbuf()그 가상 함수 인터페이스. 서식을 지정하지 않으면 추가 접근 수준이 다른 접근 방식에 비해 비례 적으로 비싸게 보일 것입니다.
CB Bailey

5
진실 op +1 우리는 복식과 관련된 로깅 정보를 출력 ofstreamfprintf때로 이동하여 질서 또는 규모가 빨라졌습니다 . WinXPsp3의 MSVC 2008. iostreams는 개가 느립니다.
KitsuneYMG

6
다음은위원회 사이트에 대한 몇 가지 테스트입니다. open-std.org/jtc1/sc22/wg21/docs/D_5.cpp
Johannes Schaub-litb

답변:


49

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의 중요한 기능, 즉 메모리 안전 및 유형 안전 설계에 진심으로 감사합니다. 이러한 이점은 대가를 치르며 이러한 비용이 실행 시간을 지배하게하는 테스트를 작성했습니다.


아마도 곧 물어볼 iostream의 형식화 된 삽입 / 추출 성능에 대한 미래의 질문에 대한 훌륭한 정보처럼 들립니다. 그러나에 관련된 패싯이 없다고 생각 ostream::write()합니다.
Ben Voigt

4
프로파일 링을 위해 +1 (리눅스 머신이라고 생각합니까?). 그러나 실제로 한 번에 4 바이트를 추가하고 있습니다 (실제로 sizeof i테스트하고있는 모든 컴파일러에는 4 바이트가 있습니다 int). 그리고 나에게 모든 것을 비현실적하지 않는 것, 어떤 크기의 덩어리는 각 호출에서 전달받을 생각 하는가 xsputn와 같은 전형적인 코드 stream << "VAR: " << var.x << ", " << var.y << endl;.
Ben Voigt

39
@beldaz : xsputn5 번만 호출하는 "일반적인"코드 예제 는 1 천만 줄 파일을 작성하는 루프 안에있을 수 있습니다. 큰 청크로 데이터를 iostream으로 전달하는 것은 벤치 마크 코드보다 실제 시나리오보다 훨씬 적습니다. 최소 통화 수로 버퍼링 된 스트림 에 작성해야하는 이유는 무엇 입니까? 자체 버퍼링을 수행해야하는 경우 어쨌든 iostream의 요점은 무엇입니까? 이진 데이터를 사용하면 텍스트 파일에 수백만 개의 숫자를 쓸 때 대량 옵션이 존재하지 않으므로 직접 버퍼링하는 옵션이 있습니다. 각 옵션을 호출 operator <<해야합니다.
Ben Voigt

1
@beldaz : 간단한 계산으로 I / O가 지배하기 시작하는시기를 추정 할 수 있습니다. 현재 소비자 등급 하드 디스크의 일반적인 90MB / s 평균 쓰기 속도에서 4MB 버퍼를 플러시하는 데 <45ms가 소요됩니다 (처리량, 대기 시간은 OS 쓰기 캐시로 인해 중요하지 않음). 내부 루프를 실행하는 것이 버퍼를 채우는 것보다 오래 걸리면 CPU가 제한 요소가됩니다. 내부 루프가 더 빨리 실행되면 I / O가 제한 요소가되거나 실제 작업을 수행하는 데 약간의 CPU 시간이 남아 있습니다.
Ben Voigt

5
물론, 이것이 iostream을 사용한다는 것이 반드시 느린 프로그램을 의미한다는 것을 의미하지는 않습니다. I / O가 프로그램의 아주 작은 부분이라면 성능이 좋지 않은 I / O 라이브러리를 사용하는 것이 전체적으로 큰 영향을 미치지는 않습니다. 그러나 문제가 될 정도로 자주 호출되지 않는 것은 좋은 성능과 같지 않으며 I / O가 많은 응용 프로그램에서는 중요합니다.
Ben Voigt

27

오히려 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 버퍼링 을 사용하는 것과 비교하여 매우 바람직하지 않습니다.


이 정보가 여전히 최신 정보입니까? GCC와 함께 제공되는 AFAIK, C ++ 11 구현은이 '미친'잠금을 수행합니다. 분명히 VS2010도 여전히 그렇게합니다. 누구나이 동작을 명확하게 설명 할 수 있습니까? '필요하지 않은'것은 여전히 ​​C ++ 11에서 보유하고 있습니까?
mloskot

2
@mloskot : 스레드 안전성 요구 사항이 없습니다 sentry... "클래스 센트리는 예외 안전 접두사 및 접미사 작업을 수행하는 클래스를 정의합니다." "보초 생성자와 소멸자는 구현에 따른 추가 작업을 수행 할 수도 있습니다." 또한 C ++위원회가 그러한 낭비적인 요구 사항을 승인하지 않는다는 "사용하지 않는 것에 대해 비용을 지불하지 않는다"는 C ++ 원칙을 추측 할 수 있습니다. 그러나 요오드 스레드 안전에 대해 언제든지 문의하십시오.
벤 Voigt

8

문제는 write ()를 호출 할 때마다 오버 헤드가 발생한다는 것입니다. 추가하는 각 추상화 레벨 (char []-> vector-> string-> ostringstream)은 함수 호출 / 반환과 백만 번 호출하면 추가하는 기타 관리 용 guff를 추가합니다.

한 번에 10 개의 정수를 쓰도록 ideone의 두 가지 예를 수정했습니다. ostringstream 시간은 53ms에서 6ms (거의 10 배 향상)가되었지만 char 루프는 개선 되었으나 (3.7에서 1.5) 유용하지만 2 배만 향상되었습니다.

성능이 염려된다면 작업에 적합한 도구를 선택해야합니다. ostringstream은 유용하고 유연하지만 원하는 방식으로 사용하면 위약금이 부과됩니다. char []는 더 어려운 작업이지만 성능 향상은 클 수 있습니다 (gcc가 아마도 memcpys를 인라인 할 것임을 기억하십시오).

요컨대 ostringstream은 깨지지 않지만 금속에 가까울수록 코드가 더 빨리 실행됩니다. 어셈블러는 여전히 일부 사람들에게 이점이 있습니다.


8
무엇 않는 ostringstream::write()것을해야 vector::push_back()하지 않는 이유는 무엇입니까? 무엇이든, 4 개의 개별 요소 대신 블록을 넘겨주기 때문에 더 빨라야합니다. 경우 ostringstream느린보다 std::vector추가 기능을 제공하지 않고, 다음 그래 내가 깨진 것을 부를 것이다.
Ben Voigt

1
@ Ben Voigt : 반대로 벡터는 ostringstream을 수행해야합니다.이 경우 벡터를보다 성능이 좋게 만들 필요가 없습니다. ostringstream이 아닌 벡터는 메모리에서 연속적임을 보장합니다. 벡터는 성능을 발휘하도록 설계된 클래스 중 하나이지만 ostringstream은 그렇지 않습니다.
Dragontamer5788

2
@Ben Voigt : 의 공용 인터페이스는 기본 클래스의 공용 비가 상 함수로 구성되고 파생 된 클래스의 보호 된 가상 함수로 디스패치 stringbuf되므로 직접 사용 하는 것이 모든 함수 호출을 제거하지는 않습니다 stringbuf.
CB Bailey

2
@Charles : 괜찮은 컴파일러에서 공개 함수 호출은 동적 유형이 컴파일러에 알려진 컨텍스트에 인라인되기 때문에 간접 호출을 제거하고 해당 호출을 인라인 할 수 있습니다.
Ben Voigt

6
@ Roddy : 모든 컴파일 단위에서 볼 수있는 모든 인라인 템플릿 코드라고 생각해야합니다. 그러나 구현에 따라 다를 수 있습니다. 확실하게 나는 토론 sputn중인 호출, virtual protected를 호출하는 퍼블릭 함수 xsputn가 인라인 될 것으로 기대합니다 . xsputn인라인되지 않더라도 컴파일러는 인라인하는 동안 필요한 sputn정확한 xsputn재정의를 결정 하고 vtable을 거치지 않고 직접 호출을 생성 할 수 있습니다.
Ben Voigt

1

더 나은 성능을 얻으려면 사용중인 컨테이너의 작동 방식을 이해해야합니다. char [] 배열 예제에서 필요한 크기의 배열이 미리 할당됩니다. 벡터 및 ostringstream 예제에서는 객체가 증가함에 따라 객체를 반복적으로 할당 및 재 할당하고 데이터를 여러 번 복사하도록합니다.

std :: vector를 사용하면 char 배열을 수행했을 때 벡터 크기를 최종 크기로 초기화하여 쉽게 해결할 수 있습니다. 대신 0으로 크기를 조정하여 불공평하게 성능을 저하시킵니다! 이것은 공정한 비교가 아닙니다.

ostringstream과 관련하여 공간을 사전 할당하는 것은 불가능합니다. 나는 그것이 부적절한 사용이라고 제안합니다. 클래스는 간단한 char 배열보다 훨씬 큰 유틸리티를 가지고 있지만, 그 유틸리티가 필요하지 않으면 사용하지 마십시오. 어쨌든 오버 헤드가 발생하기 때문입니다. 대신 데이터를 문자열로 형식화하는 것이 좋은 용도로 사용해야합니다. C ++은 광범위한 컨테이너를 제공하며 ostringstram은이 목적에 가장 적합하지 않습니다.

벡터 및 ostringstream의 경우 버퍼 오버런으로부터 보호를 받고 char 배열로는이를 얻지 못하며 보호 기능은 무료로 제공되지 않습니다.


1
할당은 ostringstream의 문제가 아닌 것 같습니다. 그는 후속 반복을 위해 0으로 되돌아갑니다. 잘리지 않습니다. 또한 나는 시도했지만 ostringstream.str.reserve(4000000)아무런 차이가 없었습니다.
Roddy

내가 가진 생각 ostringstream더미 문자열에 전달하여, 당신은 할 수 있었다 "예약", 즉 : ostringstream str(string(1000000 * sizeof(int), '\0'));로는 vector, (가) resize그것을 할 필요가있는 경우 어떤 공간 할당을 해제하지 않습니다, 그것은 단지 확장합니다.
Nim

1
"벡터 .. 버퍼 오버런 방지". 일반적인 오해- vector[]운영자는 기본적으로 경계 오류를 검사하지 않습니다. vector.at()그러나.
Roddy

2
vector<T>::resize(0)일반적으로 메모리 재 할당하지 않습니다
니키 Yoshiuchi

2
@ Roddy :을 사용하지 operator[]않지만 push_back()()를 사용하여 back_inserter오버플로를 테스트합니다. 를 사용하지 않는 다른 버전을 추가했습니다 push_back.
벤 Voigt
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.