Di C ++, apakah saya membayar untuk makanan yang tidak saya makan?


170

Mari kita perhatikan contoh halo dunia berikut dalam C dan C ++:

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

Ketika saya mengkompilasi mereka di godbolt ke assembly, ukuran kode C hanya 9 baris ( gcc -O3):

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

Tetapi ukuran kode C ++ adalah 22 baris ( 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

... yang jauh lebih besar.

Sangat terkenal bahwa di C ++ Anda membayar untuk apa yang Anda makan. Jadi, dalam hal ini, apa yang saya bayar?


3
Komentar bukan untuk diskusi panjang; percakapan ini telah dipindahkan ke obrolan .
Samuel Liew


26
Belum pernah mendengar istilah yang eatterkait dengan C ++. Saya percaya maksud Anda: "Anda hanya membayar untuk apa yang Anda gunakan "?
Giacomo Alzetta

7
@GiacomoAlzetta, ... ini adalah bahasa sehari-hari, menerapkan konsep prasmanan sepuasnya. Menggunakan istilah yang lebih tepat tentu lebih disukai dengan audiens global, tetapi sebagai penutur bahasa Inggris Amerika asli, judulnya masuk akal bagi saya.
Charles Duffy

5
@ trolley813 Kebocoran memori tidak ada hubungannya dengan kutipan dan pertanyaan OP. Maksud dari "Anda hanya membayar untuk apa yang Anda gunakan" / "Anda tidak membayar untuk apa yang tidak Anda gunakan" adalah untuk mengatakan bahwa tidak ada hit kinerja yang diambil jika Anda tidak menggunakan fitur / abstraksi tertentu. Kebocoran memori tidak ada hubungannya dengan ini sama sekali, dan ini hanya menunjukkan bahwa istilah eatitu lebih ambigu dan harus dihindari.
Giacomo Alzetta

Jawaban:


60

Apa yang Anda bayar adalah untuk memanggil perpustakaan yang berat (tidak seberat mencetak ke konsol). Anda menginisialisasi ostreamobjek. Ada beberapa penyimpanan tersembunyi. Kemudian, Anda menelepon std::endlyang bukan sinonim untuk \n. The iostreamperpustakaan membantu Anda menyesuaikan banyak pengaturan dan menempatkan beban pada prosesor daripada programmer. Ini yang Anda bayar.

Mari kita tinjau kodenya:

.LC0:
        .string "Hello world"
main:

Menginisialisasi objek oout + 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)

Memanggil coutlagi untuk mencetak baris baru dan menyiram

    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

Inisialisasi penyimpanan statis:

_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

Juga, penting untuk membedakan antara bahasa dan perpustakaan.

BTW, ini hanya sebagian dari cerita. Anda tidak tahu apa yang tertulis dalam fungsi yang Anda panggil.


5
Sebagai catatan tambahan, pengujian menyeluruh akan menunjukkan bahwa menambahkan program C ++ dengan "ios_base :: sync_with_stdio (false);" dan "cin.tie (NULL);" akan membuat cout lebih cepat dari printf (Printf memiliki format string overhead). Yang pertama menghilangkan overhead dari memastikan cout; printf; coutmenulis secara berurutan (Karena mereka memiliki buffer sendiri). Yang kedua akan melakukan sinkronisasi coutdan cin, menyebabkan cout; cinberpotensi meminta pengguna untuk informasi terlebih dahulu. Pembilasan akan memaksanya untuk melakukan sinkronisasi hanya ketika Anda benar-benar membutuhkannya.
Nicholas Pipitone

Hai Nicholas, terima kasih banyak telah menambahkan catatan berguna ini.
Arash

"sangat penting untuk membedakan antara bahasa dan perpustakaan": Ya, tetapi perpustakaan standar yang menggunakan bahasa adalah satu-satunya yang tersedia di mana-mana, jadi itu adalah yang digunakan di mana-mana (dan ya, perpustakaan standar C adalah bagian dari spesifikasi C ++, sehingga dapat digunakan saat diinginkan). Mengenai "Anda tidak tahu apa yang tertulis dalam fungsi yang Anda panggil": Anda dapat menautkan secara statis jika Anda benar-benar ingin tahu, dan memang kode panggilan yang Anda periksa mungkin tidak relevan.
Peter - Pasang kembali Monica

211

Jadi, dalam hal ini, apa yang saya bayar?

std::coutlebih kuat dan rumit daripada printf. Ini mendukung hal-hal seperti locales, flag format stateful, dan banyak lagi.

Jika Anda tidak membutuhkannya, gunakan std::printfatau std::puts- tersedia <cstdio>.


Sangat terkenal bahwa di C ++ Anda membayar untuk apa yang Anda makan.

Saya juga ingin memperjelas bahwa C ++ ! = The C ++ Standard Library. Perpustakaan Standar seharusnya bertujuan umum dan "cukup cepat", tetapi seringkali lebih lambat daripada implementasi khusus dari apa yang Anda butuhkan.

Di sisi lain, bahasa C ++ berusaha untuk memungkinkan untuk menulis kode tanpa membayar biaya tersembunyi tambahan yang tidak perlu (misalnya memilih ikut virtual, tidak ada pengumpulan sampah).


4
+1 untuk mengatakan Perpustakaan Standar seharusnya bertujuan umum dan "cukup cepat", tetapi sering kali lebih lambat daripada implementasi khusus dari apa yang Anda butuhkan. Banyak yang tampaknya menggunakan komponen STL tanpa mempertimbangkan implikasi kinerja vs menggulirkan komponen Anda sendiri.
Craig Estey

7
@Craig OTOH banyak bagian dari perpustakaan standar biasanya lebih cepat dan lebih benar daripada apa yang biasanya dapat dihasilkan.
Peter - Reinstate Monica

2
@ PeterA.Schneider OTOH, ketika versi STL lebih lambat 20x-30x, memutar sendiri adalah hal yang baik. Lihat jawaban saya di sini: codereview.stackexchange.com/questions/191747/... Di dalamnya, yang lain juga menyarankan [setidaknya sebagian] menggulung sendiri.
Craig Estey

1
@CraigEstey Vektor adalah (terlepas dari alokasi dinamis awal yang bisa signifikan, tergantung pada berapa banyak pekerjaan yang akan dilakukan pada akhirnya dengan contoh yang diberikan) tidak kurang efisien daripada array C; itu dirancang untuk tidak menjadi. Harus berhati-hati untuk tidak menyalinnya, menyediakan ruang yang cukup pada awalnya dll, tetapi semua itu harus dilakukan dengan array juga, dan kurang aman. Sehubungan dengan contoh Anda yang ditautkan: Ya, vektor vektor akan (kecuali dioptimalkan pergi) menimbulkan tipuan ekstra dibandingkan dengan array 2D, tapi saya berasumsi bahwa efisiensi 20x tidak di-root di sana tetapi dalam algoritma.
Peter - Pasang kembali Monica

174

Anda tidak membandingkan C dan C ++. Anda membandingkan printfdan std::cout, yang mampu melakukan berbagai hal (lokal, format stateful, dll).

Coba gunakan kode berikut untuk perbandingan. Godbolt menghasilkan rakitan yang sama untuk kedua file (diuji dengan 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);
    }
}


Sorakan karena menunjukkan kode yang setara dan menjelaskan alasannya.
HackSlash

134

Daftar Anda memang membandingkan apel dan jeruk, tetapi bukan karena alasan yang tersirat dalam sebagian besar jawaban lain.

Mari kita periksa apa kode Anda sebenarnya:

C:

  • cetak satu string, "Hello world\n"

C ++:

  • streaming string "Hello world"ke dalamstd::cout
  • stream std::endlmanipulator kestd::cout

Tampaknya kode C ++ Anda melakukan pekerjaan dua kali lebih banyak. Untuk perbandingan yang adil, kita harus menggabungkan ini:

#include <iostream>

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

... dan tiba-tiba kode assembly Anda untuk mainterlihat sangat mirip dengan C:

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

Bahkan, kita dapat membandingkan kode C dan C ++ baris demi baris, dan ada sedikit perbedaan :

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

Satu-satunya perbedaan nyata adalah bahwa dalam C ++ kita memanggil operator <<dengan dua argumen ( std::coutdan string). Kita dapat menghapus bahkan sedikit perbedaan dengan menggunakan persamaan C yang lebih dekat:, fprintfyang juga memiliki argumen pertama yang menentukan aliran.

Ini meninggalkan kode rakitan _GLOBAL__sub_I_main, yang dihasilkan untuk C ++ tetapi bukan C. Ini adalah satu-satunya overhead sebenarnya yang terlihat dalam daftar rakitan ini ( tentu saja , lebih banyak overhead tidak terlihat untuk kedua bahasa). Kode ini melakukan pengaturan satu kali untuk beberapa fungsi pustaka standar C ++ pada awal program C ++.

Tetapi, seperti dijelaskan dalam jawaban lain, perbedaan yang relevan antara kedua program ini tidak akan ditemukan dalam output perakitan mainfungsi karena semua pengangkatan berat terjadi di belakang layar.


21
Kebetulan, runtime C juga perlu diatur, dan ini terjadi pada fungsi yang dipanggil _starttetapi kodenya adalah bagian dari pustaka runtime C. Bagaimanapun ini terjadi untuk C dan C ++.
Konrad Rudolph

2
@Deduplicator: Sebenarnya, secara default perpustakaan iostream tidak melakukan apa pun buffering std::coutdan bukannya lewat I / O untuk pelaksanaan stdio (yang menggunakan mekanisme buffer sendiri). Secara khusus, ketika terhubung ke (apa yang dikenal sebagai) terminal interaktif, maka secara default Anda tidak akan pernah melihat output buffered sepenuhnya saat menulis std::cout. Anda harus secara eksplisit menonaktifkan sinkronisasi dengan stdio jika Anda ingin pustaka iostream menggunakan mekanisme bufferingnya sendiri std::cout.

6
@KonradRudolph: Sebenarnya, printftidak perlu menyiram aliran di sini. Bahkan, dalam kasus penggunaan umum (output diarahkan ke file), Anda biasanya akan menemukan bahwa printfpernyataan tidak rata. Hanya ketika output buffer line atau unbuffered akan printfmemicu flush.

2
@PeterCordes: Benar, Anda tidak dapat memblokir dengan buffer output yang tidak disiram, tetapi Anda dapat menemukan kejutan di mana program menerima input Anda dan berjalan tanpa menampilkan output yang diharapkan. Saya tahu ini karena saya memiliki kesempatan untuk men-debug "Bantuan, program saya tergantung selama input tetapi saya tidak tahu mengapa!" yang telah memberikan pengembang lain cocok untuk beberapa hari.

2
@PeterCordes: Argumen yang saya buat adalah "tulis apa yang Anda maksudkan" - baris baru sesuai ketika Anda bermaksud untuk output yang akhirnya tersedia, dan endl sesuai ketika Anda bermaksud agar output tersedia segera.

53

Sangat terkenal bahwa di C ++ Anda membayar untuk apa yang Anda makan. Jadi, dalam hal ini, apa yang saya bayar?

Sederhana saja. Anda membayar std::cout. "Anda hanya membayar apa yang Anda makan" tidak berarti "Anda selalu mendapatkan harga terbaik". Tentu, printflebih murah. Orang bisa membantahnyastd::cout itu lebih aman dan lebih fleksibel, sehingga biayanya yang lebih besar dapat dibenarkan (harganya lebih banyak, tetapi memberikan nilai lebih), tetapi itu tidak tepat. Anda tidak menggunakan printf, Anda menggunakan std::cout, jadi Anda membayar untuk menggunakan std::cout. Anda tidak membayar untuk menggunakan printf.

Contoh yang baik adalah fungsi virtual. Fungsi virtual memiliki beberapa biaya runtime dan persyaratan ruang - tetapi hanya jika Anda benar-benar menggunakannya. Jika Anda tidak menggunakan fungsi virtual, Anda tidak membayar apa pun.

Beberapa komentar

  1. Bahkan jika kode C ++ mengevaluasi lebih banyak instruksi perakitan, itu masih sedikit instruksi, dan setiap overhead kinerja masih mungkin dikerdilkan oleh operasi I / O yang sebenarnya.

  2. Sebenarnya, kadang-kadang bahkan lebih baik daripada "di C ++ Anda membayar apa yang Anda makan". Sebagai contoh, kompiler dapat menyimpulkan bahwa panggilan fungsi virtual tidak diperlukan dalam beberapa keadaan, dan mengubahnya menjadi panggilan non-virtual. Itu berarti Anda dapat memperoleh fungsi virtual secara gratis . Bukankah itu hebat?


6
Anda tidak mendapatkan fungsi virtual secara gratis. Anda masih harus membayar biaya untuk menulisnya, dan kemudian men-debug transformasi kompiler dari kode Anda ketika tidak sesuai dengan ide Anda tentang apa yang seharusnya dilakukan.
alephzero

2
@ alephzero Saya tidak yakin itu sangat relevan untuk membandingkan biaya pengembangan dengan biaya kinerja.

Kesempatan besar untuk permainan kata sia-sia ... Anda bisa menggunakan kata 'kalori' dan bukan 'harga'. Dari itu Anda bisa mengatakan bahwa C ++ lebih gemuk dari C. Atau setidaknya ... kode spesifik yang dimaksud (saya bias terhadap C ++ yang mendukung C jadi saya tidak bisa cukup melampaui). Sayang. @Bilkokuya Mungkin tidak relevan dalam semua kasus, tapi itu pasti sesuatu yang tidak boleh diabaikan. Jadi itu relevan secara keseluruhan.
Pryftan

46

"Daftar rakitan untuk printf" BUKAN untuk printf, tetapi untuk put (jenis optimasi kompiler?); printf jauh lebih kompleks daripada menempatkan ... jangan lupa!


13
Sejauh ini jawaban terbaik, karena semua yang lain terpaku pada ikan herring merah tentang std::coutinternal, yang tidak terlihat dalam daftar perakitan.
Konrad Rudolph

12
Daftar rakitan adalah untuk panggilan puts , yang terlihat sama dengan panggilan printfjika Anda hanya melewatkan string format tunggal dan nol argumen tambahan. (Kecuali akan ada juga xor %eax,%eaxkarena kita melewatkan nol argumen FP dalam register ke fungsi variadic.) Tidak satu pun dari ini adalah implementasinya, hanya meneruskan pointer ke string ke fungsi library. Tapi ya, mengoptimalkan printfuntuk putssesuatu gcc tidak untuk format yang hanya memiliki "%s", atau ketika tidak ada konversi, dan string berakhir dengan baris baru.
Peter Cordes

45

Saya melihat beberapa jawaban yang valid di sini, tetapi saya akan sedikit lebih detail.

Lompat ke ringkasan di bawah ini untuk menjawab pertanyaan utama Anda jika Anda tidak ingin membaca seluruh dinding teks ini.


Abstraksi

Jadi, dalam hal ini, apa yang saya bayar?

Anda membayar abstraksi . Mampu menulis kode yang lebih sederhana dan lebih ramah manusia datang dengan biaya. Dalam C ++, yang merupakan bahasa berorientasi objek, hampir semuanya adalah objek. Saat Anda menggunakan objek apa pun, tiga hal utama akan selalu terjadi di bawah tenda:

  1. Pembuatan objek, pada dasarnya alokasi memori untuk objek itu sendiri dan datanya.
  2. Inisialisasi objek (biasanya melalui beberapa init()metode). Biasanya alokasi memori terjadi di bawah tenda sebagai hal pertama dalam langkah ini.
  3. Penghancuran objek (tidak selalu).

Anda tidak melihatnya dalam kode, tetapi setiap kali Anda menggunakan objek, ketiga hal di atas perlu terjadi. Jika Anda melakukan semuanya secara manual, kode jelas akan jauh lebih lama.

Sekarang, abstraksi dapat dibuat secara efisien tanpa menambahkan overhead: inlining metode dan teknik lainnya dapat digunakan oleh kompiler dan programmer untuk menghilangkan overhead abstraksi, tetapi ini bukan kasus Anda.

Apa yang sebenarnya terjadi di C ++?

Ini dia, rusak:

  1. The std::ios_basekelas diinisialisasi, yang merupakan kelas dasar untuk semua I / O yang terkait.
  2. The std::coutobjek diinisialisasi.
  3. String Anda dimuat dan diteruskan ke std::__ostream_insert, yang (seperti yang sudah Anda ketahui namanya) adalah metode std::cout(pada dasarnya <<operator) yang menambahkan string ke aliran.
  4. cout::endljuga diteruskan ke std::__ostream_insert.
  5. __std_dso_handlediteruskan ke __cxa_atexit, yang merupakan fungsi global yang bertanggung jawab untuk "pembersihan" sebelum keluar dari program. __std_dso_handlesendiri dipanggil oleh fungsi ini untuk mendelokasi dan menghancurkan objek global yang tersisa.

Jadi menggunakan C == tidak membayar apa pun?

Dalam kode C, sangat sedikit langkah yang terjadi:

  1. String Anda dimuat dan diteruskan ke putsmelalui ediregister.
  2. puts dipanggil.

Tidak ada benda di mana pun, maka tidak perlu menginisialisasi / menghancurkan apa pun.

Namun ini tidak berarti bahwa Anda tidak "membayar" untuk apa pun di C . Anda masih membayar abstraksi, dan juga inisialisasi pustaka standar C dan resolusi dinamis printffungsi (atau, sebenarnya puts, yang dioptimalkan oleh kompiler karena Anda tidak memerlukan format string) masih terjadi di bawah tenda.

Jika Anda menulis program ini dalam perakitan murni, akan terlihat seperti ini:

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

Yang pada dasarnya hanya menghasilkan memanggil write syscall diikuti oleh exitsyscall. Sekarang ini akan menjadi minimum untuk mencapai hal yang sama.


Untuk meringkas

C adalah cara yang lebih sederhana , dan hanya melakukan minimum yang diperlukan, meninggalkan kontrol penuh kepada pengguna, yang mampu mengoptimalkan dan menyesuaikan sepenuhnya apa pun yang mereka inginkan. Anda memberi tahu prosesor untuk memuat string dalam register dan kemudian memanggil fungsi pustaka untuk menggunakan string itu. C ++ di sisi lain jauh lebih kompleks dan abstrak . Ini memiliki keuntungan yang sangat besar ketika menulis kode yang rumit, dan memungkinkan untuk lebih mudah untuk menulis dan kode yang lebih ramah manusia, tetapi jelas ada biaya. Akan selalu ada kelemahan dalam kinerja di C ++ jika dibandingkan dengan C dalam kasus-kasus seperti ini, karena C ++ menawarkan lebih dari apa yang dibutuhkan untuk menyelesaikan tugas-tugas dasar seperti itu, dan dengan demikian menambah lebih banyak overhead .

Menjawab pertanyaan utama Anda :

Apakah saya membayar untuk apa yang tidak saya makan?

Dalam kasus khusus ini, ya . Anda tidak mengambil keuntungan dari apa pun yang ditawarkan oleh C ++ lebih dari C, tetapi itu hanya karena tidak ada apa pun dalam potongan kode sederhana yang dapat membantu C ++: sangat sederhana sehingga Anda benar-benar tidak memerlukan C ++ sama sekali.


Oh, dan satu hal lagi!

Kelebihan C ++ mungkin tidak terlihat jelas pada pandangan pertama, karena Anda menulis sebuah program yang sangat sederhana dan kecil, tetapi lihatlah contoh yang sedikit lebih kompleks dan lihat perbedaannya (kedua program melakukan hal yang persis sama):

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

Semoga Anda bisa melihat dengan jelas apa yang saya maksud di sini. Juga perhatikan bagaimana dalam C Anda harus mengelola memori pada tingkat yang lebih rendah menggunakan mallocdan freebagaimana Anda harus lebih berhati-hati tentang pengindeksan dan ukuran, dan bagaimana Anda harus sangat spesifik saat mengambil input dan mencetak.


27

Ada beberapa kesalahpahaman untuk memulai. Pertama, program C ++ tidak menghasilkan 22 instruksi, ini lebih seperti 22.000 dari mereka (saya menarik nomor itu dari topi saya, tetapi kira-kira di stadion baseball itu). Juga, kode C. tidak menghasilkan 9 instruksi, baik. Hanya itu yang Anda lihat.

Apa yang dilakukan kode C adalah, setelah melakukan banyak hal yang tidak Anda lihat, ia memanggil fungsi dari CRT (yang biasanya tetapi tidak harus hadir sebagai shared lib), kemudian tidak memeriksa nilai pengembalian atau menangani kesalahan, dan uang jaminan. Tergantung pada kompiler dan pengaturan optimasi itu bahkan tidak benar-benar memanggil printftetapi puts, atau sesuatu yang lebih primitif.
Anda bisa menulis kurang lebih program yang sama (kecuali untuk beberapa fungsi init yang tak terlihat) di C ++ juga, jika saja Anda memanggil fungsi yang sama dengan cara yang sama. Atau, jika Anda ingin super-benar, fungsi yang sama diawali dengan std::.

Kode C ++ yang sesuai pada kenyataannya tidak semuanya sama. Meskipun keseluruhannya <iostream>terkenal sebagai babi jelek yang gemuk yang menambahkan biaya overhead yang besar untuk program kecil (dalam program "nyata" Anda tidak terlalu memperhatikannya), interpretasi yang agak lebih adil adalah bahwa ia melakukan hal yang buruk. banyak hal yang tidak Anda lihat dan yang berfungsi . Termasuk tetapi tidak terbatas pada pemformatan magis dari hampir semua barang sembarangan, termasuk berbagai format angka dan lokal dan yang lainnya, dan buffering, dan penanganan kesalahan yang tepat. Menangani kesalahan? Ya, coba tebak, mengeluarkan string sebenarnya bisa gagal, dan tidak seperti program C, program C ++ akan melakukannya tidak mengabaikan ini diam-diam. Mempertimbangkan apastd::ostreamtidak di bawah tenda, dan tanpa ada yang tahu, itu sebenarnya cukup ringan. Tidak seperti saya menggunakannya karena saya benci aliran sintaksis dengan penuh gairah. Tapi tetap saja, itu luar biasa jika Anda mempertimbangkan apa yang dilakukannya.

Tapi tentu saja, C ++ secara keseluruhan tidak seefisien C dapat. Itu tidak bisa seefisien karena itu bukan hal yang sama dan tidak melakukan hal yang sama. Jika tidak ada yang lain, C ++ menghasilkan pengecualian (dan kode untuk menghasilkan, menangani, atau gagal pada mereka) dan itu memberikan beberapa jaminan bahwa C tidak memberikan. Jadi, tentu saja, program C ++ perlu sedikit lebih besar. Namun, dalam gambaran besar, ini tidak masalah sama sekali. Sebaliknya, untuk program nyata , saya jarang menemukan C ++ berkinerja lebih baik karena untuk satu dan lain alasan, tampaknya memberikan optimisasi yang lebih baik. Jangan tanya kenapa, khususnya, saya tidak akan tahu.

Jika, alih-alih api-dan-lupakan-harapan-untuk-yang terbaik Anda peduli untuk menulis kode C yang benar (yaitu Anda benar-benar memeriksa kesalahan, dan program berperilaku dengan benar di hadapan kesalahan) maka perbedaannya adalah marjinal, jika ada.


16
Jawaban yang sangat bagus, kecuali bahwa pernyataan ini: "Tapi tentu saja, C ++ secara keseluruhan tidak seefisien C bisa" adalah salah. C ++ bisa seefisien C, dan kode tingkat tinggi yang cukup bisa lebih efisien daripada kode C yang setara. Ya, C ++ memiliki beberapa overhead karena harus menangani pengecualian tetapi pada kompiler modern, overhead itu dapat diabaikan dibandingkan dengan peningkatan kinerja dari abstraksi bebas biaya yang lebih baik.
Konrad Rudolph

Jika saya mengerti dengan benar, apakah ada std::coutpengecualian juga?
Saher

6
@ Saher: Ya, tidak, mungkin. std::coutadalah a std::basic_ostreamdan yang dapat dilempar, dan itu dapat mengkaji kembali pengecualian yang terjadi jika dikonfigurasi untuk melakukannya atau ia dapat menelan pengecualian. Masalahnya, hal - hal dapat gagal, dan C ++ serta lib standar C ++ (kebanyakan) dibangun sehingga kegagalan tidak mudah luput dari perhatian. Ini adalah gangguan dan berkah (tapi, lebih banyak berkah dari gangguan). C di sisi lain hanya menunjukkan jari tengah Anda. Anda tidak memeriksa kode kembali, Anda tidak pernah tahu apa yang terjadi.
Damon

1
@KonradRudolph: Benar, inilah yang saya coba tunjukkan dengan "Saya jarang menemukan C ++ berkinerja lebih baik karena karena satu dan lain alasan, sepertinya meminjamkan untuk optimasi yang lebih menguntungkan. Jangan tanya saya mengapa secara khusus" . Tidak jelas mengapa, tetapi tidak jarang itu hanya mengoptimalkan lebih baik. Untuk alasan apa pun. Anda akan berpikir itu semua sama dengan pengoptimal, tetapi tidak.
Damon

22

Anda membayar kesalahan. Pada tahun 80-an, ketika kompiler tidak cukup baik untuk memeriksa string format, overloading operator dipandang sebagai cara yang baik untuk menegakkan kemiripan keamanan jenis selama io. Namun, setiap fitur spanduknya diimplementasikan dengan buruk atau secara konsep bangkrut sejak awal:

<iomanip>

Bagian yang paling menjijikkan dari C ++ stream io api adalah keberadaan pustaka header pemformatan ini. Selain stateful dan jelek dan rentan kesalahan, itu pasangan diformat ke aliran.

Misalkan Anda ingin mencetak garis dengan 8 digit nol diisi hex unsigned int diikuti oleh spasi diikuti oleh ganda dengan 3 tempat desimal. Dengan <cstdio>, Anda bisa membaca string format ringkas. Dengan <ostream>, Anda harus menyimpan status lama, atur perataan ke kanan, atur karakter isian, atur lebar isian, atur basis ke hex, output integer, kembalikan keadaan tersimpan (jika tidak, format integer Anda akan mencemari format float Anda), mengeluarkan ruang , atur notasi untuk diperbaiki, atur presisi, output ganda dan baris baru, lalu kembalikan format lama.

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

Operator Kelebihan

<iostream> adalah anak poster tentang bagaimana tidak menggunakan kelebihan operator:

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

Performa

std::cout beberapa kali lebih lambat printf() . Merebaknya radang usus dan pengiriman maya memang memakan korban.

Keamanan Thread

Keduanya <cstdio>dan <iostream>aman dalam setiap fungsi panggilan adalah atom. Tapi, printf()lebih banyak dilakukan per panggilan. Jika Anda menjalankan program berikut dengan <cstdio>opsi, Anda hanya akan melihat deretan f. Jika Anda menggunakan <iostream>mesin multicore, kemungkinan Anda akan melihat sesuatu yang lain.

// 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
}

Retort untuk contoh ini adalah bahwa kebanyakan orang menjalankan disiplin untuk tidak pernah menulis ke deskriptor file tunggal dari beberapa utas. Nah, dalam hal ini, Anda harus mengamati bahwa <iostream>akan membantu mengambil kunci pada setiap <<dan setiap >>. Sedangkan di <cstdio>, Anda tidak akan sering mengunci, dan Anda bahkan memiliki pilihan untuk tidak mengunci.

<iostream> mengeluarkan lebih banyak kunci untuk mencapai hasil yang kurang konsisten.


2
Sebagian besar implementasi printf memiliki fitur yang sangat berguna untuk lokalisasi: parameter bernomor. Jika Anda perlu menghasilkan beberapa output dalam dua bahasa yang berbeda (seperti bahasa Inggris dan Perancis) dan urutan kata berbeda, Anda dapat menggunakan printf yang sama dengan string pemformatan yang berbeda, dan dapat mencetak parameter dalam urutan yang berbeda.
gnasher729

2
Pemformatan aliran yang pasti membuat begitu banyak kesulitan untuk menemukan bug, saya tidak tahu harus berkata apa. Jawaban yang bagus Akan membenarkan lebih dari sekali jika saya bisa.
mathreadler

6
std::coutBeberapa kali lebih lambat printf()” - Klaim ini diulangi di seluruh internet tetapi tidak berlaku lama. Implementasi IOstream modern berfungsi setara printf. Yang terakhir ini juga melakukan pengiriman virtual secara internal untuk menangani buffered stream dan IO terlokalisasi (dilakukan oleh sistem operasi tetapi tetap dilakukan).
Konrad Rudolph

3
@KevinZ Dan itu bagus tetapi melakukan pembandingan satu panggilan tunggal, spesifik, yang menampilkan kekuatan spesifik fmt (banyak format berbeda dalam satu string). Dalam penggunaan yang lebih umum, perbedaan antara printfdan coutmenyusut. Kebetulan ada banyak tolok ukur seperti itu di situs ini.
Konrad Rudolph

3
@KonradRudolph Itu juga tidak benar. Microbenchmark sering memperkirakan biaya mengasapi dan tipuan karena mereka tidak menghabiskan sumber daya terbatas tertentu (baik itu register, icache, memori, prediktor cabang) di mana program nyata akan. Ketika Anda menyinggung "penggunaan yang lebih umum", pada dasarnya mengatakan bahwa Anda memiliki lebih banyak mengasapi di tempat lain, yang baik-baik saja, tetapi di luar topik. Menurut pendapat saya, jika Anda tidak memiliki persyaratan kinerja, Anda tidak perlu memprogram dalam C ++.
KevinZ

18

Selain apa semua jawaban yang lain telah mengatakan,
ada juga fakta bahwa std::endladalah tidak sama dengan'\n' .

Sayangnya ini adalah kesalahpahaman umum. std::endltidak berarti "baris baru",
itu berarti "cetak baris baru dan kemudian siram aliran ". Pembilasan tidak murah!

Sepenuhnya mengabaikan perbedaan antara printfdan std::coutuntuk sesaat, agar secara fungsional setara dengan contoh C Anda, contoh C ++ Anda seharusnya terlihat seperti ini:

#include <iostream>

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

Dan inilah contoh bagaimana seharusnya contoh Anda jika Anda termasuk pembilasan.

C

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

Saat membandingkan kode, Anda harus selalu berhati-hati bahwa Anda membandingkan suka untuk suka dan bahwa Anda memahami implikasi dari apa yang dilakukan kode Anda. Kadang-kadang bahkan contoh paling sederhana pun lebih rumit daripada yang disadari sebagian orang.


Sebenarnya, menggunakan std::endl adalah fungsi yang setara dengan menulis baris baru ke aliran stdio buffer-line. stdout, khususnya, harus berupa buffer-line atau unbuffered ketika terhubung ke perangkat interaktif. Linux, saya percaya, menekankan opsi line-buffered.

Bahkan, pustaka iostream tidak memiliki mode buffer-line ... cara untuk mencapai efek buffer-line adalah dengan tepat digunakan std::endluntuk menampilkan baris baru.

@Hurkyl Bersikeras? Lalu apa gunanyasetvbuf(3) ? Atau apakah Anda bermaksud mengatakan bahwa defaultnya adalah buffer line? FYI: Biasanya semua file memiliki buffer blok. Jika aliran merujuk ke terminal (seperti stdout biasanya), itu adalah buffer line. Standard stream stream stderr selalu tidak dibangun secara default.
Pryftan

Tidak printfmemerah secara otomatis saat bertemu dengan karakter baris baru?
bool3max

1
@ bool3max Itu hanya akan memberi tahu saya apa lingkungan saya, mungkin berbeda di lingkungan lain. Bahkan jika itu berperilaku sama di semua implementasi paling populer, itu tidak berarti ada tepi kasus di suatu tempat. Itulah sebabnya stanard sangat penting - standar menentukan apakah sesuatu harus sama untuk semua implementasi atau apakah itu diperbolehkan bervariasi di antara implementasi.
Pharap

16

Sementara jawaban teknis yang ada sudah benar, saya pikir pertanyaannya akhirnya berasal dari kesalahpahaman ini:

Sangat terkenal bahwa di C ++ Anda membayar untuk apa yang Anda makan.

Ini hanya pembicaraan pemasaran dari komunitas C ++. (Agar adil, ada pembicaraan pemasaran di setiap komunitas bahasa.) Itu tidak berarti sesuatu yang konkret yang bisa Anda andalkan.

"Anda membayar untuk apa yang Anda gunakan" seharusnya berarti bahwa fitur C ++ hanya memiliki overhead jika Anda menggunakan fitur itu. Tetapi definisi "suatu fitur" tidak granular tanpa batas. Seringkali Anda akan mengaktifkan fitur yang memiliki banyak aspek, dan meskipun Anda hanya membutuhkan sebagian dari aspek-aspek tersebut, seringkali tidak praktis atau mungkin bagi implementasi untuk menghadirkan fitur tersebut secara parsial.

Secara umum, banyak (walaupun bisa dibilang tidak semua) bahasa berusaha untuk menjadi efisien, dengan berbagai tingkat keberhasilan. C ++ ada di suatu tempat dalam skala, tetapi tidak ada yang istimewa atau ajaib tentang desainnya yang akan memungkinkannya untuk berhasil sepenuhnya dalam tujuan ini.


1
Hanya ada dua hal yang dapat saya pikirkan di mana Anda membayar sesuatu yang tidak Anda gunakan: pengecualian dan RTTI. Dan saya tidak berpikir itu pembicaraan pemasaran; C ++ pada dasarnya adalah C yang lebih kuat, yang juga "tidak membayar untuk apa yang Anda gunakan".
Rakete1111

2
@ Rakete1111 Sudah lama ditetapkan bahwa jika pengecualian tidak melempar, mereka tidak dikenakan biaya. Jika program Anda melempar secara konsisten, itu harus dirancang ulang. Jika kondisi kegagalan di luar kendali Anda, Anda harus memeriksa kondisi dengan cek kesehatan kembali bool, sebelum memanggil metode yang bergantung pada kondisi yang salah.
schulmaster

1
@schulmaster: Pengecualian dapat memaksakan batasan desain ketika kode yang ditulis dalam C ++ perlu berinteraksi dengan kode yang ditulis dalam bahasa lain, karena transfer kontrol non-lokal hanya dapat bekerja dengan lancar di seluruh modul jika modul tahu bagaimana berkoordinasi satu sama lain.
supercat

1
(walaupun bisa dibilang tidak semua) bahasa berusaha untuk menjadi efisien . Jelas tidak semua: Bahasa pemrograman esoteris berusaha keras untuk menjadi novel / menarik, tidak efisien. esolangs.org . Beberapa dari mereka, seperti BrainFuck, terkenal tidak efisien. Atau misalnya, Bahasa Pemrograman Shakespeare, 227 byte ukuran minimum (codegolf) untuk Mencetak semua bilangan bulat . Keluar dari bahasa yang dimaksudkan untuk penggunaan produksi, sebagian besar memang bertujuan untuk efisiensi, tetapi beberapa (seperti bash) sebagian besar bertujuan untuk kenyamanan dan dikenal lambat.
Peter Cordes

2
Ya, itu pemasaran tetapi hampir sepenuhnya benar. Anda dapat tetap berpegang pada <cstdio>dan tidak memasukkan <iostream>, sama seperti cara Anda mengompilasinya -fno-exceptions -fno-rtti -fno-unwind-tables -fno-asynchronous-unwind-tables.
KevinZ

11

Fungsi Input / Output dalam C ++ ditulis dengan elegan dan dirancang agar mudah digunakan. Dalam banyak hal mereka adalah karya untuk fitur berorientasi objek di C ++.

Tetapi Anda memang menyerah sedikit kinerja sebagai imbalan, tapi itu diabaikan dibandingkan dengan waktu yang diambil oleh sistem operasi Anda untuk menangani fungsi-fungsi di tingkat yang lebih rendah.

Anda selalu dapat kembali ke fungsi gaya C karena mereka adalah bagian dari standar C ++, atau mungkin menyerahkan portabilitas sama sekali dan menggunakan panggilan langsung ke sistem operasi Anda.


23
"Fungsi Input / Output di C ++ adalah monster mengerikan yang berjuang untuk menyembunyikan sifat Cthulian mereka di balik lapisan tipis kegunaan. Dalam banyak hal mereka adalah karya untuk bagaimana tidak merancang kode C ++ modern". Mungkin akan lebih akurat.
user673679

3
@ user673679: Sangat benar. Masalah besar dengan aliran C ++ I / O adalah apa yang ada di bawahnya: benar-benar ada banyak kerumitan yang terjadi, dan siapa saja yang pernah berurusan dengan mereka (saya merujuk ke std::basic_*streambawah) tahu eadaches yang masuk. Mereka dirancang untuk menjadi sangat umum dan diperluas melalui warisan; tetapi pada akhirnya tidak ada yang melakukannya, karena kerumitannya (ada buku yang ditulis di iostreams), sehingga perpustakaan baru lahir hanya untuk itu (misalnya boost, ICU dll). Saya ragu kita akan pernah berhenti membayar kesalahan ini.
edmz

1

Seperti yang Anda lihat di jawaban lain, Anda membayar ketika Anda menautkan di perpustakaan umum dan memanggil konstruktor kompleks. Tidak ada pertanyaan khusus di sini, lebih banyak keluhan. Saya akan menunjukkan beberapa aspek dunia nyata:

  1. Barne memiliki prinsip desain inti untuk tidak membiarkan efisiensi menjadi alasan untuk tetap di C daripada C ++. Yang mengatakan, orang perlu berhati-hati untuk mendapatkan efisiensi ini, dan ada efisiensi sesekali yang selalu berhasil tetapi tidak 'secara teknis' dalam spesifikasi C. Misalnya, tata letak bidang bit tidak benar-benar ditentukan.

  2. Coba cari melalui ostream. Ya Tuhan, itu kembung! Saya tidak akan terkejut menemukan simulator penerbangan di sana. Bahkan printd stdlib () biasanya berjalan sekitar 50K. Ini bukan programmer yang malas: setengah dari ukuran printf ada hubungannya dengan argumen presisi tidak langsung yang kebanyakan orang tidak pernah gunakan. Hampir setiap perpustakaan prosesor yang benar-benar dibatasi membuat kode keluaran sendiri alih-alih printf.

  3. Peningkatan ukuran biasanya memberikan pengalaman yang lebih berisi dan fleksibel. Sebagai analogi, mesin penjual otomatis akan menjual secangkir kopi seperti substansi untuk beberapa koin dan seluruh transaksi membutuhkan waktu kurang dari satu menit. Masuk ke restoran yang baik melibatkan pengaturan meja, duduk, memesan, menunggu, mendapatkan cangkir yang enak, mendapatkan tagihan, membayar formulir pilihan Anda, menambahkan tip, dan berharap hari yang baik di jalan keluar. Ini pengalaman yang berbeda, dan lebih nyaman jika Anda mampir ke teman untuk makan yang kompleks.

  4. Orang-orang masih menulis ANSI C, meskipun jarang K&R C. Pengalaman saya adalah kami selalu mengompilasinya dengan kompiler C ++ menggunakan beberapa konfigurasi tweak untuk membatasi apa yang diseret. Ada argumen bagus untuk bahasa lain: Go menghapus overhead polimorfik dan preprocessor gila ; ada beberapa argumen bagus untuk pengemasan bidang yang lebih cerdas dan tata letak memori. IMHO Saya pikir setiap desain bahasa harus dimulai dengan daftar tujuan, seperti Zen of Python .

Ini merupakan diskusi yang menyenangkan. Anda bertanya mengapa Anda tidak dapat memiliki perpustakaan yang secara ajaib kecil, sederhana, elegan, lengkap, dan fleksibel?

Tidak ada Jawaban. Tidak akan ada jawaban. Itulah jawabannya.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.