Apakah praktik yang buruk untuk menulis kode yang mengandalkan optimisasi kompiler?


99

Saya telah belajar beberapa C ++, dan sering harus mengembalikan objek besar dari fungsi yang dibuat di dalam fungsi. Saya tahu ada pass by reference, mengembalikan pointer, dan mengembalikan solusi tipe referensi, tetapi saya juga membaca bahwa kompiler C ++ (dan standar C ++) memungkinkan untuk optimasi nilai balik, yang menghindari menyalin objek-objek besar ini melalui memori, dengan demikian menghemat waktu dan memori semua itu.

Sekarang, saya merasa bahwa sintaks lebih jelas ketika objek secara eksplisit dikembalikan oleh nilai, dan kompiler umumnya akan menggunakan RVO dan membuat proses lebih efisien. Apakah praktik yang buruk mengandalkan optimasi ini? Itu membuat kode lebih jelas dan lebih mudah dibaca oleh pengguna, yang sangat penting, tetapi haruskah saya berhati-hati dengan menganggap kompiler akan menangkap peluang RVO?

Apakah ini optimasi mikro, atau sesuatu yang harus saya ingat ketika mendesain kode saya?


7
Untuk menjawab hasil edit Anda, ini adalah optimasi mikro karena walaupun Anda mencoba membuat tolok ukur apa yang Anda hasilkan dalam nanosecond, Anda akan sulit melihatnya. Untuk selebihnya, saya terlalu busuk di C ++ untuk memberi Anda jawaban ketat mengapa itu tidak berhasil. Salah satunya jika ada kasus ketika Anda membutuhkan alokasi dinamis dan karenanya menggunakan / pointer / baru.
Walfrat

4
@ Walfrat bahkan jika objeknya cukup besar, di urutan megabyte? Array saya dapat menjadi sangat besar karena sifat dari masalah yang saya pecahkan.
Matt

6
@ Mat saya tidak akan. Referensi / petunjuk ada untuk hal ini. Optimalisasi kompiler seharusnya melampaui apa yang harus dipertimbangkan oleh programmer ketika membangun sebuah program, meskipun ya, sering kali kedua dunia tumpang tindih.
Neil

5
@ Matt Kecuali jika Anda melakukan sesuatu yang sangat spesifik yang mengandaikan untuk membutuhkan pengembang dengan 10 + pengalaman ish di kernel C /, kernel interaksi perangkat keras rendah Anda tidak perlu itu. Jika Anda merasa memiliki sesuatu yang sangat spesifik, edit posting Anda dan tambahkan deskripsi yang akurat tentang apa yang seharusnya dilakukan aplikasi Anda (real-time? Perhitungan matematika berat? ...)
Walfrat

37
Dalam kasus khusus C ++ (N) RVO, ya, mengandalkan optimasi ini sangat valid. Ini karena standar C ++ 17 secara khusus mengamanatkan hal itu terjadi, dalam situasi yang telah dilakukan oleh kompiler modern.
Caleth

Jawaban:


130

Gunakan prinsip yang paling mengejutkan .

Apakah Anda dan hanya Anda yang akan menggunakan kode ini, dan apakah Anda yakin bahwa Anda yang sama dalam 3 tahun tidak akan terkejut dengan apa yang Anda lakukan?

Lalu, lanjutkan.

Dalam semua kasus lain, gunakan cara standar; jika tidak, Anda dan kolega Anda akan menemui kesulitan untuk menemukan bug.

Misalnya, kolega saya mengeluh tentang kode saya yang menyebabkan kesalahan. Ternyata, ia mematikan evaluasi Boolean hubung singkat dalam pengaturan kompilernya. Saya hampir menamparnya.


88
@Neil itu maksud saya, semua orang bergantung pada evaluasi hubung singkat. Dan Anda tidak perlu berpikir dua kali tentang itu, itu harus dihidupkan. Ini standar de facto. Ya, Anda dapat mengubahnya, tetapi Anda tidak harus mengubahnya.
Pieter B

49
"Aku mengubah cara kerjanya, dan kode busukmu rusak! Arghh!" Wow. Menampar akan sesuai, mengirim kolega Anda ke pelatihan Zen, ada banyak di sana.

109
@PieterB Saya cukup yakin spesifikasi bahasa C dan C ++ menjamin evaluasi hubung singkat. Jadi bukan hanya standar de facto, itu yang standar. Tanpanya, Anda bahkan tidak menggunakan C / C ++ lagi, tetapi sesuatu yang mencurigakan seperti itu: P
marcelm

47
Hanya untuk referensi, cara standar di sini adalah mengembalikan berdasarkan nilai.
DeadMG

28
@ dan04 ya itu di Delphi. Guys, jangan terjebak dalam contoh ini tentang poin yang saya buat. Jangan melakukan hal-hal mengejutkan yang tidak dilakukan orang lain.
Pieter B

81

Untuk kasus khusus ini, pasti hanya mengembalikan dengan nilai.

  • RVO dan NRVO adalah optimasi terkenal dan kuat yang benar-benar harus dibuat oleh kompiler yang layak, bahkan dalam mode C ++ 03.

  • Pindahkan semantik memastikan bahwa objek dipindahkan dari fungsi jika (N) RVO tidak terjadi. Itu hanya berguna jika objek Anda menggunakan data dinamis secara internal (seperti std::vectorhalnya), tetapi itu harus benar-benar menjadi kasus jika itu yang besar - meluap tumpukan adalah risiko dengan objek otomatis besar.

  • C ++ 17 memberlakukan RVO. Jadi jangan khawatir, itu tidak akan hilang pada Anda dan hanya akan selesai memantapkan dirinya sepenuhnya setelah kompiler up-to-date.

Dan pada akhirnya, memaksakan alokasi dinamis tambahan untuk mengembalikan pointer, atau memaksa tipe hasil Anda menjadi default-konstruktif hanya agar Anda bisa meneruskannya sebagai parameter output adalah solusi yang jelek dan non-idiomatik untuk masalah yang Anda mungkin tidak akan pernah memiliki.

Cukup tulis kode yang masuk akal dan ucapkan terima kasih kepada pembuat kompiler untuk mengoptimalkan kode yang masuk akal.


9
Hanya untuk bersenang-senang, lihat bagaimana Borland Turbo C ++ 3.0 dari 1990-ish menangani RVO . Spoiler: Ini pada dasarnya bekerja dengan baik.
nwp

9
Kuncinya di sini adalah ini bukan optimasi acak khusus kompiler atau "fitur tidak terdokumentasi," tetapi sesuatu yang, sementara secara teknis opsional dalam beberapa versi standar C ++, sangat didorong oleh industri dan cukup banyak setiap kompiler utama telah melakukannya untuk Waktu yang sangat lama.

7
Optimalisasi ini tidak sekuat yang mungkin diinginkan. Ya, itu agak dapat diandalkan dalam kasus-kasus yang paling jelas, tetapi mencari misalnya di bugzilla gcc, ada banyak kasus yang hampir tidak kurang jelas di mana ia dilewatkan.
Marc Glisse

62

Sekarang, saya merasa bahwa sintaks lebih jelas ketika objek secara eksplisit dikembalikan oleh nilai, dan kompiler umumnya akan menggunakan RVO dan membuat proses lebih efisien. Apakah praktik yang buruk mengandalkan optimasi ini? Itu membuat kode lebih jelas dan lebih mudah dibaca oleh pengguna, yang sangat penting, tetapi haruskah saya berhati-hati dengan menganggap kompiler akan menangkap peluang RVO?

Ini bukan sedikit yang diketahui, imut, optimisasi mikro yang Anda baca di beberapa blog kecil yang diperdagangkan, lalu Anda merasa pintar dan unggul dalam menggunakannya.

Setelah C ++ 11, RVO adalah cara standar untuk menulis kode kode ini. Biasa, diharapkan, diajarkan, disebutkan dalam pembicaraan, disebutkan dalam blog, disebutkan dalam standar, akan dilaporkan sebagai bug penyusun jika tidak diterapkan. Dalam C ++ 17, bahasa ini selangkah lebih maju dan mengamanatkan penyalinan salinan dalam skenario tertentu.

Anda harus benar-benar mengandalkan pengoptimalan ini.

Lebih dari itu, return-by-value hanya mengarah ke kode yang lebih mudah dibaca dan dikelola daripada kode yang dikembalikan dengan referensi. Nilai semantik adalah hal yang kuat, yang dengan sendirinya dapat mengarah pada peluang optimisasi yang lebih banyak.


3
Terima kasih, ini sangat masuk akal dan konsisten dengan "prinsip paling tidak mengejutkan" yang disebutkan di atas. Itu akan membuat kode sangat jelas dan dapat dimengerti, dan membuatnya lebih sulit untuk mengacaukan pointer shenanigans.
Matt

3
@Matt Bagian dari alasan saya memutakhirkan jawaban ini adalah karena ia menyebutkan "nilai semantik". Ketika Anda mendapatkan lebih banyak pengalaman dalam C ++ (dan pemrograman secara umum), Anda akan menemukan situasi sesekali di mana nilai semantik tidak dapat digunakan untuk objek tertentu karena mereka bisa berubah dan perubahannya harus dibuat terlihat oleh kode lain yang menggunakan objek yang sama (suatu contoh "mutabilitas bersama"). Ketika situasi ini terjadi, objek yang terpengaruh harus dibagi melalui pointer (pintar).
rwong

16

Ketepatan kode yang Anda tulis tidak boleh bergantung pada pengoptimalan. Seharusnya menampilkan hasil yang benar ketika dieksekusi pada "mesin virtual" C ++ yang mereka gunakan dalam spesifikasi.

Namun, apa yang Anda bicarakan lebih merupakan pertanyaan efisiensi. Kode Anda berjalan lebih baik jika dioptimalkan dengan kompiler yang mengoptimalkan RVO. Tidak apa-apa, untuk semua alasan yang ditunjukkan dalam jawaban lain.

Namun, jika Anda memerlukan pengoptimalan ini (seperti jika konstruktor salinan akan benar-benar menyebabkan kode Anda gagal), sekarang Anda berada di kehendak kompilator.

Saya pikir contoh terbaik dari ini dalam praktik saya sendiri adalah optimasi panggilan ekor:

   int sillyAdd(int a, int b)
   {
      if (b == 0)
          return a;
      return sillyAdd(a + 1, b - 1);
   }

Ini adalah contoh konyol, tetapi menunjukkan panggilan ekor, di mana suatu fungsi disebut tepat secara rekursif di akhir fungsi. Mesin virtual C ++ akan menunjukkan bahwa kode ini beroperasi dengan benar, meskipun saya dapat menyebabkan sedikit kebingungan mengapa saya repot menulis rutin tambahan seperti itu di tempat pertama. Namun, dalam implementasi praktis C ++, kami memiliki setumpuk, dan memiliki ruang terbatas. Jika dilakukan secara pedantik, fungsi ini harus mendorong setidaknya b + 1tumpukan bingkai ke tumpukan seperti halnya penambahannya. Jika saya ingin menghitung sillyAdd(5, 7), ini bukan masalah besar. Jika saya ingin menghitung sillyAdd(0, 1000000000), saya bisa berada dalam kesulitan besar menyebabkan StackOverflow (dan bukan jenis yang baik ).

Namun, kita dapat melihat bahwa begitu kita mencapai garis balik terakhir, kita benar-benar selesai dengan semua yang ada di bingkai tumpukan saat ini. Kami benar-benar tidak perlu menyimpannya. Optimasi panggilan ekor memungkinkan Anda "menggunakan kembali" bingkai tumpukan yang ada untuk fungsi selanjutnya. Dengan cara ini, kita hanya perlu 1 frame stack, daripada b+1. (Kita masih harus melakukan semua penambahan dan pengurangan yang konyol itu, tetapi mereka tidak mengambil lebih banyak ruang.) Akibatnya, optimasi mengubah kode menjadi:

   int sillyAdd(int a, int b)
   {
      begin:
      if (b == 0)
          return a;
      // return sillyAdd(a + 1, b - 1);
      a = a + 1;
      b = b - 1;
      goto begin;  
   }

Dalam beberapa bahasa, optimisasi panggilan ekor secara eksplisit diperlukan oleh spesifikasi. C ++ bukan salah satunya. Saya tidak bisa mengandalkan kompiler C ++ untuk mengenali peluang optimisasi panggilan ekor ini, kecuali saya menggunakan kasus per kasus. Dengan versi Visual Studio saya, versi rilis melakukan optimasi panggilan ekor, tetapi versi debug tidak (menurut desain).

Dengan demikian akan buruk bagi saya untuk bergantung pada kemampuan untuk menghitung sillyAdd(0, 1000000000).


2
Ini adalah kasus sudut yang menarik, tetapi saya tidak berpikir Anda bisa menggeneralisasikannya dengan aturan di paragraf pertama Anda. Misalkan saya memiliki program untuk perangkat kecil, yang akan memuat jika dan hanya jika saya menggunakan optimisasi pengurangan ukuran kompiler - apakah salah melakukannya? tampaknya agak berlebihan untuk mengatakan bahwa satu-satunya pilihan saya yang valid adalah menulis ulang di assembler, terutama jika penulisan ulang itu melakukan hal yang sama seperti pengoptimal untuk menyelesaikan masalah.
sdenham

5
@sdenham Saya kira ada sedikit ruang dalam argumen. Jika Anda tidak lagi menulis untuk "C ++," melainkan menulis untuk "kompiler WindRiver C ++ versi 3.4.1," maka saya dapat melihat logikanya di sana. Namun, sebagai aturan umum, jika Anda menulis sesuatu yang tidak berfungsi dengan benar sesuai dengan spesifikasi, Anda berada dalam skenario yang sangat berbeda. Saya tahu perpustakaan Boost memiliki kode seperti itu, tetapi mereka selalu meletakkannya di #ifdefblok, dan memiliki solusi standar yang tersedia.
Cort Ammon

4
apakah itu salah ketik di blok kode kedua di mana dikatakan b = b + 1?
stib

2
Anda mungkin ingin menjelaskan apa yang Anda maksud dengan "mesin virtual C ++", karena itu bukan istilah yang digunakan dalam dokumen standar apa pun. Saya pikir Anda sedang berbicara tentang model eksekusi C ++, tetapi tidak sepenuhnya pasti - dan istilah Anda tampak mirip dengan "mesin virtual bytecode" yang berkaitan dengan sesuatu yang sama sekali berbeda.
Toby Speight

1
@supercat Scala juga memiliki sintaks rekursi ekor eksplisit. C ++ adalah binatang buasnya sendiri, tetapi saya pikir rekursi ekor tidak otomatis untuk bahasa non-fungsional, dan wajib untuk bahasa fungsional, meninggalkan satu set kecil bahasa di mana masuk akal untuk memiliki sintaks rekursi ekor eksplisit. Secara harfiah menerjemahkan rekursi ekor menjadi loop dan mutasi eksplisit adalah pilihan yang lebih baik untuk banyak bahasa.
prosfilaes

8

Dalam praktiknya, program C ++ mengharapkan beberapa optimasi kompiler.

Lihatlah terutama ke header standar implementasi kontainer standar Anda . Dengan GCC , Anda dapat meminta formulir preproses ( g++ -C -E) dan representasi internal GIMPLE ( g++ -fdump-tree-gimpleatau Gimple SSA dengan -fdump-tree-ssa) sebagian besar file sumber (unit terjemahan teknis) menggunakan wadah. Anda akan terkejut dengan jumlah optimasi yang dilakukan (dengan g++ -O2). Jadi implementor kontainer bergantung pada optimisasi (dan sebagian besar waktu, implementator perpustakaan standar C ++ tahu apa yang akan terjadi optimasi dan menulis implementasi kontainer dengan yang ada dalam pikiran; kadang-kadang ia juga akan menulis pass optimisasi dalam kompiler untuk berurusan dengan fitur-fitur yang diperlukan oleh perpustakaan C ++ standar saat itu).

Dalam praktiknya, optimisasi kompiler yang membuat C ++ dan kontainer standarnya cukup efisien. Jadi Anda bisa mengandalkan mereka.

Dan juga untuk kasus RVO yang disebutkan dalam pertanyaan Anda.

Standar C ++ dirancang bersama (terutama dengan bereksperimen dengan optimasi yang cukup baik sambil mengusulkan fitur-fitur baru) untuk bekerja dengan baik dengan optimasi yang mungkin.

Misalnya, pertimbangkan program di bawah ini:

#include <algorithm>
#include <vector>

extern "C" bool all_positive(const std::vector<int>& v) {
  return std::all_of(v.begin(), v.end(), [](int x){return x >0;});
}

kompilasi dengan g++ -O3 -fverbose-asm -S. Anda akan mengetahui bahwa fungsi yang dihasilkan tidak menjalankan CALLinstruksi mesin apa pun . Jadi sebagian besar langkah-langkah C ++ (konstruksi penutupan lambda, aplikasi berulang, mendapatkan begindan enditerator, dll ...) telah dioptimalkan. Kode mesin hanya berisi satu loop (yang tidak muncul secara eksplisit dalam kode sumber). Tanpa optimasi seperti itu, C ++ 11 tidak akan berhasil.

tambahan

(tambah Desember 31 st 2017)

Lihat CppCon 2017: Matt Godbolt “Apa yang Telah Dilakukan Kompiler Saya Akhir-akhir Ini? Membuka kunci Tutup Pengumpul ” bicara.


4

Setiap kali Anda menggunakan kompiler, pengertiannya adalah ia akan menghasilkan kode mesin atau byte untuk Anda. Itu tidak menjamin apa pun tentang seperti apa kode yang dihasilkan, kecuali bahwa itu akan mengimplementasikan kode sumber sesuai dengan spesifikasi bahasa. Perhatikan bahwa jaminan ini sama terlepas dari tingkat optimasi yang digunakan, dan, secara umum, tidak ada alasan untuk menganggap satu output lebih 'benar' dari yang lain.

Lebih jauh lagi, dalam kasus-kasus itu, seperti RVO, di mana ia ditentukan dalam bahasa, tampaknya tidak ada gunanya pergi keluar dari cara Anda untuk menghindari menggunakannya, terutama jika itu membuat kode sumber lebih sederhana.

Banyak upaya dilakukan untuk membuat kompiler menghasilkan output yang efisien, dan jelas tujuannya adalah agar kapabilitas tersebut digunakan.

Mungkin ada alasan untuk menggunakan kode yang tidak dioptimalkan (untuk debugging, misalnya), tetapi kasus yang disebutkan dalam pertanyaan ini tampaknya bukan salah satu (dan jika kode Anda gagal hanya ketika dioptimalkan, dan itu bukan konsekuensi dari beberapa kekhasan dari perangkat tempat Anda menjalankannya, maka ada bug di suatu tempat, dan tidak mungkin ada di kompiler.)


3

Saya pikir orang lain membahas sudut khusus tentang C ++ dan RVO dengan baik. Ini jawaban yang lebih umum:

Ketika datang ke kebenaran, Anda tidak harus bergantung pada optimisasi kompiler, atau perilaku spesifik kompiler secara umum. Untungnya, Anda sepertinya tidak melakukan ini.

Ketika datang ke kinerja, Anda harus bergantung pada perilaku spesifik kompiler secara umum, dan optimisasi kompiler pada khususnya. Compiler compliant standar bebas untuk mengkompilasi kode Anda dengan cara apa pun yang diinginkan, selama kode yang dikompilasi berperilaku sesuai dengan spesifikasi bahasa. Dan saya tidak mengetahui adanya spesifikasi untuk bahasa umum yang menentukan seberapa cepat setiap operasi harus.


1

Optimalisasi kompiler hanya akan mempengaruhi kinerja, bukan hasil. Mengandalkan optimisasi kompiler untuk memenuhi persyaratan yang tidak fungsional tidak hanya masuk akal, itu sering menjadi alasan mengapa satu kompiler dipilih dari yang lain.

Bendera yang menentukan bagaimana operasi tertentu dilakukan (misalnya kondisi indeks atau luapan), sering disamakan dengan optimisasi kompiler, tetapi seharusnya tidak. Mereka secara eksplisit mempengaruhi hasil perhitungan.

Jika optimisasi kompiler menyebabkan hasil yang berbeda, itu adalah bug - bug di kompiler. Mengandalkan bug di kompiler, apakah dalam jangka panjang kesalahan - apa yang terjadi ketika diperbaiki?

Menggunakan flag compiler yang mengubah cara penghitungan kerja harus didokumentasikan dengan baik, tetapi digunakan sesuai kebutuhan.


Sayangnya, banyak dokumentasi kompiler melakukan pekerjaan yang buruk untuk menentukan apa yang dijamin atau tidak dalam berbagai mode. Lebih lanjut, penulis kompiler "modern" tampaknya tidak menyadari kombinasi jaminan yang dilakukan dan tidak dibutuhkan oleh programmer. Jika suatu program akan bekerja dengan baik jika secara x*y>zacak menghasilkan 0 atau 1 jika terjadi luapan, asalkan ia tidak memiliki efek samping lain , yang mensyaratkan bahwa seorang programmer harus mencegah kelebihan pada semua biaya atau memaksa kompiler untuk mengevaluasi ekspresi dengan cara tertentu akan tidak perlu merusak optimasi vs mengatakan bahwa ...
supercat

... kompiler mungkin pada waktu luangnya berperilaku seolah-olah x*ymempromosikan operan ke beberapa jenis lagi yang sewenang-wenang (sehingga memungkinkan bentuk mengangkat dan mengurangi kekuatan yang akan mengubah perilaku beberapa kasus overflow) Banyak kompiler, bagaimanapun, mengharuskan programmer baik mencegah overflow di semua biaya atau memaksa kompiler untuk memotong semua nilai perantara jika terjadi overflow.
supercat

1

Tidak.

Itu yang saya lakukan sepanjang waktu. Jika saya perlu mengakses blok 16 bit sewenang-wenang dalam memori, saya melakukan ini

void *ptr = get_pointer();
uint16_t u16;
memcpy(&u16, ptr, sizeof(u16)); // ntohs omitted for simplicity

... dan andalkan kompiler melakukan apa pun untuk mengoptimalkan potongan kode itu. Kode ini berfungsi pada ARM, i386, AMD64, dan praktis pada setiap arsitektur di luar sana. Secara teori, kompiler yang tidak mengoptimalkan sebenarnya dapat memanggil memcpy, menghasilkan kinerja yang benar-benar buruk, tetapi itu tidak masalah bagi saya, karena saya menggunakan optimasi kompiler.

Pertimbangkan alternatifnya:

void *ptr = get_pointer();
uint16_t *u16ptr = ptr;
uint16_t u16;
u16 = *u16ptr;  // ntohs omitted for simplicity

Kode alternatif ini gagal berfungsi pada mesin yang membutuhkan penyelarasan yang tepat, jika get_pointer()mengembalikan pointer yang tidak selaras. Juga, mungkin ada masalah alias dalam alternatif.

Perbedaan antara -O2 dan -O0 saat menggunakan memcpytrik itu hebat: 3,2 Gbps kinerja IP checksum versus 67 Gbps kinerja IP checksum. Atas urutan perbedaan besarnya!

Terkadang Anda mungkin perlu membantu kompiler. Jadi, misalnya, alih-alih mengandalkan kompiler untuk membuka gulungan, Anda dapat melakukannya sendiri. Baik dengan mengimplementasikan perangkat Duff yang terkenal , atau dengan cara yang lebih bersih.

Kelemahan dari mengandalkan optimisasi kompiler adalah bahwa jika Anda menjalankan gdb untuk men-debug kode Anda, Anda mungkin menemukan bahwa banyak yang telah dioptimalkan. Jadi, Anda mungkin perlu mengkompilasi ulang dengan -O0, yang berarti kinerja akan sangat menyedot ketika debugging. Saya pikir ini adalah kelemahan yang layak diambil, mengingat manfaat dari mengoptimalkan kompiler.

Apa pun yang Anda lakukan, pastikan cara Anda sebenarnya bukan perilaku yang tidak terdefinisi. Tentu saja mengakses beberapa blok memori acak sebagai integer 16-bit adalah perilaku yang tidak terdefinisi karena masalah aliasing dan alignment.


0

Semua upaya pada kode efisien yang ditulis dalam apa pun kecuali assembly sangat bergantung pada optimisasi kompiler, dimulai dengan alokasi register efisien paling dasar seperti untuk menghindari tumpahan tumpukan berlebihan di semua tempat dan setidaknya cukup baik, jika tidak bagus, pemilihan instruksi. Kalau tidak kita akan kembali ke tahun 80-an di mana kita harus meletakkan registerpetunjuk di semua tempat dan menggunakan jumlah minimum variabel dalam suatu fungsi untuk membantu kompiler C kuno atau bahkan lebih awal ketika gotoitu adalah optimasi cabang yang berguna.

Jika kami merasa tidak dapat mengandalkan kemampuan pengoptimal kami untuk mengoptimalkan kode kami, kami semua masih akan mengkode jalur eksekusi kinerja-kritis dalam perakitan.

Ini benar-benar masalah seberapa andal Anda merasa optimisasi dapat dibuat yang paling baik disortir dengan profiling dan melihat kemampuan kompiler yang Anda miliki dan bahkan mungkin membongkar jika ada hotspot Anda tidak dapat mengetahui di mana kompiler tampaknya gagal membuat optimasi yang jelas.

RVO adalah sesuatu yang telah ada selama berabad-abad, dan, setidaknya tidak termasuk kasus yang sangat kompleks, adalah sesuatu yang kompiler telah diandalkan untuk usia. Jelas tidak ada gunanya mengatasi masalah yang tidak ada.

Keliru Mengandalkan Pengoptimal, Tidak Takut

Sebaliknya, saya katakan sesat di sisi terlalu mengandalkan optimisasi kompiler daripada terlalu sedikit, dan saran ini datang dari seorang pria yang bekerja di bidang yang sangat kritis terhadap kinerja di mana efisiensi, rawatan, dan kualitas yang dirasakan di antara pelanggan adalah semua satu kekaburan raksasa. Saya lebih suka membuat Anda terlalu percaya diri pada pengoptimal Anda dan menemukan beberapa kasus tepi yang tidak jelas di mana Anda mengandalkan terlalu banyak daripada hanya mengandalkan terlalu sedikit dan hanya mengkodekan ketakutan takhayul sepanjang waktu selama sisa hidup Anda. Setidaknya itu akan membuat Anda meraih profiler dan menyelidikinya dengan benar jika segala sesuatunya tidak berjalan secepat yang seharusnya dan mendapatkan pengetahuan yang berharga, bukan takhayul, di sepanjang jalan.

Anda melakukannya dengan baik untuk bersandar pada pengoptimal. Teruskan. Jangan menjadi seperti orang yang mulai secara eksplisit meminta untuk menyelaraskan setiap fungsi yang dipanggil dalam satu lingkaran bahkan sebelum membuat profil dari ketakutan yang salah kaprah akan kekurangan pengoptimal.

Pembuatan profil

Profiling sebenarnya adalah bundaran tetapi jawaban akhir untuk pertanyaan Anda. Para pemula yang tidak sabar ingin menulis kode yang efisien sering bergumul dengan bukan apa yang harus dioptimalkan, itu yang tidak untuk mengoptimalkan karena mereka mengembangkan semua jenis firasat salah tentang ketidakefisienan yang, meskipun secara manusiawi intuitif, secara komputasi salah. Mengembangkan pengalaman dengan profiler akan mulai benar-benar memberi Anda apresiasi yang tepat tidak hanya pada kemampuan pengoptimalan kompiler yang Anda percayai, tetapi juga kemampuan (serta keterbatasan) perangkat keras Anda. Ada yang lebih berharga dalam membuat profil dalam mempelajari apa yang tidak layak untuk dioptimalkan daripada mempelajari apa yang sebelumnya.


-1

Perangkat lunak dapat ditulis dalam C ++ pada platform yang sangat berbeda dan untuk banyak tujuan berbeda.

Ini sepenuhnya tergantung pada tujuan perangkat lunak. Haruskah mudah untuk mempertahankan, memperluas, menambal, refactor dll. atau hal lain yang lebih penting, seperti kinerja, biaya atau kompatibilitas dengan beberapa perangkat keras tertentu atau waktu yang diperlukan untuk berkembang.


-2

Saya pikir jawaban yang membosankan untuk ini adalah: 'itu tergantung'.

Apakah praktik yang buruk untuk menulis kode yang bergantung pada pengoptimalan kompiler yang kemungkinan akan dimatikan dan di mana kerentanan tidak didokumentasikan dan di mana kode tersebut tidak diuji unit sehingga jika rusak Anda akan mengetahuinya ? Mungkin.

Apakah praktik yang buruk untuk menulis kode yang mengandalkan optimisasi kompiler yang tidak mungkin dimatikan , yang didokumentasikan dan unit diuji ? Mungkin tidak.


-6

Kecuali ada lebih banyak yang tidak Anda katakan kepada kami, ini adalah praktik yang buruk, tetapi bukan karena alasan yang Anda sarankan.

Mungkin tidak seperti bahasa lain yang telah Anda gunakan sebelumnya, mengembalikan nilai suatu objek di C ++ menghasilkan salinan objek. Jika Anda kemudian memodifikasi objek, Anda memodifikasi objek yang berbeda . Artinya, jika saya memiliki Obj a; a.x=1;dan Obj b = a;, maka saya lakukan b.x += 2; b.f();, maka a.xmasih sama dengan 1, bukan 3.

Jadi tidak, menggunakan objek sebagai nilai alih-alih sebagai referensi atau pointer tidak memberikan fungsi yang sama dan Anda bisa berakhir dengan bug di perangkat lunak Anda.

Mungkin Anda tahu ini dan itu tidak berdampak negatif pada kasus penggunaan khusus Anda. Namun, berdasarkan kata-kata dalam pertanyaan Anda, tampaknya Anda mungkin tidak menyadari perbedaannya; kata-kata seperti "buat objek dalam fungsi."

"buat sebuah objek dalam fungsi" terdengar seperti di new Obj;mana "mengembalikan objek dengan nilai" terdengar sepertiObj a; return a;

Obj a;dan Obj* a = new Obj;hal-hal yang sangat, sangat berbeda; yang pertama dapat menyebabkan kerusakan memori jika tidak digunakan dan dipahami dengan benar, dan yang terakhir dapat menyebabkan kebocoran memori jika tidak digunakan dan dipahami dengan benar.


8
Pengembalian nilai optimasi (RVO) adalah semantik yang didefinisikan dengan baik di mana kompiler membangun objek kembali satu tingkat di atas bingkai tumpukan, khususnya menghindari salinan objek yang tidak perlu. Ini adalah perilaku yang terdefinisi dengan baik yang telah didukung jauh sebelum diamanatkan dalam C ++ 17. Bahkan 10-15 tahun yang lalu, semua kompiler utama mendukung fitur ini dan melakukannya secara konsisten.

@Snowman Saya tidak berbicara tentang fisik, manajemen memori tingkat rendah, dan saya tidak membahas memori mengasapi atau kecepatan. Seperti yang saya tunjukkan secara spesifik dalam jawaban saya, saya berbicara tentang data logis. Logikanya , memasok nilai suatu objek adalah membuat salinannya, terlepas dari bagaimana kompiler diimplementasikan atau perakitan apa yang digunakan di belakang layar. Hal-hal tingkat rendah di belakang layar adalah satu hal, dan struktur logis dan perilaku bahasa adalah hal lain; mereka terkait, tetapi mereka bukan hal yang sama - keduanya harus dipahami.
Aaron

6
jawaban Anda mengatakan "mengembalikan nilai suatu objek dalam C ++ menghasilkan salinan objek" yang sepenuhnya salah dalam konteks RVO - objek dibangun langsung di lokasi pemanggilan, dan tidak ada salinan yang pernah dibuat. Anda dapat menguji ini dengan menghapus copy constructor dan mengembalikan objek yang dibuat dalam returnpernyataan yang merupakan persyaratan untuk RVO. Selanjutnya, Anda kemudian berbicara tentang kata kunci newdan petunjuk, yang bukan tentang RVO. Saya yakin Anda tidak mengerti pertanyaannya, atau RVO, atau mungkin keduanya.

-7

Pieter B benar sekali dalam merekomendasikan paling tidak heran.

Untuk menjawab pertanyaan spesifik Anda, apa artinya ini (kemungkinan besar) dalam C ++ adalah Anda harus mengembalikan a std::unique_ptrke objek yang dikonstruksi.

Alasannya adalah bahwa ini lebih jelas untuk pengembang C ++ tentang apa yang terjadi.

Meskipun pendekatan Anda kemungkinan besar akan berhasil, Anda secara efektif memberi sinyal bahwa objek tersebut adalah tipe nilai yang kecil padahal sebenarnya tidak. Selain itu, Anda membuang segala kemungkinan untuk abstraksi antarmuka. Ini mungkin OK untuk keperluan Anda saat ini tetapi sering sangat berguna ketika berhadapan dengan matriks.

Saya menghargai bahwa jika Anda berasal dari bahasa lain, semua tanda dapat membingungkan pada awalnya. Tapi hati-hati untuk tidak menganggap bahwa, dengan tidak menggunakannya, Anda membuat kode Anda lebih jelas. Dalam praktiknya, yang terjadi justru sebaliknya.


Ketika di Roma, lakukan seperti yang dilakukan orang Romawi.

14
Ini bukan jawaban yang baik untuk tipe yang tidak melakukan alokasi dinamis sendiri. Bahwa OP merasakan hal yang wajar dalam kasus penggunaannya adalah kembali dengan nilai menunjukkan bahwa objeknya memiliki durasi penyimpanan otomatis di sisi pemanggil. Untuk objek sederhana, tidak terlalu besar, bahkan implementasi salin nilai pengembalian yang naif akan menjadi urutan yang lebih cepat dari alokasi dinamis. (Jika, di sisi lain, fungsi mengembalikan sebuah wadah, lalu mengembalikan Unique_pointer bahkan mungkin lebih menguntungkan dibandingkan dengan pengembalian kompiler yang naif dengan nilai.)
Peter A. Schneider

9
@ Mat Jika Anda tidak menyadari ini bukan praktik terbaik. Melakukan alokasi memori yang tidak perlu dan memaksa semantik pointer pada pengguna adalah buruk.
nwp

5
Pertama-tama, ketika menggunakan pointer pintar, orang harus kembali std::make_unique, bukan std::unique_ptrlangsung. Kedua, RVO bukan esoterik, optimisasi khusus vendor: dimasukkan ke dalam standar. Bahkan ketika itu bukan, itu didukung secara luas dan perilaku yang diharapkan. Tidak ada titik mengembalikan sebuah std::unique_ptrketika pointer tidak diperlukan di tempat pertama.

4
@Snowman: Tidak ada "padahal tidak". Meskipun baru-baru ini menjadi wajib , setiap standar C ++ pernah mengenali [N] RVO, dan membuat akomodasi untuk mengaktifkannya (misalnya, kompiler selalu diberikan izin eksplisit untuk menghilangkan penggunaan copy constructor pada nilai kembali, bahkan jika itu memiliki efek samping yang terlihat).
Jerry Coffin
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.