Câu trả lời này nhằm đóng góp vào tập hợp các câu trả lời hiện có, điều mà tôi tin là một điểm chuẩn có ý nghĩa hơn cho chi phí thời gian chạy của các lệnh gọi hàm std ::.
Cơ chế chức năng std :: nên được công nhận cho những gì nó cung cấp: Bất kỳ thực thể có thể gọi nào cũng có thể được chuyển đổi thành chức năng std :: có chữ ký thích hợp. Giả sử bạn có một thư viện phù hợp với một bề mặt cho một hàm được xác định bởi z = f (x, y), bạn có thể viết nó để chấp nhận a std::function<double(double,double)>
và người dùng của thư viện có thể dễ dàng chuyển đổi bất kỳ thực thể có thể gọi nào sang đó; có thể là một hàm thông thường, một phương thức của một thể hiện của lớp hoặc lambda hoặc bất cứ thứ gì được hỗ trợ bởi std :: bind.
Không giống như cách tiếp cận mẫu, cách này hoạt động mà không phải biên dịch lại hàm thư viện cho các trường hợp khác nhau; theo đó, ít mã biên dịch thêm là cần thiết cho từng trường hợp bổ sung. Luôn luôn có thể làm điều này xảy ra, nhưng nó thường yêu cầu một số cơ chế khó xử và người dùng thư viện có thể sẽ cần phải xây dựng một bộ chuyển đổi xung quanh chức năng của họ để làm cho nó hoạt động. std :: function tự động xây dựng bất kỳ bộ điều hợp nào cần thiết để có được giao diện cuộc gọi thời gian chạy chung cho tất cả các trường hợp, đây là một tính năng mới và rất mạnh mẽ.
Theo quan điểm của tôi, đây là trường hợp sử dụng quan trọng nhất đối với chức năng std :: liên quan đến hiệu suất: Tôi quan tâm đến chi phí gọi hàm std :: nhiều lần sau khi nó được xây dựng một lần và nó cần phải là một tình huống trong đó trình biên dịch không thể tối ưu hóa cuộc gọi bằng cách biết hàm thực sự được gọi (tức là bạn cần ẩn việc thực hiện trong tệp nguồn khác để có điểm chuẩn phù hợp).
Tôi đã thực hiện bài kiểm tra dưới đây, tương tự như của OP; nhưng những thay đổi chính là:
- Mỗi trường hợp lặp 1 tỷ lần, nhưng các đối tượng hàm std :: chỉ được xây dựng một lần. Tôi đã tìm thấy bằng cách xem mã đầu ra mà 'toán tử mới' được gọi khi xây dựng các lệnh gọi hàm std :: thực tế (có thể không phải khi chúng được tối ưu hóa).
- Kiểm tra được chia thành hai tệp để ngăn chặn tối ưu hóa không mong muốn
- Các trường hợp của tôi là: (a) hàm được nội tuyến (b) được truyền bởi một hàm con trỏ hàm thông thường (c) là một hàm tương thích được bao bọc bởi hàm std :: function (d) là một hàm không tương thích được thực hiện tương thích với std :: liên kết, bọc như hàm std ::
Kết quả tôi nhận được là:
trường hợp (a) (nội tuyến) 1,3 nsec
tất cả các trường hợp khác: 3,3 nsec.
Trường hợp (d) có xu hướng chậm hơn một chút, nhưng sự khác biệt (khoảng 0,05 nsec) được hấp thụ trong tiếng ồn.
Kết luận là hàm std :: có thể so sánh được (tại thời điểm cuộc gọi) với việc sử dụng một con trỏ hàm, ngay cả khi có sự thích ứng 'liên kết' đơn giản với hàm thực tế. Nội tuyến nhanh hơn 2 ns so với các nội dung khác nhưng đó là một sự đánh đổi dự kiến vì nội tuyến là trường hợp duy nhất "cứng cáp" trong thời gian chạy.
Khi tôi chạy mã của johan-lundberg trên cùng một máy, tôi thấy khoảng 39 nsec mỗi vòng lặp, nhưng có nhiều hơn trong vòng lặp ở đó, bao gồm cả hàm tạo và hàm hủy thực tế của hàm std ::, có lẽ khá cao vì nó liên quan đến một cái mới và xóa.
-O2 gcc 4.8.1, đến x86_64 mục tiêu (lõi i5).
Lưu ý, mã được chia thành hai tệp, để ngăn trình biên dịch mở rộng các hàm nơi chúng được gọi (ngoại trừ trong trường hợp nó dự định).
----- tập tin nguồn đầu tiên --------------
#include <functional>
// simple funct
float func_half( float x ) { return x * 0.5; }
// func we can bind
float mul_by( float x, float scale ) { return x * scale; }
//
// func to call another func a zillion times.
//
float test_stdfunc( std::function<float(float)> const & func, int nloops ) {
float x = 1.0;
float y = 0.0;
for(int i =0; i < nloops; i++ ){
y += x;
x = func(x);
}
return y;
}
// same thing with a function pointer
float test_funcptr( float (*func)(float), int nloops ) {
float x = 1.0;
float y = 0.0;
for(int i =0; i < nloops; i++ ){
y += x;
x = func(x);
}
return y;
}
// same thing with inline function
float test_inline( int nloops ) {
float x = 1.0;
float y = 0.0;
for(int i =0; i < nloops; i++ ){
y += x;
x = func_half(x);
}
return y;
}
----- tập tin nguồn thứ hai -------------
#include <iostream>
#include <functional>
#include <chrono>
extern float func_half( float x );
extern float mul_by( float x, float scale );
extern float test_inline( int nloops );
extern float test_stdfunc( std::function<float(float)> const & func, int nloops );
extern float test_funcptr( float (*func)(float), int nloops );
int main() {
using namespace std::chrono;
for(int icase = 0; icase < 4; icase ++ ){
const auto tp1 = system_clock::now();
float result;
switch( icase ){
case 0:
result = test_inline( 1e9);
break;
case 1:
result = test_funcptr( func_half, 1e9);
break;
case 2:
result = test_stdfunc( func_half, 1e9);
break;
case 3:
result = test_stdfunc( std::bind( mul_by, std::placeholders::_1, 0.5), 1e9);
break;
}
const auto tp2 = high_resolution_clock::now();
const auto d = duration_cast<milliseconds>(tp2 - tp1);
std::cout << d.count() << std::endl;
std::cout << result<< std::endl;
}
return 0;
}
Đối với những người quan tâm, đây là bộ điều hợp trình biên dịch được xây dựng để làm cho 'mul_by' trông giống như một float (float) - đây là 'được gọi' khi hàm được tạo dưới dạng bind (mul_by, _1,0.5) được gọi:
movq (%rdi), %rax ; get the std::func data
movsd 8(%rax), %xmm1 ; get the bound value (0.5)
movq (%rax), %rdx ; get the function to call (mul_by)
cvtpd2ps %xmm1, %xmm1 ; convert 0.5 to 0.5f
jmp *%rdx ; jump to the func
(vì vậy có thể đã nhanh hơn một chút nếu tôi viết 0,5f trong liên kết ...) Lưu ý rằng tham số 'x' đến% xmm0 và chỉ ở đó.
Đây là mã trong khu vực nơi chức năng được xây dựng, trước khi gọi test_stdfunc - chạy qua bộ lọc c ++:
movl $16, %edi
movq $0, 32(%rsp)
call operator new(unsigned long) ; get 16 bytes for std::function
movsd .LC0(%rip), %xmm1 ; get 0.5
leaq 16(%rsp), %rdi ; (1st parm to test_stdfunc)
movq mul_by(float, float), (%rax) ; store &mul_by in std::function
movl $1000000000, %esi ; (2nd parm to test_stdfunc)
movsd %xmm1, 8(%rax) ; store 0.5 in std::function
movq %rax, 16(%rsp) ; save ptr to allocated mem
;; the next two ops store pointers to generated code related to the std::function.
;; the first one points to the adaptor I showed above.
movq std::_Function_handler<float (float), std::_Bind<float (*(std::_Placeholder<1>, double))(float, float)> >::_M_invoke(std::_Any_data const&, float), 40(%rsp)
movq std::_Function_base::_Base_manager<std::_Bind<float (*(std::_Placeholder<1>, double))(float, float)> >::_M_manager(std::_Any_data&, std::_Any_data const&, std::_Manager_operation), 32(%rsp)
call test_stdfunc(std::function<float (float)> const&, int)
std::function
nếu và chỉ khi bạn thực sự cần một bộ sưu tập các đối tượng có thể gọi không đồng nhất (nghĩa là không có thêm thông tin phân biệt đối xử nào trong thời gian chạy).