Tại sao lambdas có thể được tối ưu hóa tốt hơn bởi trình biên dịch so với các hàm đơn giản?


171

Trong cuốn sách của mình, The C++ Standard Library (Second Edition)Nicolai Josuttis nói rằng lambdas có thể được trình biên dịch tối ưu hóa tốt hơn so với các hàm đơn giản.

Ngoài ra, trình biên dịch C ++ tối ưu hóa lambdas tốt hơn so với các chức năng thông thường. (Trang 213)

Tại sao vậy?

Tôi nghĩ rằng khi nói đến nội tuyến thì không nên có bất kỳ sự khác biệt nào nữa. Lý do duy nhất tôi có thể nghĩ đến là các trình biên dịch có thể có bối cảnh cục bộ tốt hơn với lambdas và như vậy có thể đưa ra nhiều giả định hơn và thực hiện nhiều tối ưu hóa hơn.



Về cơ bản, câu lệnh áp dụng cho tất cả các đối tượng hàm , không chỉ lambdas.
newacct

4
Điều đó sẽ không chính xác bởi vì con trỏ hàm cũng là đối tượng hàm.
Julian Schaub - litb

2
@litb: Tôi nghĩ rằng tôi không đồng ý với điều đó. ^ W ^ W ^ W ^ W ^ W ^ W (sau khi xem xét tiêu chuẩn) Tôi không biết về C ++ đó, ​​mặc dù tôi nghĩ theo cách nói chung (và theo wikipedia), mọi người có nghĩa là thể hiện của một số người có thể gọi được khi họ nói đối tượng hàm.
Sebastian Mach

1
Một số trình biên dịch có thể tối ưu hóa lambdas tốt hơn các hàm đơn giản, nhưng không phải tất cả :-(
Cody Grey

Câu trả lời:


175

Lý do là lambdas là các đối tượng chức năng nên việc chuyển chúng vào một mẫu hàm sẽ khởi tạo một chức năng mới dành riêng cho đối tượng đó. Do đó, trình biên dịch có thể nội tuyến một cách tầm thường cuộc gọi lambda.

Mặt khác, đối với các hàm, cảnh báo cũ được áp dụng: một con trỏ hàm được chuyển đến mẫu hàm và các trình biên dịch theo truyền thống có rất nhiều vấn đề khi thực hiện các cuộc gọi thông qua các con trỏ hàm. Về mặt lý thuyết chúng có thể được nội tuyến, nhưng chỉ khi chức năng xung quanh cũng được nội tuyến.

Ví dụ, xem xét mẫu hàm sau:

template <typename Iter, typename F>
void map(Iter begin, Iter end, F f) {
    for (; begin != end; ++begin)
        *begin = f(*begin);
}

Gọi nó bằng lambda như thế này:

int a[] = { 1, 2, 3, 4 };
map(begin(a), end(a), [](int n) { return n * 2; });

Kết quả trong phần khởi tạo này (được tạo bởi trình biên dịch):

template <>
void map<int*, _some_lambda_type>(int* begin, int* end, _some_lambda_type f) {
    for (; begin != end; ++begin)
        *begin = f.operator()(*begin);
}

Trình biên dịch biết _some_lambda_type::operator ()và có thể gọi nội tuyến một cách tầm thường. (Và việc gọi hàm mapvới bất kỳ lambda nào khác sẽ tạo ra một khởi tạo mới mapvì mỗi lambda có một loại riêng biệt.)

Nhưng khi được gọi bằng một con trỏ hàm, phần khởi tạo trông như sau:

template <>
void map<int*, int (*)(int)>(int* begin, int* end, int (*f)(int)) {
    for (; begin != end; ++begin)
        *begin = f(*begin);
}

Càng và ở đây fchỉ đến một địa chỉ khác nhau cho mỗi cuộc gọi mapvà do đó trình biên dịch không thể gọi nội tuyến ftrừ khi cuộc gọi xung quanh mapcũng đã được nội tuyến để trình biên dịch có thể phân giải fthành một chức năng cụ thể.


4
Có lẽ điều đáng nói là việc khởi tạo cùng một mẫu hàm với biểu thức lambda khác nhau sẽ tạo ra một hàm hoàn toàn mới với một kiểu duy nhất, đây có thể là một nhược điểm.
lạnh

2
@greggo Hoàn toàn đúng. Vấn đề là khi xử lý các chức năng không thể được nội tuyến (vì chúng quá lớn). Ở đây, cuộc gọi đến cuộc gọi lại vẫn có thể được nội tuyến trong trường hợp lambda, nhưng không phải trong trường hợp con trỏ hàm. std::sortlà ví dụ cổ điển về điều này bằng cách sử dụng lambdas thay vì con trỏ hàm ở đây mang lại hiệu suất gấp bảy lần (có thể nhiều hơn, nhưng tôi không có dữ liệu về điều đó!) tăng hiệu suất.
Konrad Rudolph

1
@greggo Bạn đang nhầm lẫn hai chức năng ở đây: chức năng chúng ta đang chuyển lambda đến (ví dụ std::sort, hoặc maptrong ví dụ của tôi) và chính lambda. Lambda thường nhỏ. Các chức năng khác - không nhất thiết phải. Chúng tôi quan tâm đến các cuộc gọi nội tuyến đến lambda bên trong chức năng khác.
Konrad Rudolph

2
@greggo Tôi biết. Đây đúng là những gì câu cuối cùng trong câu trả lời của tôi nói.
Konrad Rudolph

1
Điều tôi cảm thấy tò mò (vừa vấp phải nó) là đã đưa ra một hàm boolean đơn giản predcó định nghĩa hiển thị và sử dụng gcc v5.3, std::find_if(b, e, pred)không nội tuyến pred, nhưng std::find_if(b, e, [](int x){return pred(x);})không. Clang quản lý để nội tuyến cả hai, nhưng không tạo ra mã nhanh như g ++ với lambda.
rici

26

Bởi vì khi bạn chuyển một "hàm" cho một thuật toán, thực tế bạn đang chuyển một con trỏ tới hàm để nó phải thực hiện một cuộc gọi gián tiếp thông qua con trỏ đến hàm. Khi bạn sử dụng lambda, bạn đang chuyển một đối tượng đến một thể hiện mẫu được khởi tạo đặc biệt cho loại đó và cuộc gọi đến hàm lambda là một cuộc gọi trực tiếp, không phải là một cuộc gọi thông qua một con trỏ hàm nên rất có thể được nội tuyến.


5
"Cuộc gọi đến chức năng lambda là cuộc gọi trực tiếp" - thực sự. Và điều tương tự cũng đúng với tất cả các đối tượng chức năng, không chỉ lambdas. Đó chỉ là con trỏ chức năng không thể được nội tuyến dễ dàng, nếu có.
Pete Becker
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.