Bisakah penggunaan 'otomatis' C ++ 11 meningkatkan kinerja?

230

Saya bisa melihat mengapa autotipe C ++ 11 meningkatkan kebenaran dan perawatan. Saya pernah membaca bahwa itu juga dapat meningkatkan kinerja ( Almost Always Auto oleh Herb Sutter), tetapi saya kehilangan penjelasan yang bagus.

  • Bagaimana cara automeningkatkan kinerja?
  • Adakah yang bisa memberi contoh?
DaBrain
sumber
5
Lihat herbalutter.com/2013/06/13/... yang berbicara tentang menghindari konversi implisit yang tidak disengaja, misalnya dari gadget ke widget. Itu bukan masalah umum.
Jonathan Wakely
42
Apakah Anda menerima "membuatnya lebih cenderung untuk secara tidak sengaja pesimis" sebagai peningkatan kinerja?
5gon12eder
1
Kinerja kode hanya membersihkan di masa depan, mungkin
Croll
Kami membutuhkan jawaban singkat: Tidak, jika Anda baik-baik saja. Itu dapat mencegah kesalahan 'noobish'. C ++ memiliki kurva belajar yang membunuh mereka yang tidak berhasil.
Alec Teal

Jawaban:

309

autodapat membantu kinerja dengan menghindari konversi tersirat diam . Contoh yang saya temukan menarik adalah sebagai berikut.

std::map<Key, Val> m;
// ...

for (std::pair<Key, Val> const& item : m) {
    // do stuff
}

Lihat bugnya? Kita di sini, berpikir kita secara elegan mengambil setiap item di peta dengan referensi const dan menggunakan rentang-untuk ekspresi baru untuk memperjelas maksud kita, tetapi sebenarnya kita menyalin setiap elemen. Hal ini karena std::map<Key, Val>::value_typeini std::pair<const Key, Val>, tidak std::pair<Key, Val>. Jadi, ketika kita (secara implisit) memiliki:

std::pair<Key, Val> const& item = *iter;

Alih-alih mengambil referensi ke objek yang ada dan membiarkannya, kita harus melakukan konversi jenis. Anda diizinkan untuk mengambil referensi const ke objek (atau sementara) dari jenis yang berbeda selama ada konversi tersirat yang tersedia, misalnya:

int const& i = 2.0; // perfectly OK

Konversi tipe adalah konversi implisit yang diizinkan untuk alasan yang sama Anda dapat mengonversi a const Keymenjadi Key, tetapi kami harus membuat sementara dari tipe baru untuk memungkinkannya. Jadi, secara efektif loop kita tidak:

std::pair<Key, Val> __tmp = *iter;       // construct a temporary of the correct type
std::pair<Key, Val> const& item = __tmp; // then, take a reference to it

(Tentu saja, sebenarnya tidak ada __tmpobjek, itu hanya ada untuk ilustrasi, pada kenyataannya sementara yang tidak disebutkan namanya hanya terikat itemuntuk seumur hidup).

Hanya mengubah ke:

for (auto const& item : m) {
    // do stuff
}

baru saja menyelamatkan kami dari satu ton salinan - sekarang jenis yang direferensikan cocok dengan jenis penginisialisasi, jadi tidak ada sementara atau konversi diperlukan, kami hanya dapat melakukan referensi langsung.

Barry
sumber
19
@Barry Bisakah Anda menjelaskan mengapa kompiler akan dengan senang hati membuat salinan alih-alih mengeluh tentang mencoba memperlakukan std::pair<const Key, Val> const &sebagai std::pair<Key, Val> const &? Baru menggunakan C ++ 11, tidak yakin bagaimana range-for dan automemainkan ini.
Agop
@Barry Terima kasih atas penjelasannya. Itu adalah bagian yang saya lewatkan - untuk beberapa alasan, saya pikir Anda tidak dapat memiliki referensi konstan untuk sementara. Tapi tentu saja Anda bisa - itu tidak akan ada lagi di akhir cakupannya.
Agop
@barry saya mengerti, tetapi masalahnya adalah bahwa tidak ada jawaban yang mencakup semua alasan untuk menggunakan autopeningkatan kinerja itu. Jadi saya akan menuliskannya dengan kata-kata saya sendiri di bawah ini.
Yakk - Adam Nevraumont
38
Saya masih tidak berpikir ini adalah bukti bahwa " automeningkatkan kinerja". Itu hanya contoh yang " automembantu mencegah kesalahan programmer yang merusak kinerja". Saya sampaikan bahwa ada perbedaan yang halus namun penting antara keduanya. Tetap, +1.
Lightness Races in Orbit
70

Karena automenyimpulkan jenis ekspresi inisialisasi, tidak ada konversi jenis yang terlibat. Dikombinasikan dengan algoritma templated, ini berarti Anda bisa mendapatkan perhitungan yang lebih langsung daripada jika Anda membuat sendiri suatu tipe - terutama ketika Anda berurusan dengan ekspresi yang tipe yang tidak bisa Anda sebutkan!

Sebuah contoh khas berasal dari (ab) menggunakan std::function:

std::function<bool(T, T)> cmp1 = std::bind(f, _2, 10, _1);  // bad
auto cmp2 = std::bind(f, _2, 10, _1);                       // good
auto cmp3 = [](T a, T b){ return f(b, 10, a); };            // also good

std::stable_partition(begin(x), end(x), cmp?);

Dengan cmp2dan cmp3, seluruh algoritme dapat menyejajarkan panggilan perbandingan, sedangkan jika Anda membuat std::functionobjek, tidak hanya panggilan tidak dapat digarisbawahi, tetapi Anda juga harus melalui pencarian polimorfik di bagian interior yang dihapus dari pembungkus fungsi.

Varian lain pada tema ini adalah Anda dapat mengatakan:

auto && f = MakeAThing();

Ini selalu merupakan referensi, terikat pada nilai ekspresi pemanggilan fungsi, dan tidak pernah membuat objek tambahan. Jika Anda tidak tahu tipe nilai yang dikembalikan, Anda mungkin dipaksa untuk membangun objek baru (mungkin sebagai sementara) melalui sesuatu seperti T && f = MakeAThing(). (Selain itu, auto &&bahkan berfungsi ketika tipe pengembalian tidak bergerak dan nilai pengembalian adalah nilai awal.)

Kerrek SB
sumber
Jadi ini adalah alasan "hindari penghapusan tipe" untuk digunakan auto. Varian Anda yang lain adalah "hindari salinan yang tidak disengaja", tetapi perlu hiasan; mengapa automemberi Anda kecepatan lebih dari sekadar mengetik jenis di sana? (Saya pikir jawabannya adalah "Anda salah ketik, dan itu diam-diam mengubah") Yang membuatnya menjadi contoh jawaban Barry yang tidak dijelaskan dengan baik, bukan? Yaitu, ada dua kasus dasar: otomatis untuk menghindari penghapusan tipe, dan otomatis untuk menghindari kesalahan tipe diam yang secara tidak sengaja mengkonversi, yang keduanya memiliki biaya waktu berjalan.
Yakk - Adam Nevraumont
2
"tidak hanya panggilan itu tidak dapat disejajarkan" - mengapa begitu? Apakah maksud Anda bahwa pada prinsipnya sesuatu mencegah panggilan didevirtualized setelah analisis aliran data jika spesialisasi yang relevan std::bind, std::functiondan std::stable_partitionsemuanya telah diuraikan? Atau hanya dalam prakteknya tidak ada kompiler C ++ akan inline cukup agresif untuk memilah kekacauan?
Steve Jessop
@ SveveJessop: Sebagian besar yang terakhir - setelah Anda pergi melalui std::functionkonstruktor, itu akan sangat kompleks untuk melihat panggilan aktual, terutama dengan optimisasi fungsi kecil (sehingga Anda tidak benar-benar ingin devirtualization). Tentu saja pada prinsipnya semuanya seolah-olah ...
Kerrek SB
41

Ada dua kategori.

autodapat menghindari tipe erasure. Ada jenis-jenis yang tidak dapat disebutkan namanya (seperti lambdas), dan jenis-jenis yang hampir tidak dapat disebutkan namanya (seperti hasil dari std::bindatau ekspresi-templat sejenis lainnya).

Tanpa auto, Anda akhirnya harus mengetik menghapus data ke sesuatu seperti std::function. Penghapusan tipe membutuhkan biaya.

std::function<void()> task1 = []{std::cout << "hello";};
auto task2 = []{std::cout << " world\n";};

task1memiliki tipe penghapusan overhead - kemungkinan alokasi heap, kesulitan menggarisbawahi, dan overhead tabel fungsi virtual. task2tidak punya. Lambdas membutuhkan auto atau bentuk pengurangan tipe lainnya untuk disimpan tanpa penghapusan tipe; tipe lain bisa sangat kompleks sehingga mereka hanya membutuhkannya dalam praktik.

Kedua, Anda bisa mendapatkan tipe yang salah. Dalam beberapa kasus, tipe yang salah akan bekerja dengan sempurna, tetapi akan menyebabkan salinan.

Foo const& f = expression();

akan dikompilasi jika expression()kembali Bar const&atau Baratau bahkan Bar&, di mana Foodapat dibangun dari Bar. Suatu sementara Fooakan dibuat, kemudian terikat f, dan masa hidupnya akan diperpanjang sampai fhilang.

Pemrogram mungkin bermaksud Bar const& fdan tidak bermaksud membuat salinan di sana, tetapi salinan itu dibuat terlepas.

Contoh paling umum adalah jenis *std::map<A,B>::const_iterator, yang std::pair<A const, B> const&tidak std::pair<A,B> const&, tetapi kesalahan adalah kategori kesalahan yang diam-diam biaya kinerja. Anda dapat membangun std::pair<A, B>dari std::pair<const A, B>. (Kunci pada peta adalah const, karena mengeditnya adalah ide yang buruk)

@Barry dan @KerrekSB, keduanya mengilustrasikan kedua prinsip ini dalam jawaban mereka. Ini hanyalah upaya untuk menyoroti dua masalah dalam satu jawaban, dengan kata-kata yang mengarah pada masalah daripada menjadi contoh-sentris.

Yakk - Adam Nevraumont
sumber
9

Tiga jawaban yang ada memberikan contoh di mana menggunakan automembantu "membuatnya lebih kecil kemungkinannya untuk pesimis secara tidak sengaja" secara efektif menjadikannya "meningkatkan kinerja".

Ada sisi lain dari koin. Menggunakan autodengan objek yang memiliki operator yang tidak mengembalikan objek dasar dapat menghasilkan kode yang salah (masih dapat dikompilasi dan dapat dijalankan). Sebagai contoh, pertanyaan ini menanyakan bagaimana menggunakan automemberi hasil yang berbeda (tidak benar) menggunakan perpustakaan Eigen, yaitu baris berikut

const auto    resAuto    = Ha + Vector3(0.,0.,j * 2.567);
const Vector3 resVector3 = Ha + Vector3(0.,0.,j * 2.567);

std::cout << "resAuto = " << resAuto <<std::endl;
std::cout << "resVector3 = " << resVector3 <<std::endl;

menghasilkan output yang berbeda. Diakui, ini sebagian besar disebabkan oleh evaluasi malas Eigens, tetapi kode itu / harus transparan kepada pengguna (perpustakaan).

Meskipun kinerja tidak terlalu terpengaruh di sini, penggunaan autountuk menghindari pesimisasi yang tidak disengaja dapat diklasifikasikan sebagai pengoptimalan prematur, atau setidaknya salah;).

Avi Ginsburg
sumber
1
Menambahkan pertanyaan sebaliknya: stackoverflow.com/questions/38415831/…
Leon