首页 文章

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

提问于
浏览
216

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

  • auto 如何提高绩效?

  • 谁能举个例子?

4 回答

  • 7

    auto 可以通过 avoiding silent implicit conversions 帮助表现 . 我发现一个令人信服的例子如下 .

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

    看到这个bug?我们在这里,以为我们正在复制 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
    }
    

    刚刚保存了大量的副本 - 现在引用的类型与初始化程序类型匹配,因此不需要临时或转换,我们可以直接引用 .

  • 37

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

    一个典型的例子来自(ab)使用 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();
    

    这始终是一个引用,绑定到函数调用表达式的值,并且永远不会构造任何其他对象 . 如果你没有类型,你可能会被迫通过像 T && f = MakeAThing() 之类的东西构建一个新对象(可能是一个临时对象) . (此外, auto && 甚至可以在返回类型不可移动且返回值为prvalue时工作 . )

  • 289

    有两类 .

    auto 可以避免类型擦除 . 有不可思议的类型(如lambdas)和几乎不可用的类型(如 std::bind 或其他类似表达式的模板的结果) .

    如果没有 auto ,您最终必须将数据删除类似于 std::function . 类型擦除有成本 .

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

    task1 具有类型擦除开销 - 可能的堆分配,难以内联它,以及虚函数表调用开销 . task2 没有 . Lambdas需要自动或其他形式的类型扣除来存储而不需要类型擦除;其他类型可能非常复杂,以至于他们在实践中只需要它 .

    其次,你可以得到错误的类型 . 在某些情况下,错误的类型看似完美,但会产生副本 .

    Foo const& f = expression();
    

    如果 expression() 返回 Bar const&Bar 或甚至 Bar& ,将编译,其中 Foo 可以从 Bar 构造 . 将创建临时 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> . ( Map 上的键是const,因为编辑它是一个坏主意)

    @Barry和@KerrekSB都在他们的答案中首次阐述了这两个原则 . 这只是尝试在一个答案中突出这两个问题,其中的措辞旨在解决问题而不是以实例为中心 .

  • 67

    现有的三个答案举例说明了使用 auto 帮助“makes it less likely to unintentionally pessimize”有效地使它成为"improve performance" .

    这枚硬币还有另一面 . 运用 auto 对于具有不返回基本对象的运算符的对象可能导致不正确(仍可编译和可运行)代码 . 例如,this question询问如何使用 auto 使用特征库给出不同(不正确)的结果,即以下行

    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懒惰评估,但该代码对于(库)用户是透明的 .

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

相关问题