c++ - 就内存使用而言,模板 + 仿函数/lambdas 不是最理想的吗?

标签 c++ templates c++11 lambda functor

出于说明目的,假设我要实现一个通用整数比较函数。我可以想到一些方法来定义/调用函数。

(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 with std::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/

相关文章:

c++ - 使用 cmake 构建具有 "experimental/filesystem"的项目

c++ - std::enable_if 的第二个参数有什么用?

c++ - 编译时如何使用外部模板参数

python - 使用 Boost Python 从 C++ 类创建派生的 Python 类

c++ - 为 VS2010、WINDOWS7、64BIT 构建 CUDA 示例时出现错误 MSB3721

c++ - 逻辑短路是否不适用于 if-constexpr?

c++ - Visual C++ std::regex_search 错误?

c++ - 为什么在 header 上的结构中声明的模板不违反 ODR 而特化却违反了?

c++ - 如何在不使用额外模板参数的情况下使用模板模板参数声明/定义类

c++ - 在 C++ 中, "class instance"是唯一的对象类型吗?