【问题标题】:Can STL algorithms and back_inserter preallocate space?STL 算法和 back_inserter 可以预先分配空间吗?
【发布时间】:2019-03-01 04:56:58
【问题描述】:

如果我有类似的东西:

vector<int> longVector = { ... };
vector<int> newVector;
transform(longVector.begin(), longVector.end(), back_inserter(newVector),
          [] (int i) { return i * i; });

STL 是否能够在处理和添加新元素之前在newVector 中预先分配空间?我知道这不是算法的要求,但是“好的”实现能够优化它吗?或者,对于这种情况,我应该更喜欢在之前添加newVector.reserve(longVector.size()); 吗?我不一定要问每个 stdlib 实现是否存在(尽管如果有人知道具体的例子会很棒),但更多的是考虑到算法的接口和要求,它是否可能(并且是预期的)。

这个问题适用于多种 STL 算法,transformcopymovefill_n、……我想不仅适用于back_inserter,还适用于front_inserterinserter

编辑:为了清楚起见,我的意思是 stdlib 是否可以提供特定实现,例如,transform,对于输出迭代器是 back_insertervector 的情况,在这种情况下它在实际运行转换之前,将访问向量对象并保留足够的空间来存储给定迭代器对之间的distance

【问题讨论】:

  • reserve 是你的朋友。
  • back_insert_iterator 只是打电话给push_back,恕我直言。
  • 当然可以,为什么不呢?
  • @DanM。我想它将通过基于两种迭代器类型的标签调度来实现,这是零运行时成本
  • @DanM。这里的想法是在算法开始时根据迭代器之间的距离,而不是循环中的多次调用,对每个transform 进行一次调用reserve。标签调度将用于在编译时“决定”是否应该调用此reserve。不知道为什么 reserve 应该被认为是“不安全的”(或者比 push_back 更“不安全”)。

标签: c++ memory stl stdvector stl-algorithm


【解决方案1】:

这将需要在库中使用大量特殊外壳,但收益甚微。

将算法与集合分离的全部意义在于,两者都不需要了解对方,用户可以添加自己的算法来处理标准集合,或者添加新的集合来处理现有算法。

由于唯一的好处是奖励那些懒得打电话给reserve()的程序员,我觉得任何实现者都不太可能实现这样的东西。特别是因为它可能需要输入迭代器上的std::​distance() 才能工作,这进一步限制了它的使用。

还要注意,这样的实现需要在其迭代器中保留对拥有向量的引用,并且无法使用std::​vector&lt;T&gt;::​iterator 的最常见表示形式,即T*。这是所有用户都必须承担的成本,无论是否使用此新功能。

技术上可行吗?也许,在某些情况下。允许吗?我想是这样。物超所值?没有。

【讨论】:

  • 是的,当我一直在思考这个问题时,我得出了同样的结论。如果您正在处理大型集合,则无论如何都应该注意性能,并且将优化每个可能的用例的责任放在 stdlib 供应商身上是不够的。
  • 问题不是由懒惰的程序员引起的,而是由知道reserve的存在而不是调用者负责的算法引起的通用代码。但我同意这需要非常具体的专业,例如std::copy(RandomAccess, RandomAccess, std::back_inserter&lt;std::vector&lt;T&gt;&gt;)
  • 请注意,newVectorstd::back_inserter&lt;std::vector&lt;int&gt;&gt; 引用,而不是 longVector,它提供了 std::vector&lt;int&gt;::iterators,reserve 将被调用。您将创建一个特征来区分可以从保留中受益的 OutputIterators
【解决方案2】:

使用boost::transform_iterator而不是std::transformstd::back_inserter几乎可以达到1内存分配的预期效果。

但问题是因为boost::transform_iterator 不能返回对元素的引用,所以它被标记为std::input_iterator_tag。输入迭代器是单通道迭代器,与其他迭代器类别不同,当传递给std::vector 范围构造函数时,它使用push_back 填充向量。

您可以强制恢复原始迭代器类别并实现 1 个内存分配的预期效果,但需要注意的是,此类迭代器违反了双向或随机访问迭代器必须返回对元素的引用的标准要求:

#include <boost/iterator/transform_iterator.hpp>
#include <algorithm>
#include <vector>

template<class I>
struct original_category_iterator : I {
    using iterator_category = typename std::iterator_traits<typename I::base_type>::iterator_category;
    using I::I;
};

template<class I>
inline original_category_iterator<I> original_category(I i) {
    return {i};
}

int main() {
    std::vector<int> longVector = {1,2,3};
    auto f = [](auto i) { return i * i; };
    std::vector<int> newVector(original_category(boost::make_transform_iterator(longVector.begin(), f)),
                               original_category(boost::make_transform_iterator(longVector.end(), f)));
}

【讨论】:

  • std::vector 的构造函数只需要一个InputIterator。没有必要恢复任何东西。您的第一个版本运行良好。
  • @n.m. stdlibc++ 向量构造函数执行标记调度并为std::forward_iterator_tag 迭代器使用一个分配。我在第二段中解释了这一点。
  • 我不知道为什么stdlibc++ 实施策略必须对此做任何事情。标准说向量构造函数需要一个 InputIterator。为什么stdlibc++ 会为其他迭代器类别做任何特别的事情?
  • @n.m.避免多次内存分配。标准说迭代器必须是输入迭代器,你是对的。任何迭代器都是输入迭代器,除了输出迭代器。
  • 好的,它会工作,但不是一次分配。
【解决方案3】:

好消息是范围库does 保留用于随机访问迭代器容器,因此如果您愿意,可以使用它。

现在回到问题:

循环保留有问题

如果不阅读code 很难解释,但如果在循环中调用 STL 算法并且它正在执行保留,则可能会触发二次复杂度。问题是某些 STL 容器保留了所请求的确切内存量(对于小尺寸是可以理解的,但对于大尺寸的 IMAO,这是错误的行为),例如,如果当前容量为 1000 并且您调用 reserve(1005),reserve(1010 ),reserve(1010) 它将导致 3 次重新分配(意味着您每次复制约 1000 个元素以获得额外的 5 个位置)。 这是代码,有点长,但我希望你能明白:

#include<vector>
    #include<iostream>
    #include<chrono>
    int main(){
         std::vector<float> vec(10000,1.0f);
         std::vector<std::vector<float>> small_vecs(5000, std::vector<float>(50,2.0f));
         const auto t_start = std::chrono::high_resolution_clock::now();
         for(size_t i = 0; i < small_vecs.size(); i++) {
             // uncomment next line for quadratic complexity
             //vec.reserve(vec.size()+small_vecs[i].size());
             for (size_t j=0; j< small_vecs[i].size(); ++j){
                 vec.push_back(small_vecs[i][j]);
             }
         }
         const auto t_end = std::chrono::high_resolution_clock::now();
         std::cout << "runtime:" <<
             std::chrono::duration_cast<std::chrono::milliseconds>(t_end - t_start).count()
             << "ms\n";
    }

奖金:

上次我用 back_iterator 对它进行基准测试,即使使用 Reserve 也慢得可怜(以 x 衡量的速度慢,而不是 %),所以如果您关心性能,请确保在使用 back_inserter 代码时,它会针对手动循环进行基准测试。

【讨论】:

  • 基准测试很有趣。原则上,我正在考虑对transform 进行一次调用,但我知道频繁调用可能会损害这样的性能。虽然,如果我要承担 stdlib 开发人员的角色并决定实现此功能,我应该能够以比多次调用 push_back 更好的方式使用我的 reserve 实现。
【解决方案4】:

我认为这是不可能的。与适用于迭代器类别的容器和算法有明显的区别。

与 clear() 和 erase() 一样,reserve() 修改容器。引入reserve(),让算法容器感知,这违背了清晰的清晰设计。

你也可以拥有

deque<int> longDeque = { ... };
deque<int> newDeque;
transform(longDeque.begin(), longDeque.end(), back_inserter(newDeque),
          [] (int i) { return i * i; });

list<int> longList = { ... };
list<int> newList;
transform(longList.begin(), longList.end(), back_inserter(newList),
          [] (int i) { return i * i; });

和std::deque & std::list 不支持reserve(),但是代码是一样的。

最后一点: vector 没有 push_front(),因此不需要支持 front_inserter()。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-03-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多