【问题标题】:Vector initialisation using ranges vs lambda inline initialisation使用范围初始化向量与 lambda 内联初始化
【发布时间】:2021-07-08 21:21:53
【问题描述】:

我想通过转换另一个向量来初始化一个向量。我用两种内联初始化方式进行了测试,转换后的std::vector

使用 lambda 内联初始化(使用 std::transform):

   std::vector<int> foo(100,42);
   const auto fooTimesTwo = [&]{
            std::vector<int> tmp(foo.size());
            std::transform(foo.begin(), foo.end(), tmp.begin(), convert);
            return tmp;
    }();

另一个 - 使用std::ranges::views::transform

   std::vector<int> foo(100,42);
   auto transform_range = (foo | std::ranges::views::transform(convert));
   std::vector<int> fooTimesTwo {
           transform_range.begin(),
           transform_range.end()
   };

我预计两种向量初始化方式应该具有相似的性能,但由于某种原因,传统 std::transform 的解决方案基准比第二种方式快得多(快 9.7 倍 -> https://quick-bench.com/q/3PSDRO9UbMNunUpdWGNShF49WlQ )。

我的问题是:

  • 我是否错误地使用了std::ranges::views::transform
  • 为什么它工作得这么慢?

旁注 - 可以使用 boost::make_transform_iterator 完成,但我无法在 quick-bench 上检查它,因为它们不支持 boost。所以我不确定这种解决方案的效率。

【问题讨论】:

  • 范围可能效率低下。但这很容易。 convert 在做什么?
  • 将一个整数乘以二。

标签: c++ c++20 std-ranges


【解决方案1】:

为什么它工作得这么慢?

您遇到的问题是 C++98/C++17 迭代器模型和 C++20 迭代器模型之间的差异之一。 X 成为前向迭代器 was 的旧要求之一:

如果X 是可变迭代器,reference 是对T 的引用;如果X 是一个常量迭代器,reference 是对const T 的引用,

也就是说,迭代器的reference 类型必须 是一个真正的引用。它不能是代理引用或纯右值。 任何reference 是纯右值的迭代器都自动仅是输入迭代器。

C++20 中没有这样的要求。

因此,如果您查看foo | std::ranges::views::transform(convert),这是prvalue int 的范围。在 C++20 迭代器模型中,这是一个随机访问范围。但在 C++17 中,因为我们处理的是纯右值,所以这只是一个输入范围

vector的iterator-pair构造函数不是基于C++20迭代器模型,而是基于C++98/C++17迭代器模型。它使用的是对迭代器类别的旧理解,而不是新的理解。并且 C++20 范围适配器非常努力地确保它们相对于旧的迭代器模型做“正确的事情”。当检查为 C++20 并检查为 C++17 时,我们调整的范围确实正确地将自己宣传为随机访问:

void f(std::vector<int> v) {
    auto r = v | std::views::transform(convert);
    using R = decltype(r);
    static_assert(std::ranges::random_access_range<R>);
    static_assert(std::same_as<std::input_iterator_tag,
        std::iterator_traits<std::ranges::iterator_t<R>>::iterator_category>);
}

那么当你将两个输入迭代器传递给vector 的迭代器对构造函数时会发生什么?好吧,它不能预先分配一个大块(我们不能在这里做last - first,因为它是一个输入迭代器,具有“大小哨兵”可能独立于遍历类别的概念也是一个新事物C++20 迭代器模型)...相反,它基本上是这样做的:

for (; first != last; ++first) {
    push_back(*first);
}

对于输入迭代器,没有比这更好的了。但这非常低效,因为我们最终会进行 8 次分配而不是 1 次。


在 range-v3 中,您可以这样做:

auto result = foo | ranges::views::transform(convert)
                  | ranges::to<std::vector>();

to 算法理解 C++20 迭代器模型,并通过在此处提前保留来做正确的事。但是,to 非常有限,因为它是一个外部库,我们不能只修改标准库类型来选择它。我们希望在 C++23 中有一个 std::ranges::to,它会随着标准库容器的改进而更好地做到这一点。此时,此解决方案将比您的原始解决方案更好,因为 std::vector&lt;int&gt; foo(tmp.size()) 本身就是浪费,必须将一块内存初始化为零,然后立即覆盖它。


与此同时,我确实想知道保留这个reference-must-be-reference 要求的一般价值(很少有人知道,甚至可能更少依赖:最大的价值可能就是知道operator-&gt;()可以返回&amp;operator*()?)。

std::vector&lt;bool&gt; 已经对此撒谎,并宣称自己是一个随机访问 C++17 范围,例如。

虽然标准库实现应该能够更好地处理这种情况。他们应该能够安全地检查 C++20 迭代器概念并因此做一些智能的事情。在这种情况下,我们有 C++20 随机访问迭代器,所以 vector 应该 能够在这种情况下有效地构造自己。已提交100070

【讨论】:

  • 如果我理解正确 - 只需做保留即可解决问题:std::vector&lt;int&gt; fooTimesTwo; fooTimesTwo.reserve(foo.size()); fooTimesTwo.assign(transform_range.begin(), transform_range.end());
  • @PiotrNycz。不,仍然超级低效,为什么这个“不起作用”还不清楚。 (quick-bench.com/q/jsxa0cw4CSrzdwkYkuVB5JLqsuU)
  • 我找到了一个等于非范围解决方案的版本:quick-bench.com/q/7bPK8ucYrfWKrBIX-Bhp0bv4uhU - 矢量 foo2 以相同的方式构造 - 而不是 assing - 简单复制。看来 push/emplace_back 的顺序是个问题
  • 看来@cigien 删除的答案是正确的——可能与“理论”相反——但它有效
  • @PiotrNycz。你能告诉我他说了什么吗?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-08-14
  • 1970-01-01
  • 1970-01-01
  • 2013-02-13
  • 1970-01-01
  • 2019-10-14
相关资源
最近更新 更多