Saya ingin beberapa informasi tentang bagaimana berpikir dengan benar tentang penutupan C ++ 11 dan std::function
dalam hal bagaimana mereka diimplementasikan dan bagaimana memori ditangani.
Meskipun saya tidak percaya pada pengoptimalan prematur, saya memiliki kebiasaan untuk mempertimbangkan dengan cermat dampak kinerja dari pilihan saya saat menulis kode baru. Saya juga melakukan cukup banyak pemrograman real-time, misalnya pada mikrokontroler dan untuk sistem audio, di mana jeda alokasi / deallokasi memori non-deterministik harus dihindari.
Oleh karena itu, saya ingin mengembangkan pemahaman yang lebih baik tentang kapan harus menggunakan atau tidak menggunakan C ++ lambda.
Pemahaman saya saat ini adalah bahwa lambda tanpa penutupan yang ditangkap persis seperti callback C. Namun, ketika lingkungan ditangkap baik oleh nilai atau referensi, objek anonim dibuat di tumpukan. Ketika nilai-penutupan harus dikembalikan dari fungsi, itu membungkusnya std::function
. Apa yang terjadi pada memori penutupan dalam kasus ini? Apakah itu disalin dari tumpukan ke heap? Apakah dibebaskan setiap kali std::function
dibebaskan, yaitu, apakah dihitung referensi seperti a std::shared_ptr
?
Saya membayangkan bahwa dalam sistem real-time saya dapat menyiapkan rangkaian fungsi lambda, meneruskan B sebagai argumen kelanjutan ke A, sehingga pipeline pemrosesan A->B
dibuat. Dalam hal ini, penutupan A dan B akan dialokasikan satu kali. Meskipun saya tidak yakin apakah ini akan dialokasikan di tumpukan atau heap. Namun secara umum ini tampaknya aman digunakan dalam sistem waktu nyata. Di sisi lain jika B membangun beberapa fungsi lambda C, yang dikembalikannya, maka memori untuk C akan dialokasikan dan dialokasikan berulang kali, yang tidak akan dapat diterima untuk penggunaan waktu nyata.
Dalam pseudo-code, DSP loop, yang menurut saya akan aman secara real-time. Saya ingin melakukan pemrosesan blok A dan kemudian B, di mana A menyebut argumennya. Kedua fungsi ini mengembalikan std::function
objek, jadi f
akan menjadi std::function
objek, tempat lingkungannya disimpan di heap:
auto f = A(B); // A returns a function which calls B
// Memory for the function returned by A is on the heap?
// Note that A and B may maintain a state
// via mutable value-closure!
for (t=0; t<1000; t++) {
y = f(t)
}
Dan yang menurut saya mungkin buruk untuk digunakan dalam kode waktu nyata:
for (t=0; t<1000; t++) {
y = A(B)(t);
}
Dan satu di mana saya pikir memori tumpukan kemungkinan digunakan untuk penutupan:
freq = 220;
A = 2;
for (t=0; t<1000; t++) {
y = [=](int t){ return sin(t*freq)*A; }
}
Dalam kasus terakhir, closure dibangun pada setiap iterasi loop, tetapi tidak seperti contoh sebelumnya, closure murah karena hanya seperti pemanggilan fungsi, tidak ada alokasi heap yang dibuat. Selain itu, saya bertanya-tanya apakah kompiler bisa "mengangkat" penutupan dan membuat optimisasi sebaris.
Apakah ini benar? Terima kasih.
operator()
. Tidak ada "pengangkatan" yang harus dilakukan, lambda bukanlah sesuatu yang istimewa. Mereka hanyalah kependekan dari objek fungsi lokal.std::function
menyimpan statusnya di heap atau tidak, dan tidak ada hubungannya dengan lambda. Apakah itu benar?std::function
!!auto
tipe kembalian.Jawaban:
Tidak; itu selalu merupakan objek C ++ dengan tipe yang tidak diketahui, dibuat di stack. Lambda tanpa capture dapat diubah menjadi penunjuk fungsi (meskipun cocok untuk konvensi pemanggilan C bergantung pada implementasi), tetapi itu tidak berarti itu adalah penunjuk fungsi.
Lambda bukanlah sesuatu yang istimewa di C ++ 11. Itu adalah sebuah objek seperti objek lainnya. Ekspresi lambda menghasilkan sementara, yang dapat digunakan untuk menginisialisasi variabel di tumpukan:
auto lamb = []() {return 5;};
lamb
adalah objek tumpukan. Ia memiliki konstruktor dan destruktor. Dan itu akan mengikuti semua aturan C ++ untuk itu. Jenislamb
akan berisi nilai / referensi yang ditangkap; mereka akan menjadi anggota dari obyek itu, sama seperti anggota obyek lainnya dari tipe lainnya.Anda dapat memberikannya kepada
std::function
:auto func_lamb = std::function<int()>(lamb);
Dalam hal ini, ini akan mendapatkan salinan dari nilai
lamb
. Jikalamb
menangkap sesuatu berdasarkan nilainya, akan ada dua salinan dari nilai-nilai itu; satu masuklamb
, dan satu lagifunc_lamb
.Ketika ruang lingkup saat ini berakhir,
func_lamb
akan dihancurkan, diikuti olehlamb
, sesuai aturan pembersihan variabel tumpukan.Anda dapat dengan mudah mengalokasikannya di heap:
auto func_lamb_ptr = new std::function<int()>(lamb);
Tepatnya di mana memori untuk konten
std::function
pergi bergantung pada implementasi, tetapi penghapusan jenis yang digunakanstd::function
umumnya memerlukan setidaknya satu alokasi memori. Inilah sebabnya mengapastd::function
konstruktor dapat menggunakan pengalokasi.std::function
menyimpan salinan isinya. Seperti hampir semua jenis pustaka standar C ++,function
menggunakan semantik nilai . Jadi, itu bisa disalin; ketika disalin,function
objek baru benar-benar terpisah. Ini juga dapat dipindahkan, sehingga alokasi internal apa pun dapat ditransfer dengan tepat tanpa perlu lebih banyak alokasi dan penyalinan.Jadi tidak perlu menghitung referensi.
Segala sesuatu yang Anda nyatakan benar, dengan asumsi bahwa "alokasi memori" sama dengan "buruk untuk digunakan dalam kode waktu nyata".
sumber
std::function
adalah titik di mana memori dialokasikan dan disalin. Tampaknya mengikuti bahwa tidak ada cara untuk mengembalikan penutupan (karena mereka dialokasikan di tumpukan), tanpa terlebih dahulu menyalin ke astd::function
, ya?std::function
objek tanpa memori dinamis alokasi terjadi.function
memiliki konstruktor pemindahan noexcept. Inti dari mengatakan "secara umum membutuhkan" adalah bahwa saya tidak mengatakan " selalu membutuhkan": bahwa ada keadaan di mana tidak ada alokasi yang akan dilakukan.C ++ lambda hanyalah gula sintaksis di sekitar (anonim) kelas Functor dengan kelebihan beban
operator()
danstd::function
hanya pembungkus di sekitar callables (yaitu functors, lambda , c-functions, ...) yang menyalin dengan nilai "objek lambda padat" dari arus stack scope - ke heap .Untuk menguji jumlah konstruktor / relokasi yang sebenarnya, saya melakukan pengujian (menggunakan tingkat pembungkusan lain ke shared_ptr tetapi tidak demikian). Lihat diri mu sendiri:
#include <memory> #include <string> #include <iostream> class Functor { std::string greeting; public: Functor(const Functor &rhs) { this->greeting = rhs.greeting; std::cout << "Copy-Ctor \n"; } Functor(std::string _greeting="Hello!"): greeting { _greeting } { std::cout << "Ctor \n"; } Functor & operator=(const Functor & rhs) { greeting = rhs.greeting; std::cout << "Copy-assigned\n"; return *this; } virtual ~Functor() { std::cout << "Dtor\n"; } void operator()() { std::cout << "hey" << "\n"; } }; auto getFpp() { std::shared_ptr<std::function<void()>> fp = std::make_shared<std::function<void()>>(Functor{} ); (*fp)(); return fp; } int main() { auto f = getFpp(); (*f)(); }
itu membuat keluaran ini:
Set ctors / dtors yang sama persis akan dipanggil untuk objek lambda yang dialokasikan tumpukan! (Sekarang memanggil Ctor untuk alokasi tumpukan, Copy-ctor (+ alokasi heap) untuk membangunnya di std :: function dan satu lagi untuk membuat alokasi heap shared_ptr + konstruksi fungsi)
sumber