Implementasi C ++ 11 lambda dan model memori

97

Saya ingin beberapa informasi tentang bagaimana berpikir dengan benar tentang penutupan C ++ 11 dan std::functiondalam 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::functiondibebaskan, 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->Bdibuat. 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::functionobjek, jadi fakan menjadi std::functionobjek, 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.

Steve
sumber
4
Tidak ada biaya tambahan saat menggunakan ekspresi lambda. Pilihan lainnya adalah menulis sendiri objek fungsi tersebut, yang akan persis sama. Btw, pada pertanyaan sebaris, karena kompilator memiliki semua informasi yang dibutuhkannya, ia yakin dapat menyebariskan panggilan ke operator(). Tidak ada "pengangkatan" yang harus dilakukan, lambda bukanlah sesuatu yang istimewa. Mereka hanyalah kependekan dari objek fungsi lokal.
Xeo
Ini tampaknya menjadi pertanyaan tentang apakah std::functionmenyimpan statusnya di heap atau tidak, dan tidak ada hubungannya dengan lambda. Apakah itu benar?
Mooing Duck
8
Hanya untuk mengejanya dalam hal terjadi kesalahpahaman: Ekspresi lambda adalah tidak seorang std::function!!
Xeo
1
Hanya komentar sampingan: hati-hati saat mengembalikan lambda dari suatu fungsi, karena variabel lokal apa pun yang ditangkap oleh referensi menjadi tidak valid setelah meninggalkan fungsi yang telah membuat lambda.
Giorgio
2
@Steve sejak C ++ 14 Anda bisa mengembalikan lambda dari fungsi dengan autotipe kembalian.
Oktalis

Jawaban:

104

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.

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.

Ketika nilai-closure harus dikembalikan dari sebuah fungsi, ia membungkusnya dalam std :: function. Apa yang terjadi pada memori penutupan dalam kasus ini?

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;};

lambadalah objek tumpukan. Ia memiliki konstruktor dan destruktor. Dan itu akan mengikuti semua aturan C ++ untuk itu. Jenis lambakan 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. Jika lambmenangkap sesuatu berdasarkan nilainya, akan ada dua salinan dari nilai-nilai itu; satu masuk lamb, dan satu lagi func_lamb.

Ketika ruang lingkup saat ini berakhir, func_lambakan dihancurkan, diikuti oleh lamb, 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::functionpergi bergantung pada implementasi, tetapi penghapusan jenis yang digunakan std::functionumumnya memerlukan setidaknya satu alokasi memori. Inilah sebabnya mengapa std::functionkonstruktor dapat menggunakan pengalokasi.

Apakah dibebaskan setiap kali std :: function dibebaskan, yaitu, apakah itu dihitung berdasarkan referensi seperti std :: shared_ptr?

std::functionmenyimpan salinan isinya. Seperti hampir semua jenis pustaka standar C ++, functionmenggunakan semantik nilai . Jadi, itu bisa disalin; ketika disalin, functionobjek 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".

Nicol Bolas
sumber
1
Penjelasan yang bagus, terima kasih. Jadi pembuatan std::functionadalah 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 a std::function, ya?
Steve
3
@ Steve: Ya; Anda harus membungkus lambda dalam beberapa jenis wadah agar dapat keluar dari ruang lingkup.
Nicol Bolas
Apakah seluruh kode fungsi disalin, atau apakah fungsi asli waktu kompilasi dialokasikan dan diteruskan nilai tertutup?
Llamageddon
Saya ingin menambahkan bahwa standar kurang lebih secara tidak langsung mengamanatkan (§ 20.8.11.2.1 [func.wrap.func.con] ¶ 5) bahwa jika lambda tidak menangkap apa pun, itu dapat disimpan dalam std::functionobjek tanpa memori dinamis alokasi terjadi.
5gon12eder
2
@Yakk: Bagaimana Anda mendefinisikan "besar"? Apakah sebuah objek dengan dua petunjuk status "besar"? Bagaimana dengan 3 atau 4? Selain itu, ukuran objek bukan satu-satunya masalah; jika objek tidak nothrow-moveable, itu harus disimpan dalam alokasi, karena functionmemiliki 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.
Nicol Bolas
1

C ++ lambda hanyalah gula sintaksis di sekitar (anonim) kelas Functor dengan kelebihan beban operator()dan std::functionhanya 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:

Ctor 
Copy-Ctor 
Copy-Ctor 
Dtor
Dtor
hey
hey
Dtor

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)

barney
sumber