【问题标题】:Copy overhead when returning (big) objects?返回(大)对象时复制开销?
【发布时间】:2016-02-10 12:51:29
【问题描述】:

考虑以下两个简单 Matrix4x4 Identity 方法的实现。

1:这个以Matrix4x4引用为参数,数据直接写入其中。

static void CreateIdentity(Matrix4x4& outMatrix) {
    for (int i = 0; i < 4; ++i) {
        for (int j = 0; j < 4; ++j) {
            outMatrix[i][j] = i == j ? 1 : 0;
        }
    }
}

2:这个返回一个 Matrix4x4 而不接受任何输入。

static Matrix4x4 CreateIdentity() {
    Matrix4x4 outMatrix;
    for (int i = 0; i < 4; ++i) {
        for (int j = 0; j < 4; ++j) {
            outMatrix[i][j] = i == j ? 1 : 0;
        }
    }
    return outMatrix;
}

现在,如果我想真正创建一个身份矩阵,我必须这样做

Matrix4x4 mat;
Matrix4x4::CreateIdentity(mat);

对于第一个变体和

Matrix4x4 mat = Matrix4x4::CreateIdentity();

第二次。

第一个显然产生了一个优点,即没有完成一个不必要的副本,同时它不允许将其用作右值;想象一下

Matrix4x4 mat = Matrix4x4::Identity()*Matrix4x4::Translation(5, 7, 6);

最后一个问题:有没有办法在尽可能使用Matrix4x4::CreateIdentity(); 之类的方法时避免不必要的复制,同时仍然允许像我上一个代码示例中那样将该方法用作右值?它甚至由编译器自动优化吗?我很困惑如何有效地完成这个(看似)简单的任务。也许我应该实现这两个版本并使用任何合适的版本?

【问题讨论】:

  • 至少还有另一种避免复制的方法,即在语法上不那么尴尬:通过std::move 返回,并在您的类中定义移动构造函数和移动赋值运算符。
  • @VioletGiraffe 我认为一个简单的返回应该已经使用了适用的移动构造函数,即select the constructor to use for initialization of the returned value is performed twice: first as if expression were an rvalue expression (thus it may select the move constructor or a copy constructor taking reference to const)。但我不确定这是否始终如我所见。想法?
  • @VioletGiraffe 我记得,我曾经收到一个关于既没有移动也没有复制构造函数的对象的警告,警告是“注意,如果 NRVO 没有,这将不起作用发生在这里。”但这里不是这样。那么再一次,想法?
  • @VioletGiraffe 如果您使用std::move,则不会发生 NRVO,如果我没记错的话,生成的代码可能会降低效率。

标签: c++ c++11


【解决方案1】:

鉴于copy elision(在本例中为 NRVO1)是标准的一部分,因此您基本上不必担心太多。

更详细一点(危险地),返回矩阵的版本很可能最终将其分配到调用函数的堆栈上,并且仅初始化它在被调用函数中,没有调用任何复制构造函数。

所以除非有什么东西阻止了这一点(你可以通过运行它并检查是否调用了复制构造函数来发现),那么你大多不需要担心它 .

如果复制省略不能发生(或者只是出于某种原因不会,例如如果编译器不想这样做,因为它没有 to),那么您仍然可以确保提供一个 move 构造函数,然后使用它来代替2。这里的好处是,当您的 return 语句涉及到实际返回类型的转换时,它甚至可以工作。

参考资料:

  1. 如果函数按值返回类类型,并且return语句的表达式是具有自动存储持续时间的非易失性对象的名称,它不是函数参数,也不是catch子句参数,并且具有与函数的返回类型相同的类型(忽略顶级 cv 限定),则省略复制/移动。当构造该本地对象时,它直接在存储中构造,否则函数的返回值将被移动或复制到该存储中。这种复制省略的变体被称为 NRVO,“命名返回值优化”。

  2. 如果 expression 是左值表达式并且满足或将满足复制省略的条件,但表达式命名函数参数除外,则执行两次重载决策以选择用于初始化返回值的构造函数:首先好像表达式是一个右值表达式(因此它可以选择移动构造函数或引用 const 的复制构造函数),如果没有合适的转换可用,则第二次执行重载决议,使用左值表达式(因此它可以选择复制构造函数引用非常量)。

    即使函数返回类型与表达式类型不同(复制省略需要相同类型),上述规则也适用。

【讨论】:

  • 你可能想提一下,如果他们用他们的类实现了一个移动语义,那么当复制省略不会发生时,移动就会开始。
  • @NathanOliver 谢谢,添加(有一点额外说明)。
  • @NathanOliver 虽然只有在类型可以有效移动时才会有所帮助。
  • @juanchopanza 对,在 Matrix 的情况下,可能并非如此,例如如果它在内部使用一个数组来保存它的数据作为对象内存的一部分。
  • @YamMarcovic 没错,这对于小型矩阵类型很常见。顺便说一句,可能值得探索表达式模板以避免在复杂的矩阵表达式中创建临时变量。
猜你喜欢
  • 2013-10-24
  • 1970-01-01
  • 2020-08-02
  • 1970-01-01
  • 2015-08-28
  • 2016-02-15
  • 2016-10-10
  • 2010-10-18
  • 1970-01-01
相关资源
最近更新 更多