การใช้งานแลมด้า C ++ 11 และโมเดลหน่วยความจำ


97

ฉันต้องการข้อมูลเกี่ยวกับวิธีการคิดอย่างถูกต้องเกี่ยวกับการปิด C ++ 11 และstd::functionในแง่ของวิธีการใช้งานและวิธีจัดการหน่วยความจำ

แม้ว่าฉันจะไม่เชื่อในการเพิ่มประสิทธิภาพก่อนกำหนด แต่ฉันก็มีนิสัยในการพิจารณาผลกระทบด้านประสิทธิภาพของตัวเลือกของฉันอย่างรอบคอบในขณะที่เขียนโค้ดใหม่ ฉันยังเขียนโปรแกรมแบบเรียลไทม์ในปริมาณพอสมควรเช่นในไมโครคอนโทรลเลอร์และสำหรับระบบเสียงซึ่งต้องหลีกเลี่ยงการจัดสรรหน่วยความจำแบบไม่กำหนด / การหยุดการจัดสรร

ดังนั้นฉันจึงต้องการพัฒนาความเข้าใจให้ดีขึ้นว่าเมื่อใดควรใช้หรือไม่ใช้ C ++ lambdas

ความเข้าใจปัจจุบันของฉันคือแลมด้าที่ไม่มีการปิดจับนั้นเหมือนกับการเรียกกลับ C อย่างไรก็ตามเมื่อสภาพแวดล้อมถูกจับด้วยค่าหรือโดยการอ้างอิงอ็อบเจ็กต์ที่ไม่ระบุชื่อจะถูกสร้างขึ้นบนสแต็ก std::functionเมื่อมูลค่าปิดต้องถูกส่งกลับจากฟังก์ชั่นหนึ่งใน wraps จะเกิดอะไรขึ้นกับหน่วยความจำการปิดในกรณีนี้? คัดลอกจากสแต็กไปยังฮีปหรือไม่ เป็นอิสระเมื่อใดก็ตามที่std::functionเป็นอิสระกล่าวคือมีการนับการอ้างอิงเหมือน a std::shared_ptrหรือไม่?

ฉันคิดว่าในระบบเวลาจริงฉันสามารถตั้งค่าฟังก์ชั่นห่วงโซ่ของแลมบ์ดาผ่าน B เป็นอาร์กิวเมนต์ต่อเนื่องไปเพื่อให้ท่อการประมวลผลA->Bจะถูกสร้างขึ้น ในกรณีนี้การปิด A และ B จะถูกจัดสรรครั้งเดียว แม้ว่าฉันไม่แน่ใจว่าสิ่งเหล่านี้จะถูกจัดสรรบนสแต็กหรือฮีป อย่างไรก็ตามโดยทั่วไปดูเหมือนว่าปลอดภัยที่จะใช้ในระบบเรียลไทม์ ในทางกลับกันถ้า B สร้างฟังก์ชันแลมบ์ดาบางฟังก์ชัน C ซึ่งส่งคืนหน่วยความจำสำหรับ C จะได้รับการจัดสรรและจัดสรรซ้ำ ๆ ซึ่งจะไม่เป็นที่ยอมรับสำหรับการใช้งานแบบเรียลไทม์

ในรหัสหลอกคือ DSP loop ซึ่งฉันคิดว่าจะปลอดภัยแบบเรียลไทม์ ฉันต้องการประมวลผลบล็อก A และ B โดยที่ A เรียกใช้อาร์กิวเมนต์ ฟังก์ชั่นทั้งสองนี้ส่งคืนstd::functionอ็อบเจ็กต์ดังนั้นfจะเป็นstd::functionอ็อบเจ็กต์ที่สภาพแวดล้อมของมันถูกเก็บไว้บนฮีป:

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

และสิ่งที่ฉันคิดว่าอาจใช้ไม่ดีในโค้ดเรียลไทม์:

for (t=0; t<1000; t++) {
    y = A(B)(t);
}

และสิ่งหนึ่งที่ฉันคิดว่าหน่วยความจำสแต็กน่าจะใช้สำหรับการปิด:

freq = 220;
A = 2;
for (t=0; t<1000; t++) {
    y = [=](int t){ return sin(t*freq)*A; }
}

ในกรณีหลังนี้การปิดจะถูกสร้างขึ้นในการวนซ้ำแต่ละครั้ง แต่ไม่เหมือนกับตัวอย่างก่อนหน้านี้คือราคาถูกเนื่องจากเหมือนกับการเรียกใช้ฟังก์ชันไม่มีการจัดสรรฮีป ยิ่งไปกว่านั้นฉันสงสัยว่าคอมไพเลอร์สามารถ "ยก" การปิดและทำการเพิ่มประสิทธิภาพแบบอินไลน์ได้หรือไม่

ถูกต้องหรือไม่ ขอบคุณ.


4
ไม่มีค่าใช้จ่ายเมื่อใช้นิพจน์แลมบ์ดา อีกทางเลือกหนึ่งคือการเขียนวัตถุฟังก์ชันดังกล่าวด้วยตัวคุณเองซึ่งจะเหมือนกันทุกประการ Btw สำหรับคำถามแบบอินไลน์เนื่องจากคอมไพเลอร์มีข้อมูลทั้งหมดที่ต้องการจึงแน่ใจได้ว่าสามารถอินไลน์โทรไปยังไฟล์operator(). ไม่มีการ "ยก" ให้เสร็จแลมดาก็ไม่ได้มีอะไรพิเศษ พวกมันเป็นเพียงส่วนสั้น ๆ สำหรับอ็อบเจ็กต์ฟังก์ชันเฉพาะที่
Xeo

ดูเหมือนจะเป็นคำถามเกี่ยวกับว่าstd::functionเก็บสถานะไว้ในฮีปหรือไม่และไม่มีส่วนเกี่ยวข้องกับแลมบ์ดาส นั่นถูกต้องใช่ไหม?
Mooing Duck

8
เพียงเพื่อสะกดออกในกรณีของความเข้าใจผิดใด ๆแสดงออกแลมบ์ดาคือไม่std::function !!
Xeo

1
เป็นเพียงความคิดเห็นด้านข้าง: โปรดใช้ความระมัดระวังในการส่งคืนแลมด้าจากฟังก์ชันเนื่องจากตัวแปรโลคัลใด ๆ ที่จับโดยการอ้างอิงจะไม่ถูกต้องหลังจากออกจากฟังก์ชันที่สร้างแลมด้า
Giorgio

2
@Steve ตั้งแต่ C ++ 14 คุณสามารถคืนค่าแลมด้าจากฟังก์ชันที่มีautoชนิดการส่งคืน
Oktalist

คำตอบ:


104

ความเข้าใจปัจจุบันของฉันคือแลมด้าที่ไม่มีการปิดจับนั้นเหมือนกับการเรียกกลับ C อย่างไรก็ตามเมื่อสภาพแวดล้อมถูกจับด้วยค่าหรือโดยการอ้างอิงอ็อบเจ็กต์ที่ไม่ระบุชื่อจะถูกสร้างขึ้นบนสแต็ก

ไม่; มันเป็นเสมอ c ++ วัตถุที่มีชนิดที่ไม่รู้จักสร้างขึ้นบนสแต็ค แลมบ์ดาที่ไม่จับภาพสามารถแปลงเป็นตัวชี้ฟังก์ชันได้ (แม้ว่าจะเหมาะกับรูปแบบการเรียก C หรือไม่นั้นขึ้นอยู่กับการนำไปใช้งาน) แต่ไม่ได้หมายความว่าเป็นตัวชี้ฟังก์ชัน

เมื่อต้องส่งคืนค่าปิดจากฟังก์ชันหนึ่งจะรวมไว้ในฟังก์ชัน std :: จะเกิดอะไรขึ้นกับหน่วยความจำการปิดในกรณีนี้?

แลมด้าไม่ได้มีอะไรพิเศษใน C ++ 11 มันเป็นวัตถุที่เหมือนกับวัตถุอื่น ๆ นิพจน์แลมบ์ดาส่งผลให้เกิดชั่วคราวซึ่งสามารถใช้เพื่อเริ่มต้นตัวแปรบนสแต็ก:

auto lamb = []() {return 5;};

lambเป็นวัตถุสแต็ก มันมีตัวสร้างและตัวทำลาย และจะเป็นไปตามกฎ C ++ ทั้งหมดสำหรับสิ่งนั้น ประเภทของlambพินัยกรรมประกอบด้วยค่า / การอ้างอิงที่บันทึก; พวกเขาจะเป็นสมาชิกของวัตถุนั้นเช่นเดียวกับสมาชิกวัตถุอื่น ๆ ในประเภทอื่น ๆ

คุณสามารถมอบให้กับstd::function:

auto func_lamb = std::function<int()>(lamb);

ในกรณีนี้จะได้รับไฟล์ สำเนาค่าของlamb. หากlambจับสิ่งใดตามมูลค่าจะมีสำเนาของค่าเหล่านั้นสองชุด หนึ่งในและเป็นหนึ่งในlambfunc_lamb

เมื่อขอบเขตปัจจุบันสิ้นสุดลงfunc_lambจะถูกทำลายตามด้วยlambตามกฎของการล้างตัวแปรสแตก

คุณสามารถจัดสรรหนึ่งบนฮีปได้อย่างง่ายดาย:

auto func_lamb_ptr = new std::function<int()>(lamb);

ตรงที่หน่วยความจำสำหรับเนื้อหาของไฟล์ std::functionไปนั้นขึ้นอยู่กับการใช้งาน แต่การลบประเภทที่ใช้โดยstd::functionทั่วไปต้องการการจัดสรรหน่วยความจำอย่างน้อยหนึ่งครั้ง นี่คือเหตุผลstd::functionที่ตัวสร้างสามารถใช้ตัวจัดสรร

เป็นอิสระเมื่อใดก็ตามที่ฟังก์ชัน std :: เป็นอิสระกล่าวคือมีการนับการอ้างอิงเหมือน std :: shared_ptr หรือไม่

std::functionจัดเก็บสำเนาของเนื้อหา เช่นเดียวกับไลบรารีมาตรฐาน C ++ ทุกประเภทfunctionใช้ความหมายของค่าความหมายค่าดังนั้นจึงสามารถคัดลอกได้ เมื่อมีการคัดลอกfunctionวัตถุใหม่จะแยกจากกันโดยสิ้นเชิง นอกจากนี้ยังสามารถเคลื่อนย้ายได้ดังนั้นการจัดสรรภายในใด ๆ จึงสามารถถ่ายโอนได้อย่างเหมาะสมโดยไม่จำเป็นต้องจัดสรรและคัดลอกเพิ่มเติม

ดังนั้นจึงไม่จำเป็นต้องมีการนับอ้างอิง

ทุกอย่างที่คุณระบุนั้นถูกต้องโดยสมมติว่า "การจัดสรรหน่วยความจำ" เท่ากับ "ไม่ดีที่จะใช้ในโค้ดแบบเรียลไทม์"


1
คำอธิบายที่ยอดเยี่ยมขอบคุณ ดังนั้นการสร้างstd::functionคือจุดที่จัดสรรและคัดลอกหน่วยความจำ ดูเหมือนว่าจะเป็นไปตามนั้นไม่มีทางที่จะคืนการปิด (เนื่องจากมีการจัดสรรบนสแต็ก) โดยไม่ต้องคัดลอกลงใน a std::functionก่อนใช่
Steve

3
@ สตีฟ: ครับ; คุณต้องห่อแลมด้าในภาชนะบางชนิดเพื่อให้มันออกจากขอบเขต
Nicol Bolas

รหัสของฟังก์ชันทั้งหมดถูกคัดลอกหรือเป็นฟังก์ชันดั้งเดิมที่จัดสรรเวลาคอมไพล์และส่งผ่านค่าปิดทับหรือไม่
Llamageddon

ฉันต้องการเพิ่มว่ามาตรฐานมีคำสั่งทางอ้อมมากหรือน้อย (§ 20.8.11.2.1 [func.wrap.func.con] ¶ 5) ว่าถ้าแลมบ์ดาไม่จับอะไรก็สามารถเก็บไว้ในstd::functionวัตถุที่ไม่มีหน่วยความจำแบบไดนามิก การจัดสรรเกิดขึ้น
5gon12eder

2
@ ยัก: คุณกำหนด "ใหญ่" ได้อย่างไร? วัตถุที่มีพอยน์เตอร์สองสถานะ "ใหญ่" หรือไม่? แล้ว 3 หรือ 4 ล่ะ? นอกจากนี้ขนาดวัตถุไม่ใช่ปัญหาเดียว หากวัตถุไม่สามารถเคลื่อนย้ายได้ต้องเก็บไว้ในการจัดสรรเนื่องจากfunctionมีตัวสร้างการย้ายที่ไม่มีข้อยกเว้น ประเด็นทั้งหมดของการพูดว่า "ต้องการโดยทั่วไป" คือฉันไม่ได้พูดว่า " ต้องใช้เสมอ ": มีสถานการณ์ที่จะไม่มีการจัดสรร
Nicol Bolas

1

C ++ แลมบ์ดาเป็นเพียงน้ำตาลวากยสัมพันธ์รอบ ๆ (ไม่ระบุชื่อ) คลาส Functor ที่มีโอเวอร์โหลดoperator()และstd::functionเป็นเพียงกระดาษห่อหุ้มรอบ callables (เช่น functors, lambdas, c-functions, ... ) ซึ่งคัดลอกโดยให้ค่า "solid lambda object" จากปัจจุบัน ขอบเขตสแต็ก - ไปยังฮีปกอง

ในการทดสอบจำนวนตัวสร้าง / การย้ายถิ่นฐานจริงฉันได้ทำการทดสอบ (โดยใช้ระดับอื่นของการตัดเป็น shared_ptr แต่ไม่เป็นเช่นนั้น) ดูตัวคุณเอง:

#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)();
}

มันทำให้ผลลัพธ์นี้:

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

ctors / dtors ชุดเดียวกันจะถูกเรียกใช้สำหรับวัตถุแลมด้าที่จัดสรรสแต็ก! (ตอนนี้เรียก Ctor สำหรับการจัดสรรสแต็ก Copy-ctor (+ การจัดสรรฮีป) เพื่อสร้างใน std :: function และอีกอันหนึ่งสำหรับสร้างการจัดสรรฮีป shared_ptr + การสร้างฟังก์ชัน)

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.