使用 C++11 的 'auto' 可以提高性能吗?
Can the use of C++11's 'auto' improve performance?
我明白了为什么 C++11 中的 auto
类型提高了正确性和可维护性。我读过它也可以提高性能(Herb Sutter 的Almost Always Auto),但我错过了一个很好的解释。
- 如何
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?);
使用cmp2
和cmp3
,整个算法可以内联比较调用,而如果构造一个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_type
是 std::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
来避免无意的悲观化可能被归类为过早优化,或者至少是错误的 ;)。
我明白了为什么 C++11 中的 auto
类型提高了正确性和可维护性。我读过它也可以提高性能(Herb Sutter 的Almost Always Auto),但我错过了一个很好的解释。
- 如何
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?);
使用cmp2
和cmp3
,整个算法可以内联比较调用,而如果构造一个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_type
是 std::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
帮助
硬币有反面。将 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
来避免无意的悲观化可能被归类为过早优化,或者至少是错误的 ;)。