优化编译器可以添加std::move吗



如果编译器能够证明左值不会被再次使用,它是否可以进行左值到右值的自动转换?这里有一个例子来澄清我的意思:

void Foo(vector<int> values) { ...}
void Bar() {
  vector<int> my_values {1, 2, 3};
  Foo(my_values);  // may the compiler pretend I used std::move here?
}

如果将std::move添加到注释行,则可以将向量移动到Foo的参数中,而不是复制。然而,正如所写的,我没有使用std::move

很容易静态地证明my_values不会在注释行之后使用。那么,编译器是允许移动向量,还是需要复制它?

编译器需要表现得像从vectorFoo调用的复制一样。

如果编译器能够证明存在一个有效的抽象机器行为,并且没有可观察到的副作用(在抽象机器行为中,而不是在真实的计算机中!),包括将std::vector移动到Foo,那么它就可以做到这一点。

在你的上述情况下,这(移动没有抽象的机器可见的副作用)是正确的;然而,编译器可能无法证明这一点。

复制std::vector<T>时可能观察到的行为是:

  • 正在对元素调用复制构造函数。无法观察到使用int执行此操作
  • 在不同的时间调用默认的std::allocator<>。这会调用::new::delete(可能1)在任何情况下,::new::delete在上述程序中都没有被替换,因此您无法在标准下观察到这一点
  • 对不同的对象多次调用T的析构函数。int不可见
  • 在对Foo的调用之后,vector是非空的。没有人检查它,所以它是空的,就好像它不是
  • 外部向量的元素的引用、指针或迭代器与内部向量不同。没有引用、矢量或指针指向Foo之外的矢量元素

虽然你可能会说"但如果系统内存不足,矢量很大,那怎么办?":

抽象机器没有"内存不足"的情况,它只是由于非约束原因导致分配有时失败(抛出std::bad_alloc)。它not失败是抽象机器的一种有效行为,不分配(实际)内存(在实际计算机上)也有效,只要内存的不存在没有可观察的副作用。

一个稍微多一点的玩具箱:

int main() {
  int* x = new int[std::size_t(-1)];
  delete[] x;
}

虽然这个程序显然分配了太多的内存,但编译器可以自由地不分配任何东西。

我们可以走得更远。偶数:

int main() {
  int* x = new int[std::size_t(-1)];
  x[std::size_t(-2)] = 2;
  std::cout << x[std::size_t(-2)] << 'n';
  delete[] x;
}

可以变成CCD_ 21。这个大缓冲区必须存在抽象,但只要你的"真实"程序表现得像抽象机器一样,它实际上就不必分配它

不幸的是,以任何合理的规模这样做都是困难的。从C++程序中泄漏信息的方式有很多种。因此,依赖这样的优化(即使它们发生了)也不会有好的结果。


1有一些关于合并对new的调用的内容可能会混淆问题,我不确定即使有替换的::new,跳过调用是否合法。


一个重要的事实是,在某些情况下,即使没有调用std::move,编译器也不需要表现得像有副本一样。

当您从一行中的函数中return一个局部变量,看起来像return X;X是标识符,并且该局部变量具有自动存储持续时间(在堆栈上)时,该操作隐含地是一个移动,编译器(如果可以的话)可以将返回值和局部变量的存在忽略为一个对象(甚至省略move)。

当您从临时对象构造对象时也是如此——操作隐含地是一个移动(因为它绑定到右值)它可以完全消除移动。

在这两种情况下,编译器都需要将其视为移动(而不是副本),并且可以消除移动。

std::vector<int> foo() {
  std::vector<int> x = {1,2,3,4};
  return x;
}

x没有std::move,但它被移动到返回值中,并且该操作可以被消除(x和返回值可以变成一个对象)。

此:

std::vector<int> foo() {
  std::vector<int> x = {1,2,3,4};
  return std::move(x);
}

阻止省略,如下所示:

std::vector<int> foo(std::vector<int> x) {
  return x;
}

我们甚至可以阻止移动:

std::vector<int> foo() {
  std::vector<int> x = {1,2,3,4};
  return (std::vector<int> const&)x;
}

甚至:

std::vector<int> foo() {
  std::vector<int> x = {1,2,3,4};
  return 0,x;
}

因为隐性转移的规则是故意脆弱的。(0,x是使用了备受诟病的,运算符)。

现在,不建议依赖在上一个基于,的案例中没有发生的隐含动作:标准委员会已经将隐含复制案例改为隐含动作,因为隐含动作被添加到语言中是因为他们认为它无害(其中函数返回一个带有A(B&&) ctor的类型A,返回语句为return b;,其中b的类型为B;在进行复制的C++11版本中,现在它进行移动。)不能排除隐式移动的进一步扩展:显式强制转换为const&可能是现在和将来防止这种情况发生的最可靠方法。

在这种情况下,编译器可以移出my_values。这是因为这不会导致可观察行为的差异。

引用C++标准对可观察行为的定义:

对一致性实施的最低要求是:

  • 对易失性对象的访问严格按照抽象机器的规则进行评估
  • 在程序终止时,写入文件的所有数据应与根据抽象语义执行程序可能产生的结果之一相同
  • 交互式设备的输入和输出动态应以这样一种方式进行,即在程序等待输入之前实际交付提示输出。交互式设备的构成是由实现定义的

稍微解释一下:这里的"文件"包括标准输出流,对于C++标准未定义的函数的调用(例如操作系统调用或对第三方库的调用),必须假设这些函数可能会写入文件,因此必然的结果是,非标准函数调用也必须被视为可观察的行为。

但是,您的代码(如您所示)没有volatile变量,也没有对非标准函数的调用。因此,两个版本(移动或不移动)必须具有相同的可观察行为,因此编译器可以执行(甚至完全优化函数等)

当然,在实践中,编译器通常不太容易证明没有发生非标准函数调用,因此错过了许多这样的优化机会。例如,在这种情况下,编译器可能还不知道默认的::operator new是否已被生成输出的函数替换。

最新更新