【问题标题】:Do C++ compilers perform compile-time optimizations on lambda closures?C++ 编译器是否对 lambda 闭包执行编译时优化?
【发布时间】:2015-08-30 19:09:30
【问题描述】:

假设我们有以下(无意义的)代码:

const int a = 0;
int c = 0;
for(int b = 0; b < 10000000; b++)
{
    if(a) c++;
    c += 7;
}

变量 'a' 等于 0,因此编译器可以推断出编译时间,指令 'if(a) c++;'永远不会被执行,并且会优化它。

我的问题:lambda 闭包也会发生同样的情况吗?

查看另一段代码:

const int a = 0;
function<int()> lambda = [a]()
{
    int c = 0;
    for(int b = 0; b < 10000000; b++)
    {
        if(a) c++;
        c += 7;
    }
    return c;
}

编译器会知道 'a' 是 0 并且会优化 lambda 吗?

更复杂的例子:

function<int()> generate_lambda(const int a)
{
    return [a]()
    {
        int c = 0;
        for(int b = 0; b < 10000000; b++)
        {
            if(a) c++;
            c += 7;
        }
        return c;
    };
}

function<int()> a_is_zero = generate_lambda(0);
function<int()> a_is_one = generate_lambda(1);

当编译器知道 'a' 在生成时为 0 时,它会足够聪明地优化第一个 lambda 吗?

gcc 或者 llvm 有这种优化吗?

我之所以问,是因为我想知道当我知道 lambda 生成时间满足某些假设时是否应该手动进行此类优化,或者编译器会为我这样做。

【问题讨论】:

  • 你知道gcc.godbolt.org 吗?它几乎可以回答你所有的问题,甚至更多:) -- 此外,std::function 可能不会被优化,因为它的类型擦除目的与内联直接冲突。
  • @Quentin 看穿类型擦除是编译器的热门话题,而且他们在这方面做得还不错。
  • 我停止将代码编译到 DLLS 中,因为使用静态库时,编译器大多数时候会优化掉虚拟调用,即使使用 GCC 也会发生这种情况,但我担心这样的问题不是主题^^
  • 因为我们有一个memory allocation going on,所以Is the compiler allowed to optimize out heap memory allocations? 的答案可能有助于定义围绕此案例优化的限制。
  • @ShafikYaghmour 它不需要内存分配,一些库实现了优化以存储小型函子而无需分配。

标签: c++ gcc optimization lambda llvm-clang


【解决方案1】:

查看gcc5.2 -O2生成的程序集显示使用std::function时没有发生优化:

#include <functional>

int main()
{
    const int a = 0;    
    std::function<int()> lambda = [a]()
    {
        int c = 0;
        for(int b = 0; b < 10000000; b++)
        {
            if(a) c++;
            c += 7;
        }
        return c;
    };

    return lambda();
}

编译成一些样板文件和

    movl    (%rdi), %ecx
    movl    $10000000, %edx
    xorl    %eax, %eax
    .p2align 4,,10
    .p2align 3
.L3:
    cmpl    $1, %ecx
    sbbl    $-1, %eax
    addl    $7, %eax
    subl    $1, %edx
    jne .L3
    rep; ret

这是您希望优化掉的循环。 (Live) 但如果你真的使用 lambda(而不是 std::function),优化确实会发生:

int main()
{
    const int a = 0;    
    auto lambda = [a]()
    {
        int c = 0;
        for(int b = 0; b < 10000000; b++)
        {
            if(a) c++;
            c += 7;
        }
        return c;
    };

    return lambda();
}

编译成

movl    $70000000, %eax
ret

即循环被完全删除。 (Live)

Afaik,您可以期望 lambda 的开销为零,但 std::function 是不同的,并且会带来成本(至少在优化器的当前状态下,尽管人们显然正在研究这个),即使代码“ std::function" 内部会被优化。 (如果有疑问,请谨慎尝试,因为这可能会因编译器和版本而异。std::functions 开销当然可以优化掉。)

正如@MarcGlisse 正确指出的那样,clang3.6 即使使用std::function 也会执行所需的优化(相当于上面的第二种情况)。 (Live)

额外感谢@MarkGlisse:如果包含std::function 的函数称为main,则gcc5.2 的优化介于gcc+main 和clang 之间,即函数被简化为return 70000000; 加上一些额外的代码。 (Live)

奖励编辑 2,这次是我的:如果你使用 -O3,gcc 将,(出于某种原因)Marco's answer 中所述,优化 std::function

cmpl    $1, (%rdi)
sbbl    %eax, %eax
andl    $-10000000, %eax
addl    $80000000, %eax
ret

其余部分与not_main 一样。所以我猜在最后,使用std::function 时只需要测量即可。

【讨论】:

  • 是的,std::function 很难优化。已经取得了一些进展 (gcc.gnu.org/bugzilla/show_bug.cgi?id=59948)。
  • 请注意,即使是 std::function 的情况,clang 也会优化。
  • 以一种有趣的方式,如果您将 main 重命名为 not_main(因此该函数未标记为冷,这会禁用一些优化),gcc 设法理解 return 70000000; 但它保持之前的一些无用的代码...
  • 旁注:我仍然认为 std::function() 根本不应该存在,整个闭包应该是语言的一部分,而不是某个库的一部分。这将为编译器编写者打开各种聪明的大门。在语言和库之间拆分新的语言功能也发生在其他场合,我从来都不喜欢它。只是说;)
  • @bitticker std::function 语言的一部分:编译器根本不需要将其实现为 C++ 代码。大多数人都这样做,因为它更容易做对。老实说,困难的部分是您可以从std::function 中取回底层对象,因此优化必须在执行诸如“丢弃所有内容”之类的操作之前审核这种可能性。
【解决方案2】:

-O3 的 gcc 和 MSVC2015 Release 都不会用这个简单的代码优化它,而 lambda 实际上会被称为

#include <functional>
#include <iostream>

int main()
{
    int a = 0;    
    std::function<int()> lambda = [a]()
    {
        int c = 0;
        for(int b = 0; b < 10; b++)
        {
            if(a) c++;
            c += 7;
        }
        return c;
    };

    std::cout << lambda();

    return 0;
}

-O3,这是 gcc 为 lambda 生成的(代码来自 godbolt

lambda:
    cmp DWORD PTR [rdi], 1
    sbb eax, eax
    and eax, -10
    add eax, 80
    ret

这是一种人为的优化方式来表达以下内容:

  • 如果 a 为 0,第一次比较将设置进位标志 CReax 实际上会设置为 32 1 个值,and'ed 加上 -10(这将在 eax 中产生 -10),然后添加 80 -> 结果为 70。

  • 如果 a 与 0 不同,第一次比较将设置进位标志 CReax 将设置为零,and没有效果,它会加 80 -> 结果是 80。

必须注意(感谢 Marc Glisse),如果函数被标记为 cold(即不太可能被调用),gcc 会执行正确的操作并优化调用。

MSVC 生成更详细的代码,但不会跳过比较。

Clang 是唯一正确的方法:lambda 的代码优化程度不及 gcc,但它没有被调用

mov edi, std::cout
mov esi, 70
call    std::basic_ostream<char, std::char_traits<char> >::operator<<(int)

士气:Clang 似乎做对了,但优化挑战仍然开放。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-06-16
    • 2021-07-31
    • 2014-02-21
    • 1970-01-01
    • 2016-05-07
    相关资源
    最近更新 更多