Berkat C ++ 11 kami menerima std::function
keluarga pembungkus functor. Sayangnya, saya terus mendengar hanya hal-hal buruk tentang penambahan baru ini. Yang paling populer adalah mereka sangat lambat. Saya mengujinya dan mereka benar-benar payah dibandingkan dengan template.
#include <iostream>
#include <functional>
#include <string>
#include <chrono>
template <typename F>
float calc1(F f) { return -1.0f * f(3.3f) + 666.0f; }
float calc2(std::function<float(float)> f) { return -1.0f * f(3.3f) + 666.0f; }
int main() {
using namespace std::chrono;
const auto tp1 = system_clock::now();
for (int i = 0; i < 1e8; ++i) {
calc1([](float arg){ return arg * 0.5f; });
}
const auto tp2 = high_resolution_clock::now();
const auto d = duration_cast<milliseconds>(tp2 - tp1);
std::cout << d.count() << std::endl;
return 0;
}
111 ms vs 1241 ms. Saya berasumsi ini karena templat dapat digarisbawahi dengan baik, sementara function
itu menutupi internal melalui panggilan virtual.
Jelas template memiliki masalah mereka seperti yang saya lihat:
- mereka harus disediakan sebagai tajuk yang bukan sesuatu yang Anda mungkin tidak ingin lakukan ketika merilis perpustakaan Anda sebagai kode tertutup,
- mereka dapat membuat waktu kompilasi lebih lama kecuali
extern template
kebijakan -seperti diperkenalkan, - tidak ada (setidaknya diketahui oleh saya) cara yang bersih untuk mewakili persyaratan (konsep, siapa pun?) dari sebuah template, batalkan komentar yang menggambarkan fungsi apa yang diharapkan.
Dapatkah saya dengan demikian mengasumsikan bahwa function
s dapat digunakan sebagai standar de facto untuk fungsi yang lewat, dan di tempat-tempat di mana templat yang diharapkan berkinerja tinggi harus digunakan?
Edit:
Kompiler saya adalah Visual Studio 2012 tanpa CTP.
sumber
std::function
jika dan hanya jika Anda benar - benar membutuhkan koleksi objek yang dapat dipanggil yang heterogen (yaitu tidak ada informasi diskriminatif lebih lanjut tersedia saat runtime).std::function
atau template". Saya pikir di sini masalahnya hanya membungkus lambdastd::function
vs tidak membungkus lambdastd::function
. Saat ini pertanyaan Anda seperti bertanya, "Haruskah saya lebih suka apel, atau mangkuk?"Jawaban:
Secara umum, jika Anda menghadapi situasi desain yang memberi Anda pilihan, gunakan template . Saya menekankan desain kata karena saya pikir apa yang Anda perlu fokus adalah perbedaan antara kasus penggunaan
std::function
dan template, yang sangat berbeda.Secara umum, pilihan templat hanyalah turunan dari prinsip yang lebih luas: coba tentukan sebanyak mungkin kendala pada waktu kompilasi . Dasar pemikirannya sederhana: jika Anda dapat menangkap kesalahan, atau jenis ketidakcocokan, bahkan sebelum program Anda dibuat, Anda tidak akan mengirimkan program kereta kepada pelanggan Anda.
Selain itu, ketika Anda menunjukkan dengan benar, panggilan ke fungsi template diselesaikan secara statis (yaitu pada waktu kompilasi), sehingga kompiler memiliki semua informasi yang diperlukan untuk mengoptimalkan dan mungkin inline kode (yang tidak akan mungkin jika panggilan dilakukan melalui vtable).
Ya, memang benar bahwa dukungan template tidak sempurna, dan C ++ 11 masih kurang mendukung konsep; Namun, saya tidak melihat bagaimana
std::function
menyelamatkan Anda dalam hal itu.std::function
bukan alternatif untuk templat, melainkan alat untuk situasi desain di mana templat tidak dapat digunakan.Salah satu kasus penggunaan tersebut muncul ketika Anda harus menyelesaikan panggilan pada saat run-time dengan memanggil objek yang bisa dipanggil yang mematuhi tanda tangan tertentu, tetapi yang jenis konkretnya tidak diketahui pada waktu kompilasi. Ini biasanya terjadi ketika Anda memiliki koleksi panggilan balik dari tipe-tipe yang berpotensi berbeda , tetapi Anda perlu memohon secara seragam ; jenis dan jumlah panggilan balik terdaftar ditentukan pada saat run-time berdasarkan keadaan program Anda dan logika aplikasi. Beberapa dari callback tersebut dapat berupa functors, beberapa dapat berupa fungsi yang sederhana, beberapa dapat merupakan hasil dari pengikatan fungsi lainnya dengan argumen tertentu.
std::function
danstd::bind
juga menawarkan idiom alami untuk mengaktifkan pemrograman fungsional dalam C ++, di mana fungsi diperlakukan sebagai objek dan secara alami digulung dan digabungkan untuk menghasilkan fungsi lainnya. Meskipun kombinasi semacam ini dapat dicapai dengan templat juga, situasi desain serupa biasanya datang bersama dengan use case yang mengharuskan untuk menentukan jenis objek yang dapat dipanggil yang dipadukan pada saat run-time.Akhirnya, ada situasi lain di mana
std::function
tidak dapat dihindari, misalnya jika Anda ingin menulis lambda rekursif ; Namun, pembatasan ini lebih didikte oleh keterbatasan teknologi daripada oleh perbedaan konseptual yang saya percaya.Singkatnya, fokuslah pada desain dan cobalah untuk memahami apa saja kasus penggunaan konseptual untuk dua konstruksi ini. Jika Anda membandingkannya dengan cara yang Anda lakukan, Anda memaksa mereka masuk ke arena yang mungkin bukan milik mereka.
sumber
std::function
di ujung penyimpanan dan templatFun
di antarmuka".unique_ptr<void>
memanggil destruktor yang tepat bahkan untuk tipe tanpa destruktor virtual).Andy Prowl telah membahas masalah desain dengan baik. Ini, tentu saja, sangat penting, tetapi saya percaya pertanyaan awal menyangkut masalah kinerja yang lebih terkait
std::function
.Pertama-tama, komentar singkat tentang teknik pengukuran: 11ms yang diperoleh
calc1
tidak memiliki arti sama sekali. Memang, melihat rakitan yang dihasilkan (atau men-debug kode rakitan), orang dapat melihat bahwa pengoptimal VS2012 cukup pintar untuk menyadari bahwa hasil panggilancalc1
tidak tergantung pada iterasi dan memindahkan panggilan keluar dari loop:Selain itu, ia menyadari bahwa panggilan
calc1
tidak memiliki efek yang terlihat dan menjatuhkan panggilan sama sekali. Oleh karena itu, 111ms adalah waktu yang diperlukan untuk menjalankan loop kosong. (Saya terkejut bahwa pengoptimal menjaga loop.) Jadi, berhati-hatilah dengan pengukuran waktu dalam loop. Ini tidak sesederhana kelihatannya.Seperti yang telah ditunjukkan, pengoptimal memiliki lebih banyak masalah untuk dipahami
std::function
dan tidak memindahkan panggilan keluar dari loop. Jadi 1241ms adalah ukuran yang adil untukcalc2
.Perhatikan bahwa,
std::function
mampu menyimpan berbagai jenis objek yang bisa dipanggil. Oleh karena itu, ia harus melakukan beberapa sihir penghapus tipe untuk penyimpanan. Secara umum, ini menyiratkan alokasi memori dinamis (secara default melalui panggilan kenew
). Sudah diketahui bahwa ini adalah operasi yang cukup mahal.Standar (20.8.11.2.1 / 5) mendukung implementasi untuk menghindari alokasi memori dinamis untuk objek kecil yang, untungnya, VS2012 melakukannya (khususnya, untuk kode asli).
Untuk mendapatkan gambaran tentang seberapa lambat yang bisa didapat ketika alokasi memori terlibat, saya telah mengubah ekspresi lambda untuk menangkap tiga
float
. Ini membuat objek yang bisa dipanggil terlalu besar untuk menerapkan optimasi objek kecil:Untuk versi ini, waktunya sekitar 16000 ms (dibandingkan dengan 1241 ms untuk kode asli).
Akhirnya, perhatikan bahwa masa hidup lambda melampirkan bahwa dari
std::function
. Dalam hal ini, alih-alih menyimpan salinan lambda,std::function
dapat menyimpan "referensi" untuknya. Dengan "referensi" Maksud sayastd::reference_wrapper
yang mudah dibangun oleh fungsistd::ref
danstd::cref
. Lebih tepatnya, dengan menggunakan:waktu berkurang menjadi sekitar 1860 ms.
Saya menulis tentang itu beberapa waktu yang lalu:
http://www.drdobbs.com/cpp/efisien-use-of-lambda-expressions-and/232500059
Seperti yang saya katakan di artikel, argumen tidak cukup berlaku untuk VS2010 karena dukungannya yang buruk untuk C ++ 11. Pada saat penulisan, hanya versi beta VS2012 yang tersedia tetapi dukungannya untuk C ++ 11 sudah cukup baik untuk masalah ini.
sumber
calc1
bisa mengambilfloat
argumen yang akan menjadi hasil dari iterasi sebelumnya. Sesuatu sepertix = calc1(x, [](float arg){ return arg * 0.5f; });
. Selain itu, kami harus memastikancalc1
penggunaannyax
. Tapi, ini belum cukup. Kita perlu menciptakan efek samping. Misalnya, setelah pengukuran, mencetakx
pada layar. Meskipun demikian, saya setuju bahwa menggunakan kode mainan untuk pengukuran timimg tidak selalu dapat memberikan indikasi sempurna tentang apa yang akan terjadi dengan kode nyata / produksi.std::reference_wrapper
(untuk memaksa template; ini bukan hanya untuk penyimpanan umum), dan itu lucu melihat pengoptimal VS gagal untuk membuang loop kosong ... seperti yang saya perhatikan dengan bug GCC inivolatile
.Dengan Dentang tidak ada perbedaan kinerja antara keduanya
Menggunakan dentang (3.2, trunk 166872) (-O2 di Linux), binari dari kedua case tersebut sebenarnya identik .
-Aku akan kembali ke dentang di akhir posting. Tapi pertama-tama, gcc 4.7.2:
Sudah ada banyak wawasan yang terjadi, tetapi saya ingin menunjukkan bahwa hasil perhitungan calc1 dan calc2 tidak sama, karena in-lining dll. Bandingkan misalnya jumlah dari semua hasil:
dengan calc2 yang menjadi
sementara dengan calc1 menjadi
itu faktor ~ 40 dalam perbedaan kecepatan, dan faktor ~ 4 dalam nilai. Yang pertama adalah perbedaan yang jauh lebih besar daripada yang diposting OP (menggunakan studio visual). Sebenarnya mencetak nilai dan akhirnya juga merupakan ide yang baik untuk mencegah kompiler untuk menghapus kode tanpa hasil yang terlihat (as-jika aturan). Cassio Neri sudah mengatakan ini dalam jawabannya. Perhatikan betapa berbedanya hasilnya - Orang harus berhati-hati ketika membandingkan faktor kecepatan kode yang melakukan perhitungan yang berbeda.
Juga, untuk bersikap adil, membandingkan berbagai cara penghitungan berulang kali f (3.3) mungkin tidak begitu menarik. Jika inputnya konstan, seharusnya tidak dalam satu lingkaran. (Pengoptimal mudah memperhatikan)
Jika saya menambahkan argumen nilai yang disediakan pengguna ke Calc1 dan 2 faktor kecepatan antara Calc1 dan Calc2 turun ke faktor 5, dari 40! Dengan visual studio perbedaannya dekat dengan faktor 2, dan dengan dentang tidak ada perbedaan (lihat di bawah).
Juga, karena perkalian cepat, berbicara tentang faktor-faktor perlambatan seringkali tidak menarik. Pertanyaan yang lebih menarik adalah, seberapa kecil fungsi Anda, dan apakah ini disebut hambatan dalam program nyata?
Dentang:
Dentang (saya menggunakan 3,2) benar-benar menghasilkan binari identik ketika saya membalik antara calc1 dan calc2 untuk kode contoh (diposting di bawah). Dengan contoh asli yang diposting dalam pertanyaan, keduanya juga identik tetapi tidak membutuhkan waktu sama sekali (loop hanya dihapus sepenuhnya seperti dijelaskan di atas). Dengan contoh saya yang dimodifikasi, dengan -O2:
Jumlah detik untuk dieksekusi (terbaik 3):
Hasil perhitungan semua binari adalah sama, dan semua tes dieksekusi pada mesin yang sama. Akan menarik jika seseorang dengan dentang yang lebih dalam atau pengetahuan VS dapat mengomentari optimasi apa yang mungkin telah dilakukan.
Kode pengujian saya yang dimodifikasi:
Memperbarui:
Ditambahkan vs2015. Saya juga memperhatikan bahwa ada konversi float ganda di calc1, calc2. Menghapusnya tidak mengubah kesimpulan untuk studio visual (keduanya jauh lebih cepat tetapi rasionya hampir sama).
sumber
Berbeda tidak sama.
Itu lebih lambat karena ia melakukan hal-hal yang tidak bisa dilakukan templat. Secara khusus, ini memungkinkan Anda memanggil fungsi apa pun yang dapat dipanggil dengan tipe argumen yang diberikan dan yang tipe pengembaliannya dapat dikonversi ke tipe pengembalian yang diberikan dari kode yang sama .
Perhatikan bahwa objek fungsi yang sama
fun
,, sedang diteruskan ke kedua panggilan keeval
. Ini memegang dua fungsi yang berbeda .Jika Anda tidak perlu melakukan itu, maka Anda sebaiknya tidak menggunakannya
std::function
.sumber
Anda sudah memiliki beberapa jawaban yang bagus di sini, jadi saya tidak akan menentangnya, singkatnya membandingkan fungsi std :: dengan templat seperti membandingkan fungsi virtual ke fungsi. Anda tidak boleh "lebih suka" fungsi virtual daripada fungsi, tetapi Anda menggunakan fungsi virtual saat itu sesuai dengan masalahnya, memindahkan keputusan dari waktu kompilasi ke waktu berjalan. Idenya adalah bahwa daripada harus menyelesaikan masalah menggunakan solusi dipesan lebih dahulu (seperti meja lompatan) Anda menggunakan sesuatu yang memberi kompiler kesempatan yang lebih baik untuk mengoptimalkan untuk Anda. Ini juga membantu programmer lain, jika Anda menggunakan solusi standar.
sumber
Jawaban ini dimaksudkan untuk berkontribusi, ke set jawaban yang ada, apa yang saya yakini sebagai tolok ukur yang lebih bermakna untuk biaya runtime panggilan std :: function.
Mekanisme std :: function harus dikenali dari apa yang disediakannya: Entitas apa pun yang dapat dipanggil dapat dikonversi ke fungsi std :: function dari tanda tangan yang sesuai. Misalkan Anda memiliki pustaka yang sesuai dengan permukaan ke fungsi yang didefinisikan oleh z = f (x, y), Anda dapat menulisnya untuk menerima a
std::function<double(double,double)>
, dan pengguna perpustakaan dapat dengan mudah mengkonversi entitas yang dapat dipanggil apa pun menjadi itu; baik itu fungsi biasa, metode instance kelas, atau lambda, atau apa pun yang didukung oleh std :: bind.Tidak seperti pendekatan templat, ini bekerja tanpa harus mengkompilasi ulang fungsi pustaka untuk berbagai kasus; oleh karena itu, sedikit kode terkompilasi ekstra diperlukan untuk setiap kasus tambahan. Itu selalu mungkin untuk membuat ini terjadi, tetapi dulu memerlukan beberapa mekanisme canggung, dan pengguna perpustakaan mungkin perlu membangun adaptor di sekitar fungsi mereka untuk membuatnya berfungsi. std :: function secara otomatis membuat adaptor apa pun yang diperlukan untuk mendapatkan antarmuka panggilan runtime yang umum untuk semua kasus, yang merupakan fitur baru dan sangat kuat.
Menurut pendapat saya, ini adalah use case yang paling penting untuk fungsi std :: sejauh menyangkut kinerja: Saya tertarik pada biaya memanggil fungsi std :: berkali-kali setelah dibangun satu kali, dan perlu menjadi situasi di mana kompiler tidak dapat mengoptimalkan panggilan dengan mengetahui fungsi yang sebenarnya dipanggil (yaitu Anda harus menyembunyikan implementasi di file sumber lain untuk mendapatkan patokan yang tepat).
Saya membuat tes di bawah ini, mirip dengan OP; tetapi perubahan utamanya adalah:
Hasil yang saya dapatkan adalah:
case (a) (inline) 1.3 nsec
semua kasus lain: 3,3 nsec.
Kasus (d) cenderung sedikit lebih lambat, tetapi perbedaannya (sekitar 0,05 nsec) diserap dalam kebisingan.
Kesimpulannya adalah bahwa fungsi std :: adalah overhead yang sebanding (pada waktu panggilan) untuk menggunakan pointer fungsi, bahkan ketika ada adaptasi 'bind' sederhana dengan fungsi sebenarnya. Inline adalah 2 ns lebih cepat daripada yang lain tapi itu tradeoff yang diharapkan karena inline adalah satu-satunya kasus yang 'terprogram' pada saat run time.
Ketika saya menjalankan kode johan-lundberg pada mesin yang sama, saya melihat sekitar 39 nsec per loop, tetapi ada lebih banyak di loop di sana, termasuk konstruktor dan destruktor sebenarnya dari fungsi std :: function, yang mungkin cukup tinggi karena itu melibatkan yang baru dan hapus.
-O2 gcc 4.8.1, untuk target x86_64 (core i5).
Catatan, kode ini dipecah menjadi dua file, untuk mencegah kompiler memperluas fungsi di mana mereka dipanggil (kecuali dalam satu kasus di mana itu dimaksudkan untuk).
----- file sumber pertama --------------
----- file sumber kedua -------------
Bagi mereka yang tertarik, inilah adaptor yang dibuat kompiler untuk membuat 'mul_by' terlihat seperti float (float) - ini disebut 'ketika fungsi yang dibuat sebagai bind (mul_by, _1.0.5) dipanggil:
(jadi mungkin sedikit lebih cepat jika saya menulis 0,5f di bind ...) Perhatikan bahwa parameter 'x' tiba di% xmm0 dan tetap di sana.
Berikut kode di area di mana fungsi dibangun, sebelum memanggil test_stdfunc - jalankan melalui c ++ filt:
sumber
Saya menemukan hasil Anda sangat menarik sehingga saya melakukan sedikit penggalian untuk memahami apa yang sedang terjadi. Pertama karena banyak orang lain mengatakan tanpa memiliki hasil efek komputasi keadaan program, kompiler hanya akan mengoptimalkan ini. Kedua memiliki konstanta 3,3 yang diberikan sebagai persenjataan untuk panggilan balik saya menduga bahwa akan ada optimasi lain yang terjadi. Dengan mengingat hal itu saya sedikit mengubah kode benchmark Anda.
Mengingat perubahan ini pada kode saya dikompilasi dengan gcc 4.8 -O3 dan mendapat waktu 330ms untuk Calc1 dan 2702 untuk Calc2. Jadi menggunakan template itu 8 kali lebih cepat, angka ini tampak mencurigakan bagi saya, kecepatan kekuatan 8 sering menunjukkan bahwa kompiler telah membuat vektor sesuatu. ketika saya melihat kode yang dihasilkan untuk versi templat, kode itu jelas-jelas divectoreized
Sedangkan versi std :: function tidak. Ini masuk akal bagi saya, karena dengan templat kompiler tahu pasti bahwa fungsi tidak akan pernah berubah di seluruh loop tetapi dengan std :: fungsi yang diteruskan di dalamnya bisa berubah, karenanya tidak dapat vektor.
Ini mendorong saya untuk mencoba sesuatu yang lain untuk melihat apakah saya bisa mendapatkan kompiler untuk melakukan optimasi yang sama pada std :: function versi. Alih-alih meneruskan fungsi saya membuat fungsi std :: sebagai global var, dan memanggil ini.
Dengan versi ini kita melihat bahwa kompiler sekarang telah membuat vektor kode dengan cara yang sama dan saya mendapatkan hasil benchmark yang sama.
Jadi kesimpulan saya adalah kecepatan mentah dari std :: function vs functor template hampir sama. Namun itu membuat pekerjaan pengoptimal jauh lebih sulit.
sumber
calc3
Kasus Anda tidak masuk akal; calc3 sekarang hardcoded untuk memanggil f2. Tentu saja itu bisa dioptimalkan.