【问题标题】:Will C++ lambda really make copies of parameters captured by copy?C++ lambda 真的会复制由副本捕获的参数吗?
【发布时间】:2016-08-09 17:30:17
【问题描述】:

在 C++ 中使用带有按值捕获的变量的 lambda 函数意味着 copy of the value

有了一个好的编译器,假设我们不修改 lambda 函数中的值,我们是否希望代码编译和优化后不会有实际的副本?

例如,在下面将new_item 作为值传递似乎是有意义的,因为它用于只读模式。

void loadavg_file::add(loadavg_item const & new_item)
{
    auto const & it(std::find_if(
            f_items.begin(),
            f_items.end(),
            [new_item](auto const & item)
            {
                return (item.f_address == new_item.f_address);
            }));

    if(it == f_items.end())
    {
        f_items.push_back(it);
    }
    else
    {
        // replace existing item with new avg and timestamp
        it->f_timestamp = new_item.f_timestamp;
        it->f_avg = new_item.f_avg;
    }
}

是否会优化循环并导致绝对没有 new_item 的副本?

【问题讨论】:

  • 没有保证。一个好的编译器可能优化掉副本,但这不是必需的。
  • 您唯一知道的就是代码必须像以前那样运行。如果内联整个事物与不制作副本/对象与您编写的内容之间没有区别,那么编译器可以做到这一点。会吗?编译它并检查程序集。
  • 这个问题对我来说似乎比其他两个评论者更有趣。 AFAIK,编译器只允许在某些条件下删除副本,我不知道 lambda 捕获是否是其中之一。 (当副本微不足道时,这并不重要,但 OP 可能不在乎副本是否微不足道。)
  • @m.s.这也可能取决于loadavg_item 的数据成员的类型。如果它们都是平凡的类型怎么办?无法确定编译器是否总是会创建副本。无论如何,我不清楚为什么 OP 首先没有通过引用捕获new_item
  • @Praetorian,我们无法判断编译器是否会始终遵循某种优化,但我们可以确定是否至少允许这种优化。我绝对愿意被影响,但现在,我并不清楚它是。

标签: c++ lambda language-lawyer compiler-optimization


【解决方案1】:

如果new_item(即loadavg_item::loadavg_item(loadavg_item const&))的复制构造函数除了内存分配之外还有可观察到的影响,那么这些影响必须被观察到(只要,你知道,你真的努力去观察)他们)。

这是因为您可能依赖于发生的那些副作用,而这不是允许复制省略的上下文;仅当 返回 从函数中的值时,才允许(并且后来强制执行)复制省略。另一方面,任何地方都允许内存分配省略(受 [expr.new]/10 中的规则限制); clang 尤其擅长这个。

检查生成的程序集不算作副作用的观察,也不算在调试器中运行程序。

如果new_item 的复制构造函数是非内联的,那么翻译单元的程序集可能会将复制构造函数的调用显示为一个符号,但链接时优化 (LTO) 仍然可以忽略该调用,如果链接时优化器可以推断出复制构造函数没有可观察到的副作用。

【讨论】:

  • 这是否意味着使用参考更有可能增加获得优化的机会?换句话说,作为 C++ 程序员,我们是否应该在几乎所有情况下都使用引用来进行优化(显然,除非 lambda 函数真的要对对象的副本进行修改)?
  • @Alexis:我不会那样说。保证引用可以避免复制,并且还可能会杀死其他优化,因为编译器必须处理带有别名的情况。
【解决方案2】:

是的,一个副本是您的代码需要两个副本(因为 lambda 是按值传递的,所以它的成员会再次被复制)。

如果整个调用树(find_if,复制构造函数,析构函数,operator==,这些调用的任何函数)由可见函数组成并且编译器选择内联它们,则可能需要进一步优化,例如 common-subexpression - 消除可能会减少或消除这些副本的运行时成本。

在这个过程中,编译器必须证明

  • 副本的值不会更改(因为此类更改不应传播到原始值)
  • 复制构造函数没有副作用。
  • 析构函数没有副作用。
  • 所有函数都不依赖于对象的身份。
  • 副本的生命周期不能超过参数引用的对象的生命周期。
  • 参数引用的对象不存在其他别名,或者在使用 lambda 时,不会使用此类其他别名来修改所述对象,包括来自其他线程的访问(如果同步)。

如果您想避免复制,请不要编写要求该副本的代码。或仅复制对象的所需部分,例如

auto const & it(std::find_if(
        f_items.begin(),
        f_items.end(),
        [key = new_item.f_address](auto const & item)
        {
            return (item.f_address == key);
        }));

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-09-29
    • 1970-01-01
    • 2020-04-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-07-25
    相关资源
    最近更新 更多