模板+函子/ lambda在内存使用方面是不是最理想的?

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):

  • 具有1个TEST宏实例的二进制:17009
  • 1 TEST2宏实例:9932
  • 1 TEST3宏实例:9820
  • 50 TEST宏实例:59918
  • 50个TEST2宏实例:94682
  • 50个TEST3宏实例:16857

即使我显示了这些数字,它也是一个定性基准而不是定量基准.正如我们所期望的那样,基于std::function参数或函数指针的函数模板更好地(在大小方面)缩放,因为没有创建太多实例.我没有测量运行时内存使用情况.

至于性能结果(矢量大小是1000000的元素):

  • 50 TEST宏实例:5.75秒
  • 50个TEST2宏实例:1.54s
  • 50个TEST3宏实例:3.20s

这是一个值得注意的差异,我们不能忽视所引入的开销std::function(至少如果我们的算法包含数百万次迭代).

Ali*_*Ali 9

正如其他人已经指出的那样,lambda和函数对象很可能被内联,特别是如果函数的主体不太长.因此,它们在速度和内存使用方面可能比方法更好std::function.如果可以内联函数,编译器可以更积极地优化代码.令人震惊的更好. std::function除了其他方面,这将是我最后的手段.

但是当处理数十个(或数百个)模板和许多仿函数时,编译时间和内存使用差异可能很大.

至于编译时间,只要你使用如图所示的简单模板,我就不会太担心它.(如果你正在进行模板元编程,是的,那么你可以开始担心.)

现在,内存使用情况:编译时编译器还是运行时生成的可执行文件?对于前者,与编译时相同.对于后者:内联lamdas和函数对象是赢家.

我们可以说在许多情况下std::function(甚至函数指针)必须优先于模板+原始函子/ lambdas吗?即包裹你的仿函数或lambda std::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)

在性能方面,您想选择哪种方法?


Sta*_*ked 8

如果你将lambda绑定到a std::function那么你的代码将运行得更慢,因为它将不再是可内联的,调用通过函数指针和函数对象的创建可能需要堆分配,如果lambda的大小(=捕获状态的大小)超过小缓冲区限制(等于GCC IIRC上的一个或两个指针的大小).

例如auto a = []{};,如果保留lambda,那么它将与内联函数一样快(可能更快,因为当作为参数传递给函数时,没有转换为函数指针.)

在启用优化(-O1或在我的测试中更高)进行编译时,lambda和内联函数对象生成的目标代码将为零.有时,编译器可能会拒绝内联,但通常只在尝试内联大型函数体时才会发生.

如果您想确定,可以随时查看生成的装配.