Hindari Operator Penambahan Postfix

25

Saya sudah membaca bahwa saya harus menghindari operator kenaikan postfix karena alasan kinerja (dalam kasus tertentu).

Tetapi tidakkah ini mempengaruhi pembacaan kode? Menurutku:

for(int i = 0; i < 42; i++);
    /* i will never equal 42! */

Terlihat lebih baik daripada:

for(int i = 0; i < 42; ++i);
    /* i will never equal 42! */

Tapi ini mungkin hanya karena kebiasaan. Memang, saya belum melihat banyak kegunaan ++i.

Apakah kinerja yang buruk untuk mengorbankan keterbacaan, dalam hal ini? Atau apakah saya hanya buta, dan ++ilebih mudah dibaca daripada i++?

Mateen Ulhaq
sumber
1
Saya menggunakan i++sebelum saya tahu itu bisa mempengaruhi kinerja ++i, jadi saya beralih. Pada awalnya yang terakhir memang terlihat sedikit aneh, tetapi setelah beberapa saat saya terbiasa dan sekarang terasa sealami i++.
gablin
15
++idan i++melakukan hal-hal yang berbeda dalam konteks tertentu, jangan menganggap mereka sama.
Orbling
2
Apakah ini tentang C atau C ++? Mereka adalah dua bahasa yang sangat berbeda! :-) Dalam C ++ idiom untuk-loop adalah for (type i = 0; i != 42; ++i). Tidak hanya bisa operator++kelebihan beban, tetapi juga bisa operator!=dan operator<. Peningkatan awalan tidak lebih mahal dari postfix, tidak sama tidak lebih mahal daripada lebih rendah. Yang mana yang harus kita gunakan?
Bo Persson
7
Bukankah seharusnya itu disebut ++ C?
Armand
21
@Stephen: C ++ berarti ambil C, tambahkan, lalu gunakan yang lama .
supercat

Jawaban:

58

Fakta:

  1. i ++ dan ++ i sama-sama mudah dibaca. Anda tidak suka satu karena Anda tidak terbiasa, tetapi pada dasarnya tidak ada yang bisa Anda salah artikan, jadi tidak ada lagi pekerjaan untuk membaca atau menulis.

  2. Setidaknya dalam beberapa kasus, operator postfix akan kurang efisien.

  3. Namun, dalam kasus 99,99%, itu tidak masalah karena (a) itu akan bertindak pada tipe sederhana atau primitif dan itu hanya masalah jika menyalin objek besar (b) itu tidak akan berada dalam kinerja bagian penting dari kode (c) Anda tidak tahu apakah kompiler akan mengoptimalkannya atau tidak, itu bisa dilakukan.

  4. Jadi, saya sarankan menggunakan awalan kecuali Anda secara khusus membutuhkan postfix adalah kebiasaan yang baik untuk masuk, hanya karena (a) itu kebiasaan yang baik untuk menjadi tepat dengan hal-hal lain dan (b) sekali dalam bulan biru Anda akan berniat menggunakan postfix dan mendapatkannya dengan cara yang salah: jika Anda selalu menulis apa yang Anda maksudkan, itu kemungkinan kecil. Selalu ada trade-off antara kinerja dan optimisasi.

Anda harus menggunakan akal sehat Anda dan tidak mengoptimalkan mikro sampai Anda perlu, tetapi tidak akan menjadi tidak efisien untuk itu. Biasanya ini berarti: pertama, singkirkan segala konstruksi kode yang tidak efisien yang tidak dapat diterima bahkan dalam kode non-waktu-kritis (biasanya sesuatu yang mewakili kesalahan konseptual yang mendasar, seperti melewatkan 500MB objek dengan nilai tanpa alasan); dan kedua, dari setiap cara penulisan kode lainnya, pilih yang paling jelas.

Namun, di sini, saya percaya jawabannya sederhana: Saya percaya menulis awalan kecuali jika Anda secara khusus membutuhkan postfix adalah (a) sangat sedikit lebih jelas dan (b) sangat sedikit lebih mungkin lebih efisien, jadi Anda harus selalu menuliskannya secara default, tetapi jangan khawatir tentang hal itu jika Anda lupa.

Enam bulan yang lalu, saya berpikir sama dengan Anda, bahwa i ++ lebih alami, tapi ini murni yang biasa Anda lakukan.

EDIT 1: Scott Meyers, dalam "C ++ Lebih Efektif" yang secara umum saya percayai tentang hal ini, mengatakan Anda harus secara umum menghindari penggunaan operator postfix pada tipe yang ditentukan pengguna (karena satu-satunya implementasi yang waras dari fungsi kenaikan postfix adalah membuat salinan objek, panggil fungsi awalan increment untuk melakukan kenaikan, dan kembalikan salinannya, tetapi operasi penyalinan bisa mahal).

Jadi, kita tidak tahu apakah ada aturan umum tentang (a) apakah itu benar hari ini, (b) apakah itu juga berlaku (kurang begitu) untuk tipe intrinsik (c) apakah Anda harus menggunakan "++" pada apa pun lebih dari kelas iterator yang ringan. Tetapi untuk semua alasan yang saya jelaskan di atas, tidak masalah, lakukan apa yang saya katakan sebelumnya.

EDIT 2: Ini merujuk pada praktik umum. Jika Anda berpikir itu TIDAK penting dalam beberapa contoh tertentu, maka Anda harus membuat profil dan melihatnya. Pembuatan profil mudah dan murah dan berfungsi. Mendedikasikan dari prinsip pertama apa yang perlu dioptimalkan adalah sulit dan mahal dan tidak bekerja.

Jack V.
sumber
Posting Anda benar berdasarkan uang. Dalam ekspresi di mana operator infiks + dan pasca-kenaikan ++ telah kelebihan beban seperti aClassInst = someOtherClassInst + yetAnotherClassInst ++, parser akan menghasilkan kode untuk melakukan operasi aditif sebelum membuat kode untuk melakukan operasi pasca kenaikan, mengurangi kebutuhan untuk buat salinan sementara. Pembunuh kinerja di sini bukanlah peningkatan setelah kenaikan. Ini adalah penggunaan operator infiks yang kelebihan beban. Operator infix menghasilkan instance baru.
bit-twiddler
2
Saya sangat curiga bahwa alasan orang 'terbiasa' i++daripada ++iadalah karena nama bahasa pemrograman populer tertentu yang dirujuk dalam pertanyaan / jawaban ini ...
Shadow
61

Selalu kode untuk programmer pertama dan komputer kedua.

Jika ada perbedaan kinerja, setelah kompiler mengarahkan pandangan ahli atas kode Anda, DAN Anda bisa mengukurnya DAN itu penting - maka Anda bisa mengubahnya.

Martin Beckett
sumber
7
Pernyataan hebat !!!
Dave
8
@ Martin: itulah sebabnya saya menggunakan penambahan awalan. Semantik postfix menyiratkan menjaga nilai lama, dan jika tidak perlu, maka itu tidak akurat untuk menggunakannya.
Matthieu M.
1
Untuk indeks loop yang akan lebih jelas - tetapi jika Anda mengulangi array dengan menambah pointer dan menggunakan awalan berarti mulai dari alamat ilegal satu sebelum mulai yang akan buruk terlepas dari peningkatan kinerja
Martin Beckett
5
@ Matthew: Tidak benar bahwa peningkatan pasca menyiratkan menjaga salinan nilai lama di sekitar. Seseorang tidak dapat memastikan bagaimana kompiler menangani nilai-nilai perantara sampai ia melihat outputnya. Jika Anda meluangkan waktu untuk melihat daftar bahasa assembly yang dihasilkan oleh GCC beranotasi saya, Anda akan melihat bahwa GCC menghasilkan kode mesin yang sama untuk kedua loop. Omong kosong tentang lebih menyukai pra-kenaikan daripada pasca-kenaikan karena lebih efisien sedikit lebih dari dugaan.
bit-twiddler
2
@Mathhieu: Kode yang saya posting dihasilkan dengan optimasi dimatikan. Spesifikasi C ++ tidak menyatakan bahwa kompiler harus menghasilkan instance sementara dari nilai ketika post-incrementation digunakan. Ini hanya menyatakan diutamakan operator pra dan pasca kenaikan.
bit-twiddler
13

GCC menghasilkan kode mesin yang sama untuk kedua loop.

Kode C

int main(int argc, char** argv)
{
    for (int i = 0; i < 42; i++)
            printf("i = %d\n",i);

    for (int i = 0; i < 42; ++i)
        printf("i = %d\n",i);

    return 0;
}

Assembly Code (dengan komentar saya)

    cstring
LC0:
    .ascii "i = %d\12\0"
    .text
.globl _main
_main:
    pushl   %ebp
    movl    %esp, %ebp
    pushl   %ebx
    subl    $36, %esp
    call    L9
"L00000000001$pb":
L9:
    popl    %ebx
    movl    $0, -16(%ebp)  // -16(%ebp) is "i" for the first loop 
    jmp L2
L3:
    movl    -16(%ebp), %eax   // move i for the first loop to the eax register 
    movl    %eax, 4(%esp)     // push i onto the stack
    leal    LC0-"L00000000001$pb"(%ebx), %eax // load the effective address of the format string into the eax register
    movl    %eax, (%esp)      // push the address of the format string onto the stack
    call    L_printf$stub    // call printf
    leal    -16(%ebp), %eax  // make the eax register point to i
    incl    (%eax)           // increment i
L2:
    cmpl    $41, -16(%ebp)  // compare i to the number 41
    jle L3              // jump to L3 if less than or equal to 41
    movl    $0, -12(%ebp)   // -12(%ebp) is "i" for the second loop  
    jmp L5
L6:
    movl    -12(%ebp), %eax   // move i for the second loop to the eax register 
    movl    %eax, 4(%esp)     // push i onto the stack
    leal    LC0-"L00000000001$pb"(%ebx), %eax // load the effective address of the format string into the eax register
    movl    %eax, (%esp)      // push the address of the format string onto the stack
    call    L_printf$stub     // call printf
    leal    -12(%ebp), %eax  // make eax point to i
    incl    (%eax)           // increment i
L5:
    cmpl    $41, -12(%ebp)   // compare i to 41 
    jle L6               // jump to L6 if less than or equal to 41
    movl    $0, %eax
    addl    $36, %esp
    popl    %ebx
    leave
    ret
    .section __IMPORT,__jump_table,symbol_stubs,self_modifying_code+pure_instructions,5
L_printf$stub:
    .indirect_symbol _printf
    hlt ; hlt ; hlt ; hlt ; hlt
    .subsections_via_symbols
bit-twiddler
sumber
Bagaimana dengan optimasi yang dihidupkan?
serv-inc
2
@ pengguna: Mungkin tidak ada perubahan, tetapi apakah Anda benar-benar berharap bit-twiddler akan kembali dalam waktu dekat?
Deduplicator
2
Hati-hati: Sementara di C tidak ada tipe yang ditentukan pengguna dengan operator kelebihan beban, di C ++ ada, dan generalisasi dari tipe dasar ke tipe yang ditentukan pengguna tidak valid .
Deduplicator
@Dupuplikator: Terima kasih, juga telah menunjukkan bahwa jawaban ini tidak digeneralisasi ke tipe yang ditentukan pengguna. Saya belum melihat halaman penggunanya sebelum bertanya.
serv-inc
12

Jangan khawatir tentang kinerja, katakanlah 97% dari waktu. Optimalisasi Prematur adalah Akar Semua Kejahatan.

- Donald Knuth

Sekarang ini sudah keluar dari jalan kita, mari kita buat pilihan kita dengan waras :

  • ++i: kenaikan awalan , tambah nilai saat ini dan hasilkan hasilnya
  • i++: kenaikan postfix , salin nilainya, tambah nilainya saat ini, hasilkan salinannya

Kecuali jika diperlukan salinan nilai lama, gunakan penambahan postfix adalah cara bulat untuk menyelesaikan sesuatu.

Ketidaktepatan berasal dari kemalasan, selalu gunakan konstruk yang mengekspresikan niat Anda dengan cara yang paling langsung, ada sedikit peluang yang bisa disalahpahami oleh niat Anda di masa depan.

Meskipun ini (benar-benar) kecil di sini, ada kalanya saya benar-benar bingung dengan membaca kode: Saya benar-benar bertanya-tanya apakah maksud dan ekspres yang sebenarnya sesuai, dan tentu saja, setelah beberapa bulan, mereka (atau saya) juga tidak ingat ...

Jadi, tidak masalah apakah itu cocok untuk Anda, atau tidak. Rangkullah KISS . Dalam beberapa bulan Anda akan menghindari praktik lama Anda.

Matthieu M.
sumber
4

Di C ++, Anda bisa membuat perbedaan kinerja yang substansial jika ada kelebihan operator yang terlibat, terutama jika Anda sedang menulis kode templated dan tidak tahu iterator apa yang mungkin diteruskan. Logika di balik iterator X mana pun mungkin substansial dan signifikan- yaitu lambat, dan tidak dioptimalkan oleh kompiler.

Tapi ini bukan kasus di C, di mana Anda tahu itu hanya akan menjadi tipe sepele, dan perbedaan kinerja sepele dan kompiler dapat dengan mudah mengoptimalkannya.

Jadi tip: Anda memprogram dalam C, atau dalam C ++, dan pertanyaan berhubungan dengan satu atau yang lain, tidak keduanya.

DeadMG
sumber
2

Kinerja dari kedua operasi ini sangat tergantung pada arsitektur yang mendasarinya. Kita harus meningkatkan nilai yang disimpan dalam memori, yang berarti bahwa bottleneck von Neumann adalah faktor pembatas dalam kedua kasus.

Dalam kasus ++ i, kita harus

Fetch i from memory 
Increment i
Store i back to memory
Use i

Dalam kasus i ++, kita harus

Fetch i from memory
Use i
Increment i
Store i back to memory

Operator ++ dan - melacak asal mereka ke set instruksi PDP-11. PDP-11 dapat melakukan peningkatan pasca-otomatis pada register. Itu juga bisa melakukan pra-pengurangan otomatis pada alamat efektif yang terkandung dalam register. Dalam kedua kasus tersebut, kompiler hanya dapat mengambil keuntungan dari operasi tingkat mesin ini jika variabel yang dimaksud adalah variabel "register".

bit-twiddler
sumber
2

Jika Anda ingin tahu apakah ada sesuatu yang lambat, ujilah. Ambil BigInteger atau yang setara, tempelkan di loop yang sama untuk menggunakan kedua idiom, pastikan bagian dalam loop tidak dioptimalkan, dan waktu keduanya.

Setelah membaca artikel itu, saya merasa tidak terlalu meyakinkan, karena tiga alasan. Pertama, kompiler harus dapat mengoptimalkan pembuatan objek yang tidak pernah digunakan. Dua, i++konsepnya idiomatis untuk numerik untuk loop , sehingga kasus yang saya lihat benar-benar terpengaruh terbatas. Tiga, mereka memberikan argumen teoretis murni, tanpa nomor untuk mendukungnya.

Berdasarkan alasan # 1 khususnya, tebakan saya adalah bahwa ketika Anda benar-benar melakukan pengaturan waktu, mereka akan berada tepat di sebelah satu sama lain.

jprete
sumber
-1

Pertama-tama itu tidak mempengaruhi IMO keterbacaan. Ini bukan apa yang biasa Anda lihat, tetapi itu hanya sebentar sebelum Anda terbiasa.

Kedua, kecuali jika Anda menggunakan banyak operator postfix dalam kode Anda, Anda tidak akan melihat banyak perbedaan. Argumen utama untuk tidak menggunakannya bila mungkin adalah bahwa salinan dari nilai var asli harus disimpan sampai akhir argumen di mana var asli masih bisa digunakan. Itu 32bit atau 64bit tergantung pada arsitektur. Itu sama dengan 4 atau 8 byte atau 0,00390625 atau 0,0078125 MB. Peluangnya sangat tinggi, kecuali jika Anda menggunakan banyak dari mereka yang perlu disimpan untuk jangka waktu yang sangat lama dengan sumber daya dan kecepatan komputer saat ini Anda bahkan tidak akan melihat perbedaan saat beralih dari postfix ke prefix.

EDIT: Lupakan bagian yang tersisa ini karena kesimpulan saya terbukti salah (kecuali untuk bagian ++ i dan i ++ tidak selalu melakukan hal yang sama ... itu masih benar).

Juga telah ditunjukkan sebelumnya bahwa mereka tidak melakukan hal yang sama dalam sebuah kasus. Berhati-hatilah untuk beralih jika Anda memutuskan untuk melakukannya. Saya tidak pernah mencobanya (saya selalu menggunakan postfix) jadi saya tidak tahu pasti tapi saya pikir mengubah dari postfix ke prefix akan menghasilkan hasil yang berbeda: (sekali lagi saya bisa salah ... tergantung pada kompiler / penerjemah juga)

for (int i=0; i < 10; i++) //the set of i values here will be {0,1,2,3,4,5,6,7,8,9}
for (int i=0; i < 10; ++i) //the set of i values here will be {1,2,3,4,5,6,7,8,9,10}
Kenneth
sumber
4
Operasi kenaikan terjadi pada akhir for loop, sehingga mereka akan memiliki output yang sama persis. Itu tidak tergantung pada kompiler / juru bahasa.
jsternberg
@ jsternberg ... Terima kasih, saya tidak yakin kapan kenaikan itu terjadi karena saya tidak pernah benar-benar punya alasan untuk mengujinya. Sudah terlalu lama sejak saya membuat kompiler di perguruan tinggi! lol
Kenneth
Salah salah salah.
ruohola
-1

Saya pikir secara semantik, ++ilebih masuk akal daripada i++, jadi saya akan tetap pada yang pertama, kecuali itu umum untuk tidak melakukannya (seperti di Jawa, di mana Anda harus menggunakan i++karena itu banyak digunakan).

Oliver Weiler
sumber
-2

Ini bukan hanya tentang kinerja.

Terkadang Anda ingin menghindari penerapan penyalinan sama sekali, karena itu tidak masuk akal. Dan karena penggunaan penambahan awalan tidak bergantung pada ini, lebih mudah untuk tetap menggunakan formulir awalan.

Dan menggunakan peningkatan berbeda untuk tipe primitif dan tipe kompleks ... itu benar-benar tidak dapat dibaca.

maxim1000
sumber
-2

Kecuali Anda benar-benar membutuhkannya, saya akan tetap menggunakan ++ i. Dalam kebanyakan kasus, inilah yang ingin dicapai. Ini tidak terlalu sering Anda butuhkan i ++, dan Anda selalu harus berpikir dua kali ketika membaca konstruksi seperti itu. Dengan ++ i, mudah: Anda menambahkan 1, menggunakannya, dan kemudian saya masih sama.

Jadi, saya setuju sepenuh hati dengan @martin beckett: buat sendiri lebih mudah, itu sudah cukup sulit.

Peter Frings
sumber