10

我想通过转换另一个向量来初始化一个向量。我用两种内联初始化的方式进行了测试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,但我无法在快速台上检查它,因为它们不支持 boost。所以我不确定这种解决方案的效率。

4

1 回答 1

18

为什么它工作得这么慢?

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

ifX是一个可变迭代器,是对;reference的引用 TifX是一个常量迭代器,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它非常有限,因为它是一个外部库,我们不能只修改标准库类型来选择它。我们希望std::ranges::to在 C++23 中能够改进标准库容器,从而更好地做到这一点。那时,此解决方案将比您的原始解决方案更好,因为std::vector<int> foo(tmp.size())必须对一块内存进行零初始化,然后立即覆盖它,这本身就是一种浪费。


与此同时,我确实想知道保留这个reference必须参考的要求的一般价值(很少有人知道并且可能更少依赖:最大的价值可能就是知道operator->()可以返回&operator*()?)。

std::vector<bool>例如,已经对此撒谎并宣传自己是一个随机访问 C++17 范围。

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

于 2021-04-13T21:26:23.387 回答