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

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 ++에서는 먹는 것을 지불하는 것이 유명합니다. 그래서이 경우, 나는 무엇을 지불하고 있습니까?



답변

당신이 지불하는 것은 무거운 라이브러리 (콘솔에 인쇄하는 것만 큼 무겁지 않은)를 호출하는 것입니다. 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, 이것은 이야기의 일부일뿐입니다. 호출하는 함수에 쓰여진 내용을 모릅니다.


답변

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

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

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


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

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

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


답변

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);
    }
}


답변

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

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

씨:

  • 단일 문자열을 인쇄 "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모든 무거운 리프팅이 장면 뒤에서 발생하기 때문에 함수 의 어셈블리 출력에서 ​​찾을 수 없습니다 .


답변

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

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

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

몇 가지 언급

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

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


답변

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


답변

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

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


추출

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

추상화 비용을 지불하고 있습니다. 더 간단하고 인간 친화적 인 코드를 작성할 수있는 비용이 발생합니다. 객체 지향 언어 인 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인덱싱 및 크기에 대한 더 조심해야하는지, 그리고 입력을 복용하고 인쇄 할 때 매우 구체적으로해야하는지.