C ++에서 내가 먹지 않은 것에 대해 지불하고 있습니까?


170

C 및 C ++의 다음 hello world 예제를 고려하십시오.

main.c

#include <stdio.h>

int main()
{
    printf("Hello world\n");
    return 0;
}

main.cpp

#include <iostream>

int main()
{
    std::cout<<"Hello world"<<std::endl;
    return 0;
}

Godbolt에서 어셈블리로 컴파일 할 때 C 코드의 크기는 9 줄 ( gcc -O3)입니다.

.LC0:
        .string "Hello world"
main:
        sub     rsp, 8
        mov     edi, OFFSET FLAT:.LC0
        call    puts
        xor     eax, eax
        add     rsp, 8
        ret

그러나 C ++ 코드의 크기는 22 줄 ( g++ -O3)입니다.

.LC0:
        .string "Hello world"
main:
        sub     rsp, 8
        mov     edx, 11
        mov     esi, OFFSET FLAT:.LC0
        mov     edi, OFFSET FLAT:_ZSt4cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
        mov     edi, OFFSET FLAT:_ZSt4cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)
        xor     eax, eax
        add     rsp, 8
        ret
_GLOBAL__sub_I_main:
        sub     rsp, 8
        mov     edi, OFFSET FLAT:_ZStL8__ioinit
        call    std::ios_base::Init::Init() [complete object constructor]
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:_ZStL8__ioinit
        mov     edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
        add     rsp, 8
        jmp     __cxa_atexit

... 훨씬 큽니다.

C ++에서는 먹는 것을 지불하는 것이 유명합니다. 그래서이 경우, 나는 무엇을 지불하고 있습니까?


3
의견은 긴 토론을위한 것이 아닙니다. 이 대화는 채팅 으로 이동 되었습니다 .
Samuel Liew


26
eatC ++과 관련된 용어를 들어 본 적이 없습니다 . 나는 당신이 의미하는 것을 믿는다 : "당신이 사용한 것에 대해서만 지불 하는가?"
Giacomo Alzetta

7
@GiacomoAlzetta, ... 뷔페 뷔페 개념을 적용한 구어체입니다. 보다 정확한 용어를 사용하는 것은 전 세계 잠재 고객에게 확실히 바람직하지만 미국 영어 원어민으로서 제목이 의미가 있습니다.
Charles Duffy

5
@ trolley813 메모리 누수는 따옴표 및 OP 질문과 관련이 없습니다. "사용하는 것에 대해서만 비용을 지불합니다"/ "사용하지 않는 것에 대해 지불하지 않습니다"의 요점은 특정 기능 / 추상을 사용하지 않으면 성능 저하가 발생하지 않는다는 것입니다. 메모리 누수는 이것과 전혀 관련이 없으며 이는 용어 eat가 더 모호하므로 피해야 함을 나타냅니다.
Giacomo Alzetta

답변:


60

당신이 지불하는 것은 무거운 라이브러리 (콘솔에 인쇄하는 것만 큼 무겁지 않은)를 호출하는 것입니다. ostream객체를 초기화 합니다. 숨겨진 스토리지가 있습니다. 그런 다음 std::endl의 동의어가 아닌을 ( 를) 호출 합니다 \n. iostream라이브러리는 여러 설정을 조정하고 프로그래머가 아닌 프로세서에 부담을 넣어하는 데 도움이됩니다. 이것이 당신이 지불하는 것입니다.

코드를 검토하자 :

.LC0:
        .string "Hello world"
main:

ostream 객체 + cout 초기화

    sub     rsp, 8
    mov     edx, 11
    mov     esi, OFFSET FLAT:.LC0
    mov     edi, OFFSET FLAT:_ZSt4cout
    call    std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)

cout새 줄을 인쇄하고 플러시하기 위해 다시 전화

    mov     edi, OFFSET FLAT:_ZSt4cout
    call    std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)
    xor     eax, eax
    add     rsp, 8
    ret

정적 스토리지 초기화 :

_GLOBAL__sub_I_main:
        sub     rsp, 8
        mov     edi, OFFSET FLAT:_ZStL8__ioinit
        call    std::ios_base::Init::Init() [complete object constructor]
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:_ZStL8__ioinit
        mov     edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
        add     rsp, 8
        jmp     __cxa_atexit

또한 언어와 라이브러리를 구별하는 것이 필수적입니다.

BTW, 이것은 이야기의 일부일뿐입니다. 호출하는 함수에 쓰여진 내용을 모릅니다.


5
추가적인 참고 사항으로, 철저한 테스트를 통해 "ios_base :: sync_with_stdio (false);" 및 "cin.tie (NULL);" printf보다 printout이 더 빠릅니다 (Printf에 형식 문자열 오버 헤드가 있음). 첫 번째 cout; printf; cout는 순서대로 쓰기 를 수행하는 오버 헤드를 제거합니다 (자체 버퍼가 있으므로). 둘째 desync 것 coutcin일으키는 cout; cin잠재적 제 1 정보를 사용자에게 물어. 플러싱은 실제로 필요할 때만 동기화되도록합니다.
니콜라스 피피 톤

안녕하세요 Nicholas 님, 유용한 메모를 추가해 주셔서 감사합니다.
Arash

"언어와 라이브러리를 구별하는 것이 필수적입니다": 예,하지만 언어와 함께 제공되는 표준 라이브러리는 어디에서나 사용할 수있는 유일한 라이브러리이므로 모든 곳에서 사용되는 라이브러리입니다 (그리고 C 표준 라이브러리는 일부입니다) C ++ 사양 중 하나이므로 원하는 경우 사용할 수 있습니다. "당신은 당신이 호출하는 함수에 쓰여진 것을 모른다"에 관하여 : 당신이 정말로 알고 싶다면 정적으로 링크 할 수 있으며, 실제로 당신이 검사하는 호출 코드는 관련이 없을 것입니다.
피터-복원 모니카

211

그래서이 경우, 나는 무엇을 지불하고 있습니까?

std::cout보다 강력하고 복잡합니다 printf. 로케일, 상태 저장 형식 플래그 등을 지원합니다.

당신은 그 사용이 필요하지 않은 경우 std::printf또는 std::puts- 그들에게있는 거 로모을 <cstdio>.


C ++에서는 먹는 것을 지불하는 것이 유명합니다.

또한 C ++ ! = C ++ 표준 라이브러리 임을 분명히하고 싶습니다 . 표준 라이브러리는 범용이며 "충분히 빠르다"고 가정하지만 필요한 것의 특수 구현보다 속도가 느릴 수 있습니다.

반면에 C ++ 언어는 불필요한 추가 비용을 지불하지 않고 코드를 작성할 수 있도록 노력합니다 (예 : 옵트 인 virtual, 가비지 수집 없음).


4
표준 라이브러리는 범용이며 "충분히 빠르다"고 말하지만 +1은 필요한 것의 전문화 된 구현보다 느리다. 많은 사람들이 성능에 미치는 영향을 고려하지 않고 STL 구성 요소를 사용하는 것처럼 보입니다.
Craig Estey

7
@Craig OTOH 표준 라이브러리의 많은 부분은 일반적으로 대신 생성 할 수있는 것보다 더 빠르고 정확합니다.
피터-복원 모니카

2
@ PeterA.Schneider OTOH, STL 버전이 20 배에서 30 배 느리면 나름대로 굴리는 것이 좋습니다. 여기에 내 대답을 참조하십시오 : codereview.stackexchange.com/questions/191747/… 거기에서 다른 사람들도 자신의 롤을 제안했습니다.
Craig Estey

1
@CraigEstey 벡터는 C 배열보다 비효율적이지 않습니다 (초기 동적 할당과 별도로 주어진 인스턴스에서 수행 할 작업량에 따라 중요 할 수 있음). 되어 설계 할 수 없습니다. 주변을 복사하거나 초기에 충분한 공간을 확보하지 않도록주의해야하지만, 어레이로 수행해야하는 모든 작업은 덜 안전합니다. 링크 된 예와 관련하여 그렇습니다. 벡터의 벡터는 (최적화되지 않은 한) 2D 배열과 비교하여 추가 간접 처리가 발생하지만 20 배의 효율은 뿌리가 아니라 알고리즘에 있다고 가정합니다.
피터-복원 모니카

174

C와 C ++을 비교하지 않습니다. 당신은 비교할 printfstd::cout다른 일 (로케일, 상태 서식, 등) 할 수있는,.

다음 코드를 사용하여 비교해보십시오. Godbolt는 두 파일 모두에 대해 동일한 어셈블리를 생성합니다 (gcc 8.2, -O3으로 테스트).

main.c :

#include <stdio.h>

int main()
{
    int arr[6] = {1, 2, 3, 4, 5, 6};
    for (int i = 0; i < 6; ++i)
    {
        printf("%d\n", arr[i]);
    }
    return 0;
}

main.cpp :

#include <array>
#include <cstdio>

int main()
{
    std::array<int, 6> arr {1, 2, 3, 4, 5, 6};
    for (auto x : arr)
    {
        std::printf("%d\n", x);
    }
}


동등한 코드를 보여주고 이유를 설명하는 건배입니다.
HackSlash

134

귀하의 목록은 실제로 사과와 오렌지를 비교하지만 대부분의 다른 답변에서 암시되는 것은 아닙니다.

코드의 실제 기능을 확인하십시오.

씨:

  • 단일 문자열을 인쇄 "Hello world\n"

C ++ :

  • 문자열 "Hello world"std::cout
  • std::endl조작기를 스트림std::cout

분명히 C ++ 코드는 두 배나 많은 작업을 수행하고 있습니다. 공정한 비교를 위해 다음을 결합해야합니다.

#include <iostream>

int main()
{
    std::cout<<"Hello world\n";
    return 0;
}

… 갑자기 어셈블리 코드는 mainC와 매우 유사합니다.

main:
        sub     rsp, 8
        mov     esi, OFFSET FLAT:.LC0
        mov     edi, OFFSET FLAT:_ZSt4cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
        xor     eax, eax
        add     rsp, 8
        ret

실제로 C와 C ++ 코드를 한 줄씩 비교할 수 있으며 차이점거의 없습니다 .

sub     rsp, 8                      sub     rsp, 8
mov     edi, OFFSET FLAT:.LC0   |   mov     esi, OFFSET FLAT:.LC0
                                >   mov     edi, OFFSET FLAT:_ZSt4cout
call    puts                    |   call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
xor     eax, eax                    xor     eax, eax
add     rsp, 8                      add     rsp, 8
ret                                 ret

유일한 차이점은 C ++ operator <<에서 두 개의 인수 ( std::cout및 문자열)로 호출한다는 것입니다 . 더 가까운 C eqivalent :를 사용하면 약간의 차이도 제거 할 수 fprintf있습니다.

이것에 대한 어셈블리 코드 _GLOBAL__sub_I_main는 C ++에서는 생성되지만 C에서는 생성되지 않습니다. 이것은이 어셈블리 목록에서 볼 수있는 유일한 실제 오버 헤드입니다 ( 물론 언어에 대해 더 많은 보이지 않는 오버 헤드 가 있습니다). 이 코드는 C ++ 프로그램 시작시 일부 C ++ 표준 라이브러리 함수의 일회성 설정을 수행합니다.

그러나 다른 답변에서 설명 했듯이이 두 프로그램 간의 관련 차이점은 main모든 무거운 리프팅이 장면 뒤에서 발생하기 때문에 함수 의 어셈블리 출력에서 ​​찾을 수 없습니다 .


21
또한 C 런타임 설정해야하며,이 함수는 호출 된 함수에서 발생 _start하지만 해당 코드는 C 런타임 라이브러리의 일부입니다. 어쨌든 이것은 C와 C ++ 모두에서 발생합니다.
Konrad Rudolph

2
@Deduplicator : 사실, 기본적으로 iostream 라이브러리는하지 않습니다 어떤 의 버퍼링을 std::cout대신 I에게 (자신의 버퍼링 메커니즘을 사용)를 표준 입출력 구현 / O를 전달합니다. 특히, 대화식 터미널에 연결되어있는 경우 기본적으로에 쓸 때 완전히 버퍼링 된 출력을 볼 수 없습니다 std::cout. iostream 라이브러리가에 자체 버퍼링 메커니즘을 사용하도록하려면 stdio와의 동기화를 명시 적으로 비활성화해야합니다 std::cout.

6
@ KonradRudolph : 실제로, printf스트림을 플러시 할 필요는 없습니다. 사실, 일반적인 사용 사례 (파일로 리디렉션 된 출력)에서는 일반적으로 해당 printf명령문 플러시 되지 않습니다 . 출력이 라인 버퍼링되거나 버퍼링되지 않은 경우에만 printf플러시 가 트리거 됩니다 .

2
@PeterCordes : 맞지 않는 출력 버퍼로 차단할 수는 없지만 프로그램이 입력을 수락하고 예상 출력을 표시하지 않고 행진하는 것에 놀라게 될 수 있습니다. "도움말, 입력하는 동안 프로그램이 중단되었지만 그 이유를 알 수 없습니다!"를 디버깅 할 기회가 있었기 때문에 이것을 알고 있습니다. 며칠 동안 다른 개발자에게 적합했습니다.

2
@PeterCordes : 내가 주장하는 주장은 "무엇을 의미하는지 쓰기"입니다. 출력을 최종적으로 사용할 수있게하려면 줄 바꿈이 적합하고 출력을 즉시 사용할 수있게하려면 endl이 적절합니다.

53

C ++에서는 먹는 것을 지불하는 것이 유명합니다. 그래서이 경우, 나는 무엇을 지불하고 있습니까?

간단합니다. 당신은 지불합니다 std::cout. "먹는 것만 지불한다"는 것이 "항상 가장 좋은 가격을 받는다"는 의미는 아닙니다. 물론 printf더 저렴합니다. std::cout더 안전하고 다재다능하다고 주장 할 수 있으므로 더 큰 비용이 정당화되지만 (비용이 많이 들지만 더 많은 가치를 제공함), 요점을 놓치게됩니다. 당신은 사용하지 않는 printf당신은 사용, std::cout사용 비용을 지불하므로 std::cout. 을 (를) 사용하여 비용을 지불하지 않습니다 printf.

좋은 예는 가상 함수입니다. 가상 함수에는 런타임 비용과 공간 요구 사항이 있지만 실제로 사용하는 경우에만 필요 합니다. 가상 기능을 사용하지 않으면 아무것도 지불하지 않습니다.

몇 가지 언급

  1. C ++ 코드가 더 많은 어셈블리 명령어로 평가 되더라도 여전히 소수의 명령어이며 실제 I / O 작업으로 인해 성능 오버 헤드가 여전히 줄어 듭니다.

  2. 실제로, 때로는 "C ++에서는 먹는 것에 대해 지불하는 것"보다 낫습니다. 예를 들어, 컴파일러는 일부 상황에서 가상 함수 호출이 필요하지 않다고 추론하고이를 가상이 아닌 호출로 변환 할 수 있습니다. 그것은 당신이 무료로 가상 기능을 얻을 수 있음을 의미합니다 . 대단하지 않습니까?


6
가상 기능은 무료로 제공되지 않습니다. 먼저 코드를 작성하고 코드가 컴파일러가 수행해야 할 작업에 대한 아이디어와 일치하지 않을 때 코드를 디버깅하는 비용을 지불해야합니다.
alephzero

2
@alephzero 개발 비용과 성능 비용을 비교하는 것이 특히 관련이 있는지 확실하지 않습니다.

말장난에 대한 그러한 좋은 기회는 낭비되었습니다 ... 당신은 '가격'대신 '칼로리'라는 단어를 사용했을 수 있습니다. 그로부터 C ++이 C보다 뚱뚱하다고 말할 수 있습니다. 또는 적어도 ... 문제의 특정 코드 (C를 선호하여 C ++에 대해 편향되어 있으므로 상당히 넘어갈 수는 없습니다). 아아. @Bilkokuya 모든 경우에 관련이 없을 수도 있지만 반드시 무시해서는 안되는 것입니다. 따라서 그것은 전체적으로 관련이 있습니다.
Pryftan

46

"printf에 대한 어셈블리리스트"는 printf에 대한 것이 아니라 puts에 대한 것입니다 (컴파일러 최적화의 종류?); printf는 예전보다 훨씬 복잡합니다 ... 잊지 마세요!


13
이것은 다른 모든 것들이 std::cout어셈블리 목록에 보이지 않는 내부 에 대해 빨간 청어에 매달리기 때문에 지금까지 가장 좋은 대답 입니다.
Konrad Rudolph

12
어셈블리 목록은에 대한 호출을 위한 것으로puts , printf단일 형식 문자열 만 전달하고 추가 인수가없는 경우 의 호출과 동일하게 보입니다 . ( xor %eax,%eax우리가 레지스터의 0 FP 인수를 가변 함수로 전달하기 때문에 또한 예외가 있습니다 .) 이들 중 어느 것도 문자열 함수에 대한 포인터를 라이브러리 함수에 전달하는 구현이 아닙니다. 그러나 그렇습니다. 최적화 printf하는 puts것은 gcc 만있는 형식 "%s"이나 변환이없는 경우 문자열에서 줄 바꿈으로 끝나는 것입니다.
Peter Cordes

45

여기에 유효한 답변이 있지만 세부 사항에 대해 조금 더 설명하겠습니다.

이 전체 텍스트를 통과하고 싶지 않은 경우 주요 질문에 대한 답변을 보려면 아래 요약으로 이동하십시오.


추출

그래서이 경우, 나는 무엇을 지불하고 있습니까?

추상화 비용을 지불하고 있습니다. 더 간단하고 인간 친화적 인 코드를 작성할 수있는 비용이 발생합니다. 객체 지향 언어 인 C ++에서 거의 모든 것이 객체입니다. 어떤 물체를 사용하면 항상 세 가지 주요 일이 발생합니다.

  1. 객체 생성, 기본적으로 객체 자체와 데이터에 대한 메모리 할당.
  2. 객체 초기화 init() 방법을 ). 일반적으로 메모리 할당은이 단계에서 가장 먼저 발생합니다.
  3. 객체 파괴 (항상 그런 것은 아님).

코드에서 볼 수는 없지만 매번 객체를 사용할 때마다 위의 세 가지 일이 모두 발생해야합니다. 모든 것을 수동으로 수행한다면 코드는 분명히 길어질 것입니다.

이제 오버 헤드를 추가하지 않고도 추상화를 효율적으로 수행 할 수 있습니다. 컴파일러와 프로그래머 모두 메소드 인라이닝 및 기타 기술을 사용하여 추상화의 오버 헤드를 제거 할 수 있지만, 그렇지 않습니다.

C ++에서 실제로 무슨 일이 일어나고 있습니까?

여기에 세분화되어 있습니다.

  1. 그만큼 std::ios_base클래스는 모든 것을 I / 관련 O의 기본 클래스 인, 초기화됩니다.
  2. 그만큼 std::cout개체가 초기화됩니다.
  3. 문자열은에로드되어 전달됩니다 std::__ostream_insert. (이미 이름으로 알 수 있듯이 ) 문자열을 스트림에 추가하는 방법 std::cout(기본적으로 <<연산자)입니다.
  4. cout::endl또한로 전달됩니다 std::__ostream_insert.
  5. __std_dso_handle__cxa_atexit프로그램으로 나가기 전에 "세척"을 담당하는 전역 함수 인 로 전달됩니다 . __std_dso_handle이 함수는 나머지 전역 객체를 할당 해제하고 파괴하기 위해 자체적으로 호출됩니다.

C ==를 사용하여 아무것도 지불하지 않습니까?

C 코드에서 매우 적은 단계가 발생합니다.

  1. 문자열이로드되고 레지스터 를 puts통해 전달됩니다 edi.
  2. puts 호출됩니다.

어디에도 객체가 없으므로 초기화 / 파괴 할 필요가 없습니다.

그러나 이것이 C의 어떤 것에 대해서도 "지불"하지 않는다는 것을 의미하지는 않습니다 . 여전히 추상화 비용을 지불하고 C 표준 라이브러리의 초기화 및 printf함수의 동적 해상도 (또는 실제로puts 는 형식 문자열이 필요하지 않으므로 컴파일러에서 최적화 된)의 여전히 후드 아래에서 발생합니다.

이 프로그램을 순수한 어셈블리로 작성한다면 다음과 같이 보일 것입니다.

jmp start

msg db "Hello world\n"

start:
    mov rdi, 1
    mov rsi, offset msg
    mov rdx, 11
    mov rax, 1          ; write
    syscall
    xor rdi, rdi
    mov rax, 60         ; exit
    syscall

어떤 기본적에만 호출 결과 write 에 의해 다음 exit콜을. 이제 이것은 동일한 것을 달성하기위한 최소한의 것입니다.


요약

C는 더 베어 본 이며 필요한 최소한의 값만 사용하면 사용자가 모든 것을 완벽하게 제어 할 수 있으므로 원하는 것을 기본적으로 완전히 최적화하고 사용자 지정할 수 있습니다. 프로세서에 레지스터에 문자열을로드 한 다음 라이브러리 함수를 호출하여 해당 문자열을 사용하도록 지시합니다. 반면에 C ++은 더 복잡하고 추상적 입니다. 이것은 복잡한 코드를 작성할 때 엄청난 이점이 있으며, 작성하기 쉽고 인간 친화적 인 코드를 허용하지만 비용이 많이 듭니다. C ++은 이러한 기본 작업을 수행하는 데 필요한 것보다 많은 것을 제공하므로 오버 헤드가 더 많기 때문에 C 와 비교할 경우 항상 C ++의 성능 저하가 있습니다.

주요 질문에 대한 답변 :

내가 먹지 않은 것에 대한 비용을 지불하고 있습니까?

이 특정한 경우에는 yes 입니다. C ++이 C보다 더 많은 것을 제공하는 것을 활용하지는 않지만, C ++이 도와 줄 수있는 간단한 코드에는 아무것도 없기 때문입니다. 너무 간단해서 C ++이 전혀 필요하지 않습니다.


아, 한가지 만 더!

매우 간단하고 작은 프로그램을 작성했지만 조금 더 복잡한 예제를보고 차이점을 확인하기 때문에 C ++의 장점은 언뜻보기에 분명하지 않을 수 있습니다.

C :

#include <stdio.h>
#include <stdlib.h>

int cmp(const void *a, const void *b) {
    return *(int*)a - *(int*)b;
}

int main(void) {
    int i, n, *arr;

    printf("How many integers do you want to input? ");
    scanf("%d", &n);

    arr = malloc(sizeof(int) * n);

    for (i = 0; i < n; i++) {
        printf("Index %d: ", i);
        scanf("%d", &arr[i]);
    }

    qsort(arr, n, sizeof(int), cmp)

    puts("Here are your numbers, ordered:");

    for (i = 0; i < n; i++)
        printf("%d\n", arr[i]);

    free(arr);

    return 0;
}

C ++ :

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

int main(void) {
    int n;

    cout << "How many integers do you want to input? ";
    cin >> n;

    vector<int> vec(n);

    for (int i = 0; i < vec.size(); i++) {
        cout << "Index " << i << ": ";
        cin >> vec[i];
    }

    sort(vec.begin(), vec.end());

    cout << "Here are your numbers:" << endl;

    for (int item : vec)
        cout << item << endl;

    return 0;
}

바라건대 여기서 내가 의미하는 바를 명확하게 볼 수 있습니다. 사용 낮은 수준의 메모리를 관리 할 수있는 방법 C에서 또한 통보 mallocfree인덱싱 및 크기에 대한 더 조심해야하는지, 그리고 입력을 복용하고 인쇄 할 때 매우 구체적으로해야하는지.


27

시작할 때 몇 가지 오해가 있습니다. 첫째, C ++ 프로그램 22 개의 명령어를 생성 하지 않으며 , 22,000 개와 비슷합니다 (저는 모자에서 해당 숫자를 가져 왔지만 대략 야구장에 있습니다). 또한 C 코드 9 개의 명령어를 생성 하지 않습니다 . 그것들은 당신이 보는 것입니다.

C 코드는 보이지 않는 많은 작업을 수행 한 후 CRT (보통 공유 라이브러리로 존재하지는 않지만)에서 함수를 호출 한 다음 반환 값을 확인하거나 처리 하지 않습니다. 오류와 구제. 컴파일러 및 최적화 설정에 따라 실제로 호출 printf하지는 않지만 puts훨씬 원시적입니다.
동일한 함수를 같은 방식으로 호출 한 경우 C ++에서 동일한 프로그램 (보이지 않는 초기화 함수 제외)을 작성했을 수도 있습니다. 또는 수퍼 교정을 원한다면 동일한 함수 앞에 접두사가 붙습니다 std::.

해당 C ++ 코드는 실제로는 전혀 동일하지 않습니다. <iostream>그것 의 전체가 작은 프로그램에 대한 엄청난 오버 헤드를 추가하는 뚱뚱한 못생긴 돼지로 잘 알려져 있지만 ( "실제"프로그램에서는 그다지 눈치 채지 못합니다) 다소 공정한 해석은 그것이 끔찍한 일이라는 것입니다 당신이 보지 못하고 작동 하는 많은 것들 . 다른 숫자 형식과 로케일 및 기타, 버퍼링 및 적절한 오류 처리를 포함하여 거의 모든 위험 요소의 마법 형식을 포함하지만 이에 국한되지 않습니다. 오류 처리? 글쎄, 문자열 출력이 실제로 실패 할 수있는 것을 추측하십시오 .C 프로그램과 달리 C ++ 프로그램은 그렇지 않습니다. 이것을 자동으로 무시 . 무엇을 고려std::ostream사람들이 알아 차리지 않고 실제로는 매우 가볍습니다. 나는 열정으로 스트림 구문을 싫어하기 때문에 그것을 사용하는 것과 다릅니다. 그러나 여전히 당신이하는 일을 고려하면 꽤 굉장합니다.

그러나 C ++ 전체는 C만큼 효율적 이지 않습니다 . 이 같은 일이 아니며이되지 않기 때문에 그것은 효율적으로 할 수없는 일을 같은 일을. 다른 것이 없으면 C ++은 예외를 생성하고 (생성, 처리 또는 실패하는 코드) C가 제공하지 않는다는 보장을합니다. 따라서 C ++ 프로그램은 반드시 조금 더 커야합니다. 그러나 큰 그림에서 이것은 중요하지 않습니다. 반대로, 실제 프로그램의 경우 C ++이 어떤 이유로 든 더 나은 성능을 제공하는 것으로 보지 못했습니다. 왜 그런지 묻지 마라.

최선을 다하는 것 대신에 C 코드를 작성하는 것이 올바른 경우 (즉, 실제로 오류를 확인하고 오류가있는 경우 프로그램이 올바르게 작동하는 경우) 차이는 미미합니다. 존재하는 경우.


16
이 주장은“하지만 C ++이 C만큼 효율적이지는 않다”는 것을 제외하고는 아주 좋은 대답이다. C ++는 C만큼 효율적일 수 있으며 충분히 높은 수준의 코드는 동등한 C 코드 보다 효율적일 수 있습니다 . 예, C ++은 예외를 처리해야하기 때문에 약간의 오버 헤드가 있지만 최신 컴파일러의 경우 오버 헤드는 더 나은 비용없는 추상화로 인한 성능 향상과 비교할 때 무시할 수 있습니다.
Konrad Rudolph

올바르게 이해하면 std::cout예외도 발생합니까?
Saher

6
@Saher : 예, 아니요. std::coutA는 std::basic_ostream하나 그 던져, 그리고 그것을 할 수 있는 경우 그렇지 않은 경우 발생하는 예외를 다시 던지는 구성 그렇게하도록하거나 수있는 예외를 삼키는. 문제는 실패 할 수 있으며 C ++ 표준 라이브러리뿐만 아니라 C ++도 (대부분) 빌드되므로 실패를 쉽게 알 수 없습니다. 이것은 성가심 축복입니다 (그러나 성가심보다 더 많은 축복). 반면에 C는 가운데 손가락을 보여줍니다. 리턴 코드를 확인하지 않고 어떤 일이 발생했는지 전혀 알 수 없습니다.
데이먼

1
@KonradRudolph : 사실, 이것은 제가 지적하려고 했던 것입니다. " 어떤 이유로 C ++이 더 잘 수행되는 것을 거의 찾지 못했습니다. 더 유리한 최적화를 빌려주는 것 같습니다. 왜 그런지 묻지 마십시오." . 이유를 즉시 알 수는 없지만 더 잘 최적화되는 경우는 거의 없습니다. 이유가 무엇이든지. 최적화 프로그램과 모두 동일하다고 생각하지만 그렇지 않습니다.
데이먼

22

당신은 실수를 지불하고 있습니다. 80 년대에 컴파일러가 형식 문자열을 확인하기에 충분하지 않은 경우, 연산자 오버로딩은 io 동안 유형 안전의 유사성을 적용하는 좋은 방법으로 여겨졌습니다. 그러나 모든 배너 기능은 처음부터 잘못되거나 개념적으로 파산되었습니다.

<iomanip>

C ++ 스트림 IO API의 가장 유명한 부분은이 형식화 헤더 라이브러리가 존재한다는 것입니다. 상태가 좋고 추악하고 오류가 발생하기 쉬우 며 서식을 스트림에 연결합니다.

8 자리의 0으로 채워진 16 진 부호없는 int와 공백, 소수점 이하 3 자리를 갖는 행을 인쇄한다고 가정하십시오. 을 사용하면 <cstdio>간결한 형식 문자열을 읽을 수 있습니다. 을 사용하면 <ostream>이전 상태를 저장하고 정렬을 오른쪽으로 설정하고 채우기 문자를 설정하고 채우기 너비를 설정하고 밑을 16 진수로 설정하고 정수를 출력하고 저장된 상태를 복원합니다 (그렇지 않으면 정수 형식이 부동 형식을 오염시킵니다), 공간을 출력해야합니다 , 표기법을 고정으로 설정하고 정밀도를 설정하고 이중 및 개행을 출력 한 다음 이전 형식을 복원하십시오.

// <cstdio>
std::printf( "%08x %.3lf\n", ival, fval );

// <ostream> & <iomanip>
std::ios old_fmt {nullptr};
old_fmt.copyfmt (std::cout);
std::cout << std::right << std::setfill('0') << std::setw(8) << std::hex << ival;
std::cout.copyfmt (old_fmt);
std::cout << " " << std::fixed << std::setprecision(3) << fval << "\n";
std::cout.copyfmt (old_fmt);

연산자 오버로딩

<iostream> 연산자 오버로드를 사용하지 않는 방법의 포스터 하위입니다.

std::cout << 2 << 3 && 0 << 5;

공연

std::cout몇 배 느립니다 printf(). 만연한 기능 장애와 가상 파견은 큰 타격을받습니다.

스레드 안전

모두 <cstdio><iostream>모든 함수 호출 원자 인 것을 스레드 안전합니다. 그러나 printf()통화 당 더 많은 작업을 수행 할 수 있습니다. <cstdio>옵션으로 다음 프로그램을 실행하면 의 행만 표시됩니다 f. <iostream>멀티 코어 머신에서 사용 하는 경우 다른 것을 볼 수 있습니다.

// g++ -Wall -Wextra -Wpedantic -pthread -std=c++17 cout.test.cpp

#define USE_STREAM 1
#define REPS 50
#define THREADS 10

#include <thread>
#include <vector>

#if USE_STREAM
    #include <iostream>
#else
    #include <cstdio>
#endif

void task()
{
    for ( int i = 0; i < REPS; ++i )
#if USE_STREAM
        std::cout << std::hex << 15 << std::dec;
#else
        std::printf ( "%x", 15);
#endif

}

int main()
{
    auto threads = std::vector<std::thread> {};
    for ( int i = 0; i < THREADS; ++i )
        threads.emplace_back(task);

    for ( auto & t : threads )
        t.join();

#if USE_STREAM
        std::cout << "\n<iostream>\n";
#else
        std::printf ( "\n<cstdio>\n" );
#endif
}

이 예제의 레토르트는 대부분의 사람들이 여러 스레드에서 단일 파일 디스크립터에 쓰지 않기 위해 훈련을 받는다는 것입니다. 음, 그 경우에, 당신은 관찰해야합니다 <iostream>유용하게 모든에 대한 잠금 사로 잡고 <<모든를 >>. 에 있지만 <cstdio>자주 잠금을 해제하지 않으며 잠금을 해제하는 옵션도 있습니다.

<iostream> 덜 일관된 결과를 달성하기 위해 더 많은 잠금을 소비합니다.


2
printf의 대부분의 구현은 지역화에 매우 유용한 기능인 숫자 매개 변수를 갖습니다. 두 가지 언어 (영어 및 프랑스어와 같은)로 일부 출력을 생성해야하고 단어 순서가 다른 경우 다른 형식화 ​​문자열과 동일한 printf를 사용하고 매개 변수를 다른 순서로 인쇄 할 수 있습니다.
gnasher729

2
스트림의 상태 저장 형식은 내가 무엇을 말 해야할지 모르는 버그를 찾기가 너무 어려웠습니다. 좋은 대답입니다. 내가 할 수 있다면 한 번 이상 공표 할 것입니다.
mathreadler

6
“ 이는 std::cout몇 배 느리다 printf()”—이 주장은 인터넷 전체에서 반복되지만 시대에 맞지 않았다. 최신 IOstream 구현은 다음과 같습니다 printf. 후자는 또한 내부적으로 가상 디스패치를 ​​수행하여 버퍼링 된 스트림 및 지역화 된 IO (운영 체제에서 수행하지만 그럼에도 불구하고 수행됨)를 처리합니다.
Konrad Rudolph

3
@KevinZ 그리고 그것은 훌륭하지만 하나의 특정 호출을 벤치마킹하여 fmt의 특정 강점 (단일 문자열에 다른 형식의 많은)을 보여줍니다. 더 일반적인 사용에서의 차이 printfcout정신과 의사. 우연히이 사이트에는 수많은 벤치 마크가 있습니다.
Konrad Rudolph

3
@KonradRudolph 그것은 사실이 아닙니다. Microbenchmarks는 실제 프로그램의 특정 제한된 리소스 (레지스터, icache, 메모리, 분기 예측기 등)를 소모하지 않기 때문에 팽창 및 간접 비용을 종종 과소 평가합니다. "보다 일반적인 사용법"을 암시 할 때, 기본적으로 다른 곳에서는 상당히 부풀어 오른 것입니다. 내 의견으로는, 성능 요구 사항이 없다면 C ++로 프로그래밍 할 필요가 없습니다.
KevinZ

18

모든 다른 답변이 말한뿐만 아니라,
또한 사실 거기 std::endl입니다 하지 같은 '\n'.

불행히도 일반적인 오해입니다. std::endl"새 줄"을 의미하는 것이 아니라 "새 줄을
인쇄 한 다음 스트림을 플러시 "한다는 의미 입니다. 플러싱은 저렴하지 않습니다!

완전히 사이의 차이를 무시 printf하고 std::cout잠시를 기능적으로 귀하의 C 예제에 eqvuialent 할하려면 C ++ 예는 다음과 같습니다한다고 :

#include <iostream>

int main()
{
    std::cout << "Hello world\n";
    return 0;
}

플러싱을 포함하는 경우 예제의 예는 다음과 같습니다.

#include <stdio.h>

int main()
{
    printf("Hello world\n");
    fflush(stdout);
    return 0;
}

C ++

#include <iostream>

int main()
{
    std::cout << "Hello world\n";
    std::cout << std::flush;
    return 0;
}

코드를 비교할 때는 항상 같은 방식으로 비교 하고 코드가 수행하는 작업의 의미를 이해해야합니다. 때로는 가장 간단한 예조차도 사람들이 생각하는 것보다 더 복잡합니다.


실제로, 사용 std::endl 라인 버퍼링 된 stdio 스트림에 개행을 쓰는 것과 동등한 기능입니다. stdout특히 인터랙티브 장치에 연결될 때 라인 버퍼링되거나 버퍼링되지 않아야합니다. 리눅스는 라인 버퍼 옵션을 고집합니다.

실제로 iostream 라이브러리 에는 라인 버퍼 모드 가 없습니다 . 라인 버퍼링의 효과를 얻는 방법은 줄 std::endl바꿈을 출력 하는 데 정확하게 사용 하는 것입니다.

@ 허킬 주장? 그렇다면 무엇을 사용 setvbuf(3)합니까? 아니면 기본값이 라인 버퍼링되었다는 의미입니까? 참고 : 일반적으로 모든 파일은 블록 버퍼링됩니다. 스트림이 터미널을 참조하면 (정상적으로 stdout처럼), 라인 버퍼링됩니다. 표준 오류 스트림 stderr는 기본적으로 항상 버퍼링되지 않습니다.
Pryftan

printf줄 바꿈 문자가 발생할 때 자동으로 플러시 되지 않습니까?
bool3max

1
@ bool3max 내 환경이하는 일만 말해 주면 다른 환경에서는 다를 수 있습니다. 가장 널리 사용되는 모든 구현에서 동일하게 작동하더라도 어딘가에 가장자리가 있다는 의미는 아닙니다. stanard가 매우 중요한 이유입니다. 표준은 모든 구현에서 무언가가 동일해야하는지 또는 구현마다 다를 수 있는지 여부를 나타냅니다.
파라 프

16

기존 기술 답변은 정확하지만이 질문은 궁극적으로이 오해에서 비롯된 것으로 생각합니다.

C ++에서는 먹는 것을 지불하는 것이 유명합니다.

이것은 C ++ 커뮤니티의 마케팅 강연입니다. (공평하게 말하면, 모든 언어 커뮤니티에서 마케팅 대화가 있습니다.) 이것은 당신이 진지하게 의지 할 수있는 구체적인 것을 의미하지는 않습니다.

"사용하는 대금을 지불"한다는 것은 C ++ 기능이 해당 기능을 사용하는 경우 오버 헤드 만 있음을 의미합니다. 그러나 "특징"의 정의는 무한정 세분화되지 않습니다. 여러 측면이있는 기능을 활성화하는 경우가 종종 있으며 이러한 측면의 하위 집합 만 필요한 경우에도 구현에서 기능을 부분적으로 가져 오는 것은 실용적이지 않거나 불가능합니다.

일반적으로, 많은 언어 (다만 모든 언어는 아님)는 다양한 수준의 성공을 거두면서 효율적으로 노력합니다. C ++은 규모가 어딘가에 있지만이 목표에서 완벽하게 성공할 수있는 디자인에는 특별한 또는 마술이 없습니다.


1
내가 사용하지 않는 것에 대한 비용을 지불 할 수있는 곳은 예외와 RTTI의 두 가지뿐입니다. 그리고 나는 그것이 마케팅 대화라고 생각하지 않습니다. C ++은 기본적으로 더 강력한 C이며 "사용하는 비용을 지불하지 않습니다".
Rakete1111

2
@ Rakete1111 예외가 발생하지 않으면 비용이 들지 않는다는 것이 오랫동안 확립되었습니다. 프로그램이 지속적으로 발생하는 경우 다시 디자인해야합니다. 실패 조건이 제어 할 수없는 경우 조건의 거짓에 의존하는 메소드를 호출하기 전에 부울 리턴 정상 상태 점검으로 조건을 점검해야합니다.
schulmaster

1
@schulmaster : C ++로 작성된 코드가 다른 언어로 작성된 코드와 상호 작용해야하는 경우 예외로 인해 디자인 제약이 발생할 수 있습니다. 로컬이 아닌 제어 전송은 모듈이 서로 조정하는 방법을 알고있는 경우에만 모듈간에 원활하게 작동 할 수 있기 때문입니다.
supercat

1
언어가 효율적이되기 위해 노력하고 있습니다 . 확실히 모든 것은 아니다 : 밀교적인 프로그래밍 언어는 비효율적이고 신기하고 흥미 롭도록 노력한다. esolangs.org . BrainFuck과 같은 일부는 매우 비효율적입니다. 또는 예를 들어 셰익스피어 프로그래밍 언어, 최소 227 바이트 (codegolf)로 모든 정수인쇄합니다 . 프로덕션 용 언어 중 대부분은 효율성을 목표로하지만 bash와 같은 일부는 주로 편의를 목표로하며 속도가 느립니다.
Peter Cordes

2
글쎄, 그것은 마케팅이지만 거의 사실입니다. 컴파일 할 수있는 것처럼, 당신은 고집 <cstdio>하고 포함하지 않을 수있다 . <iostream>-fno-exceptions -fno-rtti -fno-unwind-tables -fno-asynchronous-unwind-tables
KevinZ

11

C ++의 입력 / 출력 기능은 우아하게 작성되었으며 사용하기 쉽도록 설계되었습니다. 여러 측면에서 C ++의 객체 지향 기능에 대한 쇼케이스입니다.

그러나 실제로 대가로 약간의 성능을 포기하지만 운영 체제가 기능을 낮은 수준에서 처리하는 데 걸리는 시간과 비교하면 무시할 만합니다.

C 스타일 함수는 C ++ 표준의 일부이므로 항상 C 스타일 함수로 넘어가거나 이식성을 완전히 포기하고 운영 체제에 대한 직접 호출을 사용할 수 있습니다.


23
"C ++의 입력 / 출력 기능은 얇고 유용한 베니어 뒤에 Cthulian 특성을 숨기려고 애 쓰고있는 무시 무시한 괴물입니다. 아마도 더 정확할 것입니다.
user673679

3
@ user673679 : 매우 그렇습니다. C ++ I / O 스트림의 가장 큰 문제는 바로 아래에있는 것입니다. 실제로 많은 복잡성이 진행되고 있으며,이를 처리 한 사람 ( std::basic_*stream아래로 참조 )은 들어오는 문제를 알고 있습니다. 그것들은 널리 일반화되고 상속을 통해 확장되도록 설계되었습니다. 그러나 결국 아무도 복잡성을 잃었 기 때문에 (말 그대로 iostream에 쓰여진 책이 있습니다) 너무 많은 새로운 라이브러리가 태어났습니다 (예 : 부스트, ICU 등). 나는 우리가이 실수에 대한 비용을 지불하지 않을 것이라고 의심한다.
edmz

1

다른 답변에서 볼 수 있듯이 일반 라이브러리에서 링크하고 복잡한 생성자를 호출하면 비용을 지불합니다. 여기에 특별한 질문은 없습니다. 나는 실제 측면을 지적 할 것이다 :

  1. Barne은 효율성이 C ++이 아닌 C에 머무르는 이유가되지 않도록하는 핵심 설계 원칙을 가지고있었습니다. 즉, 이러한 효율성을 확보하기 위해주의를 기울여야하며, 항상 효과가 있지만 C 사양 내에서 '기술적으로'그렇지 않은 효율성이 가끔 있습니다. 예를 들어, 비트 필드의 레이아웃은 실제로 지정되지 않았습니다.

  2. ostream을 통해보십시오. 맙소사! 나는 거기에서 비행 시뮬레이터를 발견하는 것에 놀라지 않을 것입니다. stdlib의 printf ()조차도 약 50K를 실행합니다. 이것들은 게으른 프로그래머가 아닙니다. printf 크기의 절반은 대부분의 사람들이 사용하지 않는 간접적 인 정밀 인수와 관련이 있습니다. 거의 모든 제약이있는 프로세서 라이브러리는 printf 대신 자체 출력 코드를 만듭니다.

  3. 크기가 커지면 일반적으로 더 유연하고 유연한 경험이 제공됩니다. 유사하게, 자동 판매기는 몇 동전에 커피와 같은 물질의 컵을 판매하며 전체 거래는 1 분이 걸립니다. 좋은 식당에 들어가려면 테이블 설정, 앉기, 주문, 대기, 멋진 컵 받기, 청구서 받기, 양식 선택, 팁 추가, 퇴근길에 좋은 날이되기를 바랍니다. 그것은 다른 경험이며, 복잡한 식사를 위해 친구들과 함께 들어가면 더 편리합니다.

  4. K & R C는 거의 없지만 사람들은 여전히 ​​ANSI C를 작성합니다. 필자의 경험에 따르면 항상 몇 가지 구성 조정을 사용하여 C ++ 컴파일러로 컴파일하여 드래그되는 것을 제한합니다. 다른 언어에 대한 좋은 주장이 있습니다. ; 더 스마트 한 필드 패킹 및 메모리 레이아웃에 대한 몇 가지 좋은 주장이있었습니다. IMHO 저는 모든 언어 디자인이 Python of Zen 과 같은 목표 목록으로 시작해야한다고 생각합니다 .

재미있는 토론이었습니다. 당신은 왜 마술처럼 작고, 단순하고, 우아하고, 완전하고 유연한 라이브러리를 가질 수 없는지 묻습니다.

답이 없습니다. 답변이 없습니다. 그게 답입니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.