11

2013 主题演讲:Chandler Carruth:优化 C++ 的紧急结构

  • 42:45
    你不需要输出参数,我们在 C++ 中有值语义。...任何时候你看到有人争辩说 nonono 我不会按价值返回,因为复制成本太高,有人在优化器上说他们错了。好的?我还从未见过该论点正确的一段代码。...人们没有意识到值语义对优化器的重要性,因为它完全阐明了别名场景。

谁能把这个放在这个答案的上下文中:https ://stackoverflow.com/a/14229152

我听说不断重复,但是,对我来说,返回某些东西的函数是source。通过引用的输出参数从函数中取出该特征,并且从函数中删除这种硬编码特征允许人们在外部管理,而不是管理输出将如何存储/重用。

我的问题是,即使在那个 SO 答案的上下文中,有没有办法告诉,以其他等效方式重组代码,“现在看,这种方式的值语义不会丢失输出参数版本”,或者钱德勒的评论是针对某些人为情况的?我什至看到 Andrei Alexandrescu 在一次谈话中争论这一点,并告诉你不能逃避使用 by ref 输出以获得更好的性能。

有关 Andrei 评论的另一种看法,请参阅Eric Niebler:Out Parameters、Move Semantics 和 Stateful Algorithms

4

3 回答 3

3

我的程序无法接受这要么是夸大其词、泛化、笑话,要么是 Chandler 的“完全合理的性能”(使用现代 C++ 工具链/库)的想法。

我发现它的优化范围相当狭窄。由于程序中发现的实际复杂性和设计,惩罚超出了不能忽略的范围——堆分配就是一个getline例子。尽管您尝试减少特定优化,但它们可能始终适用于相关程序,也可能并不总是适用。现实世界的结构将引用可能别名的内存。您可以减少它,但相信您可以消除混叠(从优化器的角度来看)是不切实际的。

当然,RBV 可能是一件很棒的事情——它只是不适用于所有情况。甚至您引用的链接也指出了如何避免大量分配/释放。真正的程序和其中的数据结构要复杂得多。

在演讲的后期,他继续批评了成员函数的使用(参考:)S::compute()。当然,有一点要带走,但是完全避免使用这些语言特性真的合理吗,因为它使优化器的工作更容易?不会。它总是会产生更具可读性的程序吗?不会。这些代码转换是否总能产生明显更快的程序?不。转换代码库所需的更改是否值得您投入时间?有时。你能拿走一些观点并做出更明智的决定,这些决定会影响你现有或未来的一些代码库吗?是的。

有时它有助于分解您的程序将如何执行,或者它在 C 中的样子。

优化器不会解决所有的性能问题,你不应该假设你正在处理的程序是“完全脑死和坏掉的设计”来重写程序,也不应该相信使用 RBV 总是会导致“Perfectly Reasonable Performance” ”。您可以利用新的语言功能并使优化器的工作变得更轻松,尽管有很多收获,通常更重要的优化可以投入您的时间。

可以考虑提议的更改;理想情况下,在采用这些建议之前,您应该衡量这些更改对现实执行时间的影响以及对源代码的影响。

以您的示例为例:即使按价值复制+分配大型结构也可能会产生巨大的成本。除了运行构造函数和析构函数的成本(以及它们获取和拥有的资源的相关创建/清理,如您引用的链接中所指出的那样)之外,即使是避免不必要的结构副本这样简单的事情也可以为您节省大量 CPU如果您使用参考文献(在适当的情况下)。结构副本可能像memcpy. 这些不是人为的问题。它们出现在实际程序中,复杂性会随着程序的复杂性而大大增加。减少一些内存的混叠和其他优化是否值得付出代价,它是否会导致“完全合理的性能”?不总是。

于 2014-02-11T17:33:44.330 回答
1

我正要使用string_view范围实施解决方案,然后发现:

这支持return 输出的值语义 ,我接受它比引用的 SO 答案中的当前选择更漂亮。这种设计也将作为一个函数的的特性,即使它正在返回。简单说明:可以有一个外部保留向量被返回的范围重复填充。

无论如何,我不确定这样的版本是否split会在任何意义上帮助优化器(我在这里与 Chandler 的谈话有关)。

注意

输出参数版本强制在调用站点存在命名变量,这可能看起来很难看,但可能会使调试调用站点变得更容易。

样品溶液

虽然std::split没有到达,但我已经通过这种方式执行了返回版本输出的值语义:

#include <string>
#include <string_view>
#include <boost/regex.hpp>
#include <boost/range/iterator_range.hpp>
#include <boost/iterator/transform_iterator.hpp>

using namespace std;
using namespace std::experimental;
using namespace boost;

string_view stringfier(const cregex_token_iterator::value_type &match) {
    return {match.first, static_cast<size_t>(match.length())};
}

using string_view_iterator =
    transform_iterator<decltype(&stringfier), cregex_token_iterator>;

iterator_range<string_view_iterator> split(string_view s, const regex &r) {
    return {
        string_view_iterator(
            cregex_token_iterator(s.begin(), s.end(), r, -1),
            stringfier
        ),
        string_view_iterator()
    };
}

int main() {
    const regex r(" +");
    for (size_t i = 0; i < 1000000; ++i) {
        split("a b c", r);
    }
}

我使用了https://github.com/mclow/string_viewstring_view上的 Marshall Clow 的libc++ 实现。

我已经在参考答案的底部发布了时间。

于 2014-02-11T17:02:06.807 回答
1

链接问题中描述的输出参数的问题是,它们通常使常见的调用情况(即,您没有vector要使用的存储)比正常情况要详细得多。例如,如果您使用按值返回:

auto a = split(s, r);

如果您使用输出参数:

std::vector<std::string> a;
split(s,r,a);

在我看来,第二个看起来那么漂亮。此外,正如 Chandler 所提到的,优化器可以使用第一个而不是第二个执行更多操作,具体取决于您的代码的其余部分。

有没有办法让我们两全其美?强调yes,使用移动语义:

std::vector<std::string> split(const std::string &s, const std::regex &r, std::vector<std::string> v = {})
{
    auto rit = std::sregex_token_iterator(s.begin(), s.end(), r, -1);
    auto rend = std::sregex_token_iterator();
    v.clear();
    while(rit != rend)
    {
        v.push_back(*rit);
        ++rit;
    }

    return v;
}

现在,在一般情况下,我们可以split正常调用(即第一个示例),它会vector为我们分配一个新的存储空间。在重要但罕见的情况下,当我们必须重复拆分并且想要重用相同的存储时,我们可以只移动到在调用之间持续存在的存储中:

int main()
{
    const std::regex r(" +");
    std::vector<std::string> a;
    for(auto i=0; i < 1000000; ++i)
      a = split("a b c", r, std::move(a));
    return 0;
}

这与输出参数方法的运行速度一样快,并且发生了什么非常清楚。您不必为了在某些时候获得良好的性能而一直使您的功能难以使用。

于 2014-02-11T21:35:58.527 回答