Mô hình bộ nhớ và triển khai lambda C ++ 11


92

Tôi muốn một số thông tin về cách nghĩ đúng về các bao đóng C ++ 11 và std::functionvề cách chúng được triển khai và cách xử lý bộ nhớ.

Mặc dù tôi không tin vào việc tối ưu hóa quá sớm, nhưng tôi có thói quen xem xét cẩn thận tác động hiệu suất của các lựa chọn của mình trong khi viết mã mới. Tôi cũng thực hiện một lượng lớn lập trình thời gian thực, ví dụ như trên bộ vi điều khiển và hệ thống âm thanh, nơi cần tránh các tạm dừng phân bổ / phân bổ bộ nhớ không xác định.

Do đó, tôi muốn hiểu rõ hơn về thời điểm sử dụng hoặc không sử dụng lambdas C ++.

Sự hiểu biết hiện tại của tôi là một lambda không có bao đóng bị bắt giống hệt như một lệnh gọi lại C. Tuy nhiên, khi môi trường được nắm bắt bằng giá trị hoặc bằng tham chiếu, một đối tượng ẩn danh sẽ được tạo trên ngăn xếp. Khi một hàm đóng giá trị phải được trả về từ một hàm, thì một hàm sẽ bao bọc nó std::function. Điều gì xảy ra với bộ nhớ đóng trong trường hợp này? Nó có được sao chép từ ngăn xếp vào đống không? Nó có được giải phóng bất cứ khi nào std::functionđược giải phóng, tức là, nó có được tính tham chiếu như a std::shared_ptrkhông?

Tôi tưởng tượng rằng trong một hệ thống thời gian thực, tôi có thể thiết lập một chuỗi các hàm lambda, chuyển B làm đối số tiếp tục cho A, để một đường dẫn xử lý A->Bđược tạo. Trong trường hợp này, các đóng A và B sẽ được phân bổ một lần. Mặc dù tôi không chắc liệu những thứ này sẽ được phân bổ trên ngăn xếp hay đống. Tuy nhiên nói chung điều này có vẻ an toàn khi sử dụng trong hệ thống thời gian thực. Mặt khác, nếu B xây dựng một số hàm lambda C, hàm này trả về, thì bộ nhớ cho C sẽ được cấp phát và phân bổ nhiều lần, điều này sẽ không được chấp nhận cho việc sử dụng thời gian thực.

Trong mã giả, một vòng lặp DSP, mà tôi nghĩ là sẽ an toàn trong thời gian thực. Tôi muốn thực hiện xử lý khối A và sau đó là B, nơi A gọi đối số của nó. Cả hai hàm này đều trả về std::functioncác đối tượng, vì vậy fsẽ là một std::functionđối tượng, nơi môi trường của nó được lưu trữ trên 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)
}

Và một cái mà tôi nghĩ có thể không tốt khi sử dụng trong mã thời gian thực:

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

Và một nơi mà tôi nghĩ rằng bộ nhớ ngăn xếp có khả năng được sử dụng để đóng:

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

Trong trường hợp sau, bao đóng được xây dựng ở mỗi lần lặp lại của vòng lặp, nhưng không giống như ví dụ trước, nó rẻ vì nó giống như một lệnh gọi hàm, không có phân bổ heap nào được thực hiện. Hơn nữa, tôi tự hỏi liệu một trình biên dịch có thể "dỡ bỏ" việc đóng và thực hiện tối ưu hóa nội tuyến hay không.

Điều này có chính xác? Cảm ơn bạn.


4
Không có chi phí khi sử dụng biểu thức lambda. Lựa chọn khác sẽ là tự viết một đối tượng hàm như vậy, điều này sẽ giống hệt như vậy. Btw, đối với câu hỏi nội tuyến, vì trình biên dịch có tất cả thông tin mà nó cần, nó chắc chắn có thể chỉ nội tuyến cuộc gọi tới operator(). Không có "nâng" được thực hiện, lambdas không có gì đặc biệt. Chúng chỉ là một tay ngắn cho một đối tượng chức năng cục bộ.
Xeo

Đây dường như là một câu hỏi về việc liệu có std::functionlưu trữ trạng thái của nó trên heap hay không và không liên quan gì đến lambdas. Có đúng không?
Mooing Duck,

8
Chỉ để đánh vần nó trong trường hợp có bất kỳ hiểu lầm nào: Một biểu thức lambda không phải là một std::function!!
Xeo

1
Chỉ là một nhận xét bên lề: hãy cẩn thận khi trả về lambda từ một hàm, vì bất kỳ biến cục bộ nào được tham chiếu bắt giữ đều trở nên không hợp lệ sau khi rời khỏi hàm đã tạo lambda.
Giorgio

2
@Steve kể từ C ++ 14, bạn có thể trả về lambda từ một hàm có autokiểu trả về.
Oktalist

Câu trả lời:


100

Sự hiểu biết hiện tại của tôi là một lambda không có bao đóng bị bắt giống hệt như một lệnh gọi lại C. Tuy nhiên, khi môi trường được nắm bắt bằng giá trị hoặc bằng tham chiếu, một đối tượng ẩn danh sẽ được tạo trên ngăn xếp.

Không; nó là luôn là một đối tượng C ++ với kiểu không xác định, được tạo trên ngăn xếp. Một lambda không chiếm được có thể được chuyển đổi thành một con trỏ hàm (mặc dù nó có phù hợp với quy ước gọi C hay không là tùy thuộc vào việc triển khai), nhưng điều đó không có nghĩa là nó một con trỏ hàm.

Khi một hàm đóng giá trị phải được trả về từ một hàm, người ta sẽ gói nó trong hàm std ::. Điều gì xảy ra với bộ nhớ đóng trong trường hợp này?

Một lambda không phải là bất cứ điều gì đặc biệt trong C ++ 11. Nó là một đối tượng giống như bất kỳ đối tượng nào khác. Biểu thức lambda dẫn đến kết quả tạm thời, có thể được sử dụng để khởi tạo một biến trên ngăn xếp:

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

lamblà một đối tượng ngăn xếp. Nó có một hàm tạo và hàm hủy. Và nó sẽ tuân theo tất cả các quy tắc C ++ cho điều đó. Loại lambý chí chứa các giá trị / tham chiếu được nắm bắt; chúng sẽ là thành viên của đối tượng đó, giống như bất kỳ thành viên đối tượng nào khác của bất kỳ loại nào khác.

Bạn có thể đưa nó cho một std::function:

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

Trong trường hợp này, nó sẽ nhận được một bản sao của giá trị lamb. Nếu lambđã nắm bắt bất cứ thứ gì theo giá trị, sẽ có hai bản sao của những giá trị đó; một tronglamb và một trong func_lamb.

Khi phạm vi hiện tại kết thúc, func_lambsẽ bị hủy, tiếp theo là theo lambquy tắc dọn dẹp các biến ngăn xếp.

Bạn có thể dễ dàng phân bổ một cái trên heap:

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

Vị trí chính xác của bộ nhớ cho nội dung của std::functionchuyển đi phụ thuộc vào việc triển khai, nhưng kiểu xóa được sử dụng bởistd::function thường yêu cầu ít nhất một cấp phát bộ nhớ. Đây là lý do tại sao hàm tạo std::functioncủa có thể lấy một bộ cấp phát.

Nó có được giải phóng bất cứ khi nào hàm std :: được giải phóng, tức là nó có được tính tham chiếu giống như std :: shared_ptr không?

std::functionlưu trữ một bản sao nội dung của nó. Giống như hầu hết mọi loại thư viện tiêu chuẩn C ++, functionsử dụng ngữ nghĩa giá trị . Vì vậy, nó có thể sao chép được; khi nó được sao chép, functionđối tượng mới hoàn toàn tách biệt. Nó cũng có thể di chuyển, vì vậy bất kỳ phân bổ nội bộ nào cũng có thể được chuyển một cách thích hợp mà không cần phải phân bổ và sao chép nhiều hơn.

Do đó không cần phải đếm tham chiếu.

Mọi thứ khác mà bạn nêu đều đúng, giả sử rằng "cấp phát bộ nhớ" tương đương với "không hợp lệ để sử dụng trong mã thời gian thực".


1
Lời giải thích tuyệt vời, cảm ơn bạn. Vì vậy, việc tạo ra std::functionlà điểm tại đó bộ nhớ được cấp phát và sao chép. Có vẻ như theo sau rằng không có cách nào để trả về một bao đóng (vì chúng được phân bổ trên ngăn xếp), mà không cần sao chép đầu tiên vào a std::function, vâng?
Steve,

3
@Steve: Có; bạn phải bọc lambda trong một số loại thùng chứa để nó thoát khỏi phạm vi.
Nicol Bolas,

Toàn bộ mã của hàm được sao chép hay hàm gốc được phân bổ thời gian biên dịch và chuyển các giá trị đóng?
Llamageddon

Tôi muốn nói thêm rằng tiêu chuẩn ít nhiều bắt buộc gián tiếp (§ 20.8.11.2.1 [func.wrap.func.con] ¶ 5) rằng nếu lambda không nắm bắt bất cứ thứ gì, nó có thể được lưu trữ trong một std::functionđối tượng không có bộ nhớ động phân bổ đang diễn ra.
5gon12eder

2
@Yakk: Làm thế nào để bạn định nghĩa "lớn"? Một đối tượng có hai con trỏ trạng thái là "lớn"? Làm thế nào về 3 hoặc 4? Ngoài ra, kích thước đối tượng không phải là vấn đề duy nhất; nếu đối tượng không phải là không thể di chuyển, nó phải được lưu trữ trong một cấp phát, vì functioncó một hàm tạo di chuyển không chấp nhận. Toàn bộ quan điểm của việc nói "yêu cầu chung" là tôi không nói " luôn luôn yêu cầu": rằng có những trường hợp mà việc phân bổ sẽ không được thực hiện.
Nicol Bolas

0

C ++ lambda chỉ là một đường cú pháp xung quanh lớp Functor (ẩn danh) với quá tải operator()std::functionchỉ là một trình bao bọc xung quanh các tệp được gọi (tức là chức năng, lambdas, c-functions, ...) sao chép theo giá trị "đối tượng lambda rắn" từ hiện tại phạm vi ngăn xếp - lên đống .

Để kiểm tra số lượng các hàm tạo / định vị lại thực tế, tôi đã thực hiện một bài kiểm tra (sử dụng một mức gói khác cho shared_ptr nhưng không phải như vậy). Hãy tự mình xem:

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

nó tạo ra kết quả này:

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

Tập hợp chính xác các ctors / dtors sẽ được gọi cho đối tượng lambda được phân bổ ngăn xếp! (Bây giờ nó gọi Ctor để phân bổ ngăn xếp, Copy-ctor (+ heap CẤP) để xây dựng nó trong std :: function và một cái khác để thực hiện phân bổ heap shared_ptr + xây dựng hàm)

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.