jbg*_*bgs 13 c++ lambda templates functor c++11
为了便于说明,假设我想实现一个通用的整数比较函数.我可以想到一些定义/调用函数的方法.
(A)功能模板+仿函数
template <class Compare> void compare_int (int a, int b, const std::string& msg, Compare cmp_func)
{
if (cmp_func(a, b)) std::cout << "a is " << msg << " b" << std::endl;
else std::cout << "a is not " << msg << " b" << std::endl;
}
struct MyFunctor_LT {
bool operator() (int a, int b) {
return a<b;
}
};
Run Code Online (Sandbox Code Playgroud)
这将是对此函数的几个调用:
MyFunctor_LT mflt;
MyFunctor_GT mfgt; //not necessary to show the implementation
compare_int (3, 5, "less than", mflt);
compare_int (3, 5, "greater than", mflt);
Run Code Online (Sandbox Code Playgroud)
(B)功能模板+ lambdas
我们会这样称呼compare_int
:
compare_int (3, 5, "less than", [](int a, int b) {return a<b;});
compare_int (3, 5, "greater than", [](int a, int b) {return a>b;});
Run Code Online (Sandbox Code Playgroud)
(C)函数模板+ std :: function
相同的模板实现,调用:
std::function<bool(int,int)> func_lt = [](int a, int b) {return a<b;}; //or a functor/function
std::function<bool(int,int)> func_gt = [](int a, int b) {return a>b;};
compare_int (3, 5, "less than", func_lt);
compare_int (3, 5, "greater than", func_gt);
Run Code Online (Sandbox Code Playgroud)
(D)原始的"C风格"指针
执行:
void compare_int (int a, int b, const std::string& msg, bool (*cmp_func) (int a, int b))
{
...
}
bool lt_func (int a, int b)
{
return a<b;
}
Run Code Online (Sandbox Code Playgroud)
调用:
compare_int (10, 5, "less than", lt_func);
compare_int (10, 5, "greater than", gt_func);
Run Code Online (Sandbox Code Playgroud)
根据这些方案,我们在每种情况下:
(A)将在内存中编译和分配两个模板实例(两个不同的参数).
(B)我想说还会编译两个模板实例.每个lambda都是一个不同的类.如果我错了,请纠正我.
(C)只编译一个模板实例,因为模板参数总是相同:std::function<bool(int,int)>
.
(D)显然我们只有一个实例.
不用说,这对一个天真的例子没有任何影响.但是当处理数十个(或数百个)模板和许多仿函数时,编译时间和内存使用差异可能很大.
我们可以说在许多情况下(即,当使用具有相同签名的太多仿函数)std::function
(或甚至函数指针)时,必须优先于模板+原始仿函数/ lambdas吗?包裹你的std::function
算子或lambda 可能非常方便.
我知道std::function
(函数指针也是)引入了开销.这值得么?
编辑.我使用以下宏和一个非常常见的标准库函数模板(std :: sort)做了一个非常简单的基准测试:
#define TEST(X) std::function<bool(int,int)> f##X = [] (int a, int b) {return (a^X)<(b+X);}; \
std::sort (v.begin(), v.end(), f##X);
#define TEST2(X) auto f##X = [] (int a, int b) {return (a^X)<(b^X);}; \
std::sort (v.begin(), v.end(), f##X);
#define TEST3(X) bool(*f##X)(int, int) = [] (int a, int b) {return (a^X)<(b^X);}; \
std::sort (v.begin(), v.end(), f##X);
Run Code Online (Sandbox Code Playgroud)
结果如下关于生成的二进制文件的大小(GCC at -O3):
即使我显示了这些数字,它也是一个定性基准而不是定量基准.正如我们所期望的那样,基于std::function
参数或函数指针的函数模板更好地(在大小方面)缩放,因为没有创建太多实例.我没有测量运行时内存使用情况.
至于性能结果(矢量大小是1000000的元素):
这是一个值得注意的差异,我们不能忽视所引入的开销std::function
(至少如果我们的算法包含数百万次迭代).
正如其他人已经指出的那样,lambda和函数对象很可能被内联,特别是如果函数的主体不太长.因此,它们在速度和内存使用方面可能比方法更好std::function
.如果可以内联函数,编译器可以更积极地优化代码.令人震惊的更好. std::function
除了其他方面,这将是我最后的手段.
但是当处理数十个(或数百个)模板和许多仿函数时,编译时间和内存使用差异可能很大.
至于编译时间,只要你使用如图所示的简单模板,我就不会太担心它.(如果你正在进行模板元编程,是的,那么你可以开始担心.)
现在,内存使用情况:编译时编译器还是运行时生成的可执行文件?对于前者,与编译时相同.对于后者:内联lamdas和函数对象是赢家.
我们可以说在许多情况下
std::function
(甚至函数指针)必须优先于模板+原始函子/ lambdas吗?即包裹你的仿函数或lambdastd::function
可能非常方便.
我不太清楚如何回答这个问题.我无法定义"很多情况".
但是,有一点我可以肯定地说,类型擦除是一种避免/减少由模板引起的代码膨胀的方法,参见条款44:在Effective C++ 中模板中的与参数无关的代码.顺便说一下,在std::function
内部使用类型擦除.所以,是的,代码臃肿是一个问题.
我知道std :: function(函数指针也是)引入了开销.这值得么?
"想要速度?测量." (Howard Hinnant)
还有一件事:通过函数指针调用函数可以内联(甚至跨编译单元!).这是一个证据:
#include <cstdio>
bool lt_func(int a, int b)
{
return a<b;
}
void compare_int(int a, int b, const char* msg, bool (*cmp_func) (int a, int b)) {
if (cmp_func(a, b)) printf("a is %s b\n", msg);
else printf("a is not %s b\n", msg);
}
void f() {
compare_int (10, 5, "less than", lt_func);
}
Run Code Online (Sandbox Code Playgroud)
这是您的代码的略微修改版本.我删除了所有的iostream东西,因为它会使生成的程序集混乱.这是组装f()
:
.LC1:
.string "a is not %s b\n"
[...]
.LC2:
.string "less than"
[...]
f():
.LFB33:
.cfi_startproc
movl $.LC2, %edx
movl $.LC1, %esi
movl $1, %edi
xorl %eax, %eax
jmp __printf_chk
.cfi_endproc
Run Code Online (Sandbox Code Playgroud)
这意味着,GCC 4.7.2内联lt_func
的-O3
.实际上,生成的汇编代码是最佳的.
我还检查过:我将实现移动lt_func
到一个单独的源文件中并启用了链接时优化(-flto
).GCC仍然乐意通过函数指针内联调用!这是非常重要的,你需要一个高质量的编译器来做到这一点.
只是为了记录,你可以真正感受到这种std::function
方法的开销:
这段代码:
#include <cstdio>
#include <functional>
template <class Compare> void compare_int(int a, int b, const char* msg, Compare cmp_func)
{
if (cmp_func(a, b)) printf("a is %s b\n", msg);
else printf("a is not %s b\n", msg);
}
void f() {
std::function<bool(int,int)> func_lt = [](int a, int b) {return a<b;};
compare_int (10, 5, "less than", func_lt);
}
Run Code Online (Sandbox Code Playgroud)
产生这个组件-O3
(大约140行):
f():
.LFB498:
.cfi_startproc
.cfi_personality 0x3,__gxx_personality_v0
.cfi_lsda 0x3,.LLSDA498
pushq %rbx
.cfi_def_cfa_offset 16
.cfi_offset 3, -16
movl $1, %edi
subq $80, %rsp
.cfi_def_cfa_offset 96
movq %fs:40, %rax
movq %rax, 72(%rsp)
xorl %eax, %eax
movq std::_Function_handler<bool (int, int), f()::{lambda(int, int)#1}>::_M_invoke(std::_Any_data const&, int, int), 24(%rsp)
movq std::_Function_base::_Base_manager<f()::{lambda(int, int)#1}>::_M_manager(std::_Any_data&, std::_Function_base::_Base_manager<f()::{lambda(int, int)#1}> const&, std::_Manager_operation), 16(%rsp)
.LEHB0:
call operator new(unsigned long)
.LEHE0:
movq %rax, (%rsp)
movq 16(%rsp), %rax
movq $0, 48(%rsp)
testq %rax, %rax
je .L14
movq 24(%rsp), %rdx
movq %rax, 48(%rsp)
movq %rsp, %rsi
leaq 32(%rsp), %rdi
movq %rdx, 56(%rsp)
movl $2, %edx
.LEHB1:
call *%rax
.LEHE1:
cmpq $0, 48(%rsp)
je .L14
movl $5, %edx
movl $10, %esi
leaq 32(%rsp), %rdi
.LEHB2:
call *56(%rsp)
testb %al, %al
movl $.LC0, %edx
jne .L49
movl $.LC2, %esi
movl $1, %edi
xorl %eax, %eax
call __printf_chk
.LEHE2:
.L24:
movq 48(%rsp), %rax
testq %rax, %rax
je .L23
leaq 32(%rsp), %rsi
movl $3, %edx
movq %rsi, %rdi
.LEHB3:
call *%rax
.LEHE3:
.L23:
movq 16(%rsp), %rax
testq %rax, %rax
je .L12
movl $3, %edx
movq %rsp, %rsi
movq %rsp, %rdi
.LEHB4:
call *%rax
.LEHE4:
.L12:
movq 72(%rsp), %rax
xorq %fs:40, %rax
jne .L50
addq $80, %rsp
.cfi_remember_state
.cfi_def_cfa_offset 16
popq %rbx
.cfi_def_cfa_offset 8
ret
.p2align 4,,10
.p2align 3
.L49:
.cfi_restore_state
movl $.LC1, %esi
movl $1, %edi
xorl %eax, %eax
.LEHB5:
call __printf_chk
jmp .L24
.L14:
call std::__throw_bad_function_call()
.LEHE5:
.L32:
movq 48(%rsp), %rcx
movq %rax, %rbx
testq %rcx, %rcx
je .L20
leaq 32(%rsp), %rsi
movl $3, %edx
movq %rsi, %rdi
call *%rcx
.L20:
movq 16(%rsp), %rax
testq %rax, %rax
je .L29
movl $3, %edx
movq %rsp, %rsi
movq %rsp, %rdi
call *%rax
.L29:
movq %rbx, %rdi
.LEHB6:
call _Unwind_Resume
.LEHE6:
.L50:
call __stack_chk_fail
.L34:
movq 48(%rsp), %rcx
movq %rax, %rbx
testq %rcx, %rcx
je .L20
leaq 32(%rsp), %rsi
movl $3, %edx
movq %rsi, %rdi
call *%rcx
jmp .L20
.L31:
movq %rax, %rbx
jmp .L20
.L33:
movq 16(%rsp), %rcx
movq %rax, %rbx
testq %rcx, %rcx
je .L29
movl $3, %edx
movq %rsp, %rsi
movq %rsp, %rdi
call *%rcx
jmp .L29
.cfi_endproc
Run Code Online (Sandbox Code Playgroud)
在性能方面,您想选择哪种方法?
如果你将lambda绑定到a std::function
那么你的代码将运行得更慢,因为它将不再是可内联的,调用通过函数指针和函数对象的创建可能需要堆分配,如果lambda的大小(=捕获状态的大小)超过小缓冲区限制(等于GCC IIRC上的一个或两个指针的大小).
例如auto a = []{};
,如果保留lambda,那么它将与内联函数一样快(可能更快,因为当作为参数传递给函数时,没有转换为函数指针.)
在启用优化(-O1
或在我的测试中更高)进行编译时,lambda和内联函数对象生成的目标代码将为零.有时,编译器可能会拒绝内联,但通常只在尝试内联大型函数体时才会发生.
如果您想确定,可以随时查看生成的装配.
归档时间: |
|
查看次数: |
1400 次 |
最近记录: |