出于说明目的,假设我要实现一个通用整数比较函数。我可以想到一些方法来定义/调用函数。
(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;
}
};
这将是对该函数的几次调用:
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);
(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;});
(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);
(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;
}
调用:
compare_int (10, 5, "less than", lt_func);
compare_int (10, 5, "greater than", gt_func);
在列出这些场景后,我们在每种情况下都有:
(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);
关于生成的二进制文件大小的结果如下(GCC at -O3):
- 带有 1 个 TEST 宏实例的二进制文件:17009
- 1 TEST2 宏实例:9932
- 1 TEST3 宏实例:9820
- 50 个 TEST 宏实例:59918
- 50 个 TEST2 宏实例:94682
- 50 个 TEST3 宏实例:16857
即使我展示了数字,它也是一个比定量基准更定性的基准。正如我们所料,基于 std::function
的函数模板参数或函数指针可以更好地缩放(就大小而言),因为创建的实例不多。不过我没有测量运行时内存使用情况。
关于性能结果( vector 大小为 1000000 个元素):
- 50 个 TEST 宏实例:5.75 秒
- 50 个 TEST2 宏实例:1.54 秒
- 50 个 TEST3 宏实例:3.20 秒
这是一个显着的区别,我们不能忽视 std::function
引入的开销(至少如果我们的算法包含数百万次迭代)。
最佳答案
正如其他人已经指出的那样,lambdas 和函数对象很可能是内联的,尤其是在函数体不太长的情况下。因此,它们在速度和内存使用方面可能比 std::function
方法更好。 如果函数可以内联,编译器可以更积极地优化您的代码。好得惊人。 std::function
将是我最后的手段,除此之外。
But when working with dozens (or hundreds) of templates, and numerous functors, the compilation times and memory usage difference can be substantial.
至于编译时间,只要您使用如图所示的简单模板,我就不会担心太多。 (如果你正在做模板元编程,是的,那么你可以开始担心了。)
现在,内存使用情况:编译期间由编译器使用,还是在运行时由生成的可执行文件使用?对于前者,与编译时间相同。对于后者:内联 lamdas 和函数对象是赢家。
Can we say that in many circumstances
std::function
(or even function pointers) must be preferred over templates+raw functors/lambdas? I.e. wrapping your functor or lambda withstd::function
may be very convenient.
我不太确定如何回答这个问题。我无法定义“许多情况”。
但是,我可以肯定地说,类型删除是一种避免/减少由于模板导致的代码膨胀的方法,请参阅 Item 44: Factor-independent code out of templates in Effective C++ .顺便说一句,std::function
在内部使用类型删除。所以是的,代码膨胀是个问题。
I am aware that std::function (function pointer too) introduces an overhead. Is it worth it?
“想要速度?测量。” (霍华德·欣南特)
还有一件事:通过函数指针的函数调用可以内联(甚至跨编译单元!)。这是一个证明:
#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);
}
这是您的代码稍作修改的版本。我删除了所有 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
这意味着,gcc 4.7.2 在 -O3
内联了 lt_func
。事实上,生成的汇编代码是最优的。
我还检查了:我将 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);
}
在 -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
在性能方面您想选择哪种方法?
关于c++ - 就内存使用而言,模板 + 仿函数/lambdas 不是最理想的吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/21125464/