使用 C++11 的 'auto' 可以提高性能吗?

Can the use of C++11's 'auto' improve performance?

我明白了为什么 C++11 中的 auto 类型提高了正确性和可维护性。我读过它也可以提高性能(Herb Sutter 的Almost Always Auto),但我错过了一个很好的解释。

因为auto推导出初始化表达式的类型,所以不涉及类型转换。结合模板化算法,这意味着您可以获得比自己构建类型更直接的计算 - 特别是当您处理无法命名其类型的表达式时!

一个典型的例子来自(ab)using std::function:

std::function<bool(T, T)> cmp1 = std::bind(f, _2, 10, _1);  // bad
auto cmp2 = std::bind(f, _2, 10, _1);                       // good
auto cmp3 = [](T a, T b){ return f(b, 10, a); };            // also good

std::stable_partition(begin(x), end(x), cmp?);

使用cmp2cmp3,整个算法可以内联比较调用,而如果构造一个std::function对象,不仅不能内联调用,而且你还必须在函数包装器的类型擦除内部进行多态查找。

这个主题的另一个变体是你可以说:

auto && f = MakeAThing();

这始终是一个引用,绑定到函数调用表达式的值,并且从不构造任何额外的对象。如果您不知道 returned 值的类型,您可能被迫通过 T && f = MakeAThing() 之类的东西构造一个新对象(可能是临时对象)。 (此外,auto && 甚至在 return 类型不可移动且 return 值为纯右值时也有效。)

auto 可以通过 避免静默隐式转换 来提高性能。下面是一个我觉得很有说服力的例子。

std::map<Key, Val> m;
// ...

for (std::pair<Key, Val> const& item : m) {
    // do stuff
}

看到错误了吗?我们在这里,认为我们通过 const 引用优雅地获取地图中的每个项目并使用新的 range-for 表达式来明确我们的意图,但实际上我们正在复制 every 元素.这是因为 std::map<Key, Val>::value_typestd::pair<const Key, Val>,而不是 std::pair<Key, Val>。因此,当我们(隐含地)有:

std::pair<Key, Val> const& item = *iter;

我们必须进行类型转换,而不是引用现有对象并保留它。只要存在可用的隐式转换,您就可以对不同类型的对象(或临时对象)进行 const 引用,例如:

int const& i = 2.0; // perfectly OK

类型转换是一种允许的隐式转换,原因与您可以将 const Key 转换为 Key 的原因相同,但我们必须构造一个新类型的临时类型以允许那。因此,我们的循环实际上是:

std::pair<Key, Val> __tmp = *iter;       // construct a temporary of the correct type
std::pair<Key, Val> const& item = __tmp; // then, take a reference to it

(当然,实际上并没有 __tmp 对象,它只是为了说明,实际上未命名的临时对象在其生命周期内仅绑定到 item)。

刚改成:

for (auto const& item : m) {
    // do stuff
}

刚刚为我们节省了大量副本 - 现在引用的类型与初始化器类型匹配,因此不需要临时或转换,我们可以直接引用。

有两类。

auto 可以避免类型擦除。有不可命名的类型(如 lambda)和几乎不可命名的类型(如 std::bind 的结果或其他表达式模板之类的东西)。

如果没有 auto,您最终不得不将数据擦除到类似 std::function 的类型。类型擦除是有成本的。

std::function<void()> task1 = []{std::cout << "hello";};
auto task2 = []{std::cout << " world\n";};

task1 具有类型擦除开销——可能的堆分配、内联困难以及虚函数 table 调用开销。 task2 有 none。 Lambdas 需要 auto 或其他形式的类型推导来存储而无需类型擦除;其他类型可能非常复杂,以至于他们只在实践中需要它。

其次,您可能会弄错类型。在某些情况下,错误的类型看似完美,但会导致复制。

Foo const& f = expression();
如果 expression() returns Bar const&Bar 甚至 Bar&

将编译,其中 Foo 可以从 [=20 构造=].将创建一个临时 Foo,然后绑定到 f,其生命周期将延长,直到 f 消失。

程序员的意思可能是 Bar const& f 而不是打算在那里制作副本,但无论如何都会制作副本。

最常见的例子是 *std::map<A,B>::const_iterator 的类型,它是 std::pair<A const, B> const& 而不是 std::pair<A,B> const&,但该错误是一类会默默降低性能的错误。您可以从 std::pair<const A, B> 构建 std::pair<A, B>。 (地图上的关键是常量,因为编辑它是个坏主意)

@Barry 和@KerrekSB 都在他们的回答中首先说明了这两个原则。这只是试图在一个答案中强调这两个问题,措词针对的是问题而不是以示例为中心。

现有的三个答案给出了使用 auto 帮助 有效实现 "improve performance".

的示例

硬币有反面。将 auto 与具有不 return 基本对象的运算符的对象一起使用可能会导致不正确(仍然可编译和可运行)的代码。例如,this question 询问如何使用 auto 使用 Eigen 库给出不同(不正确)的结果, 以下行

const auto    resAuto    = Ha + Vector3(0.,0.,j * 2.567);
const Vector3 resVector3 = Ha + Vector3(0.,0.,j * 2.567);

std::cout << "resAuto = " << resAuto <<std::endl;
std::cout << "resVector3 = " << resVector3 <<std::endl;

导致不同的输出。诚然,这主要是由于 Eigens 惰性求值,但代码 is/should 对(库)用户是透明的。

虽然这里的性能没有受到太大影响,但使用 auto 来避免无意的悲观化可能被归类为过早优化,或者至少是错误的 ;)。