为了便于说明,假设我想实现一个通用的整数比较函数。我可以想到一些定义/调用函数的方法。
(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)的结果如下:
即使我显示了数字,它也比定量基准更具质量。正如我们所期望的那样,基于std::function
参数或函数指针的函数模板更好地(在大小方面)缩放,因为没有创建太多实例。我没有测量运行时内存使用情况。
至于性能结果(矢量大小为1000000个元素):
这是一个显着的差异,我们不能忽视std::function
引入的开销(至少如果我们的算法包含数百万次迭代)。
答案 0 :(得分:9)
正如其他人已经指出的那样, lambdas和函数对象很可能被内联,特别是如果函数的主体不太长。因此,它们在速度和内存使用方面可能比std::function
方法更好。 如果可以内联函数,编译器可以更积极地优化代码。令人震惊的更好。 std::function
因此而成为我的最后手段。
但是在处理数十个(或数百个)模板时,数量众多 仿函数,编译时间和内存使用量的差异都可以 显着的。
至于编译时间,只要你使用如图所示的简单模板,我就不会太担心它。 (如果你正在进行模板元编程,是的,那么你可以开始担心。)
现在,内存使用情况:编译时编译器还是运行时生成的可执行文件?对于前者,与编译时相同。对于后者:内联lamdas和函数对象是赢家。
我们可以在很多情况下说
std::function
(甚至是 函数指针)必须优先于模板+原始 仿函数/ lambda表达式?即包裹你的算子或lambdastd::function
可能非常方便。
我不太确定如何回答这个问题。我无法定义“很多情况”。
但是,我可以肯定地说,类型擦除是一种避免/减少模板引起的代码膨胀的方法,请参阅项目44:模板中与因子参数无关的代码 { {3}}。顺便说一下,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);
}
这是您的代码的略微修改版本。我删除了所有的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在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);
}
在-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
在性能方面,您想选择哪种方法?
答案 1 :(得分:8)
如果你将lambda绑定到std::function
,那么你的代码将运行得更慢,因为它将不再是可内联的,调用通过函数指针和函数对象的创建可能需要堆分配,如果大小lambda(=捕获状态的大小)超过小缓冲区限制(等于GCC IIRC上的一个或两个指针的大小)。
如果你保留你的lambda,例如auto a = []{};
,然后它将与内联函数一样快(可能更快,因为当作为参数传递给函数时,没有转换为函数指针。)
在启用优化(-O1
或更高版本的测试中)编译时,lambda和内联函数对象生成的目标代码将为零。有时,编译器可能会拒绝内联,但通常只在尝试内联大型函数体时才会发生。
如果您想确定,可以随时查看生成的装配体。
答案 2 :(得分:2)
我将讨论天真发生的事情,以及常见的优化。
(A)功能模板+仿函数
在这种情况下,将会有一个仿函数,其类型和参数完整地描述了在调用()
时会发生什么。传递给每个仿函数的template
函数将有一个实例。
虽然仿函数的最小值为1个字节,并且必须在技术上复制,但副本是无操作的(甚至不需要复制1个字节的空间:一个哑编译器/低优化设置可能会导致它无论如何都被复制了。)
优化编译器对象的存在对编译器来说很容易:方法是inline
并且具有相同的符号名称,所以这甚至会发生在多个编译单元上。内联电话也很容易。
如果你有多个具有相同实现的函数对象,尽管如此需要花费一些函数,但是有些编译器可以做到这一点。内联template
函数也很容易。在您的玩具示例中,由于输入在编译时是已知的,因此可以在编译时评估分支,消除死代码,并将所有内容简化为单个std::cout
调用。
(B)功能模板+ lambdas
在这种情况下,每个lambda都是它自己的类型,每个闭包实例都是该lambda的未定义大小实例(通常为1个字节,因为它什么都不捕获)。如果定义相同的lambda并在不同的位置使用,则这些是不同的类型。函数对象的每个调用位置都是template
函数的独特实例。
删除1字节闭包的存在(假设它们是无状态的)很容易。内联它们也很容易。删除具有相同实现但不同签名的template
函数的重复实例更难,但某些编译器会这样做。内联所述功能并不比上面更难。在您的玩具示例中,由于输入在编译时是已知的,因此可以在编译时评估分支,消除死代码,并将所有内容简化为单个std::cout
调用。
(C)功能模板+ std :: function
std::function
是类型擦除对象。具有给定签名的std::function
实例与另一个具有相同的类型。但是,std::function
的构造函数是template
d传入的类型。在您的情况下,您传入一个lambda - 所以每个位置用lambda初始化std::function
生成一个独特的std::function
构造函数,它会执行未知代码。
std::function
的典型实现将使用pImpl
模式将指向抽象接口的指针存储到包装可调用对象的辅助对象,并知道如何使用它来复制/移动/调用它。 std::function
签名。每种类型创建一个这样的可调用类型std::function
是根据它构造的每个std::function
签名构建的。
将创建一个函数实例,取std::function
。
有些编译器可以注意到重复的方法并对两者使用相同的实现,并且可能为(很多)virtual
函数表提取类似的技巧(但不是全部,因为动态转换要求它们不同) 。与早期的重复功能消除相比,这种情况发生的可能性较小。 std::function
帮助程序使用的重复方法中的代码可能比其他重复函数更简单,因此可能更便宜。
虽然template
函数可以是inline
d,但我不知道可以优化std::function
的存在的C ++编译器,因为它们通常被实现为库解决方案由编译器的相对复杂和不透明的代码组成。因此,虽然理论上可以对所有信息进行评估,但实际上std::function
不会内联到template
函数中,也不会消除死代码。两个分支都会将它组成生成的二进制文件,以及一堆std::function
样板本身及其助手。
调用std::function
与调用virtual
方法一样昂贵 - 或者,根据经验,与两个函数指针调用一样昂贵。
(D)Raw&#34; C-style&#34;指针强>
创建一个函数,获取其地址,该地址传递给compare_int
。然后它取消引用该指针以找到实际的函数,并调用它。
有些编译器擅长注意函数指针是从文字创建的,然后在此处内联调用:并非所有编译器都可以执行此操作,并且在一般情况下没有或很少编译器可以执行此操作。如果它们不能(因为初始化不是来自文字,因为接口是在一个编译单元而实现是在另一个中),跟随数据的函数指针有很大的代价 - 计算机往往是无法缓存它的位置,因此存在管道停滞。
请注意,您可以调用原始&#34; C风格&#34;无状态lambda的指针,无状态lambdas隐式转换为函数指针。另请注意,此示例严格弱于其他示例:它不接受有状态函数。具有相同功能的版本将是C风格的函数,它同时采用{{1}}和int
状态。
答案 3 :(得分:1)
你应该使用带有lambda函数的auto关键字,而不是std :: function。这样你就可以获得独特的类型,并且没有std :: function的运行时开销。
另外,正如 dyp 所示,无状态(即没有捕获)lamba函数可以转换为函数指针。
答案 4 :(得分:0)
在A,B和C中,您可能最终得到一个不包含比较器的二进制文件,也不包含任何模板。它将字面上内联所有的比较,甚至可能删除打印的非真实分支 - 实际上,它将“放置”调用实际结果,而不做任何检查。
在D中,您的编译器无法做到这一点。
相反,在这个例子中它更优。它也更灵活 - 一个std :: function可以隐藏存储的成员,或者它只是一个普通的C函数,或者它是一个复杂的对象。它甚至允许您从类型的各个方面派生实现 - 如果您可以对POD类型进行更有效的比较,您可以实现它,并在其余部分忘记它。
以这种方式思考 - A和B是更高级的抽象实现,允许您告诉编译器“这个代码可能最好分别为每种类型实现,所以当我使用它时这样做”。 C是一个说“复杂的多个比较运算符,但它们都看起来像这样只做一个compare_int函数的实现”。在D你告诉它“不要打扰,只要让我这些功能。我最了解。”没有一个明显优于其他人。