【问题标题】:Inserting a vector transformation插入向量变换
【发布时间】:2015-02-16 22:20:02
【问题描述】:

我之前发布了question,介绍了连接两个std::vectors 的最佳方式,其中一个向量必须首先进行转换。虽然使用std::transform 的明显解决方案可能不是理论上的最佳解决方案,但多次容量检查的成本不太可能很大。

但是,如果我们考虑插入一个必须转换为另一个向量的更一般的问题,那么现在涉及到潜在的重大开销。

执行此操作的最佳方法是什么?

【问题讨论】:

    标签: c++ c++11 vector stl


    【解决方案1】:

    @VaughnCato 的approach 将boost::transform_iterator 用于您的other question 应该也适用于这个:

    auto vec1begin = boost::make_transform_iterator(vec1.begin(), f);
    auto vec1end = boost::make_transform_iterator(vec1.end(), f);
    vec2.insert(middle, vec1begin, vec1end);
    

    【讨论】:

    • 是的,我注意到了.. 我正要告诉他好消息:p
    • 我无法复制 VaughnCato 的结果(请参阅我在第一个问题中的答案,并在此问题中编辑我的答案)。
    【解决方案2】:

    我在这里看到了一些选项:

    方法一:临时变换

    std::vector<T1> temp {};
    temp.reserve(vec.size());
    std::transform(vec2.begin(), vec2.end(), std::back_inserter(temp), op);
    vec1.insert(vec1.begin() + pos, std::make_move_iterator(temp.begin()),
                   std::make_move_iterator(temp.end()));
    

    但是现在开销不是微不足道的容量检查,而是额外的成本是size_of(T1) * vec2.size() 的分配/解除分配temp。如果vec2 很大,这很容易成为一笔巨大的开销。

    方法二:循环插入

    vec1.reserve(vec1.size() + vec2.size());
    std::size_t i {pos};
    for (const auto& p : vec2) {
        vec1.insert(vec1.begin() + i, op);
        ++i;
    }
    

    这避免了方法 1 的额外分配/解除分配,但此解决方案还有另一个严重问题:vec1 中的每个 n = vec1.size() - pos 元素必须移动 n 次,O(n^2) 操作。

    方法三:移位复制

    vec1.resize(vec1.size() + pair_vec.size());
    std::move(vec1.begin() + pos, vec1.end(), vec1.begin() + pos + vec2.size());
    std::transform(vec2.begin(), vec2.end(), vec1.begin() + pos, op);
    

    这似乎与我们想要的很接近,我们只需为“额外的”n 默认构造函数付费。

    编辑

    我的移位复制方法(3)不正确,应该是:

    auto v1_size = vec1.size();
    vec1.resize(vec1.size() + vec2.size());
    std::move(vec1.begin() + pos, vec1.begin() + v1_size, vec1.begin() + pos + vec2.size());
    std::transform(vec2.begin(), vec2.end(), vec1.begin() + pos, op);
    

    测试(使用@VaughnCato 和@ViktorSehr 的方法更新)

    我测试了方法 1 和 3(方法 2 显然效果不佳 - 易于验证),以及 @VaughnCato 和 @ViktorSehr 建议的方法。完整代码如下:

    #include <iostream>
    #include <vector>
    #include <iterator>
    #include <algorithm>
    #include <cstdint>
    #include <chrono>
    #include <numeric>
    #include <random>
    #include <boost/iterator/transform_iterator.hpp>
    #include <boost/range/adaptor/transformed.hpp>
    
    using std::size_t;
    
    std::vector<int> generate_random_ints(size_t n)
    {
        std::default_random_engine generator;
        auto seed1 = std::chrono::system_clock::now().time_since_epoch().count();
        generator.seed((unsigned) seed1);
        std::uniform_int_distribution<int> uniform {};
        std::vector<int> v(n);
        std::generate_n(v.begin(), n, [&] () { return uniform(generator); });
        return v;
    }
    
    std::vector<std::string> generate_random_strings(size_t n)
    {
        std::default_random_engine generator;
        auto seed1 = std::chrono::system_clock::now().time_since_epoch().count();
        generator.seed((unsigned) seed1);
        std::uniform_int_distribution<int> uniform {};
        std::vector<std::string> v(n);
        std::generate_n(v.begin(), n, [&] () { return std::to_string(uniform(generator)); });
        return v;
    }
    
    template <typename D=std::chrono::nanoseconds, typename F>
    D benchmark(F f, unsigned num_tests)
    {
        D total {0};
        for (unsigned i = 0; i < num_tests; ++i) {
            auto start = std::chrono::system_clock::now();
            f();
            auto end = std::chrono::system_clock::now();
            total += std::chrono::duration_cast<D>(end - start);
        }
        return D {total / num_tests};
    }
    
    template <typename T1, typename T2, typename UnaryOperation>
    void temp_transform(std::vector<T1> vec1, const std::vector<T2> &vec2, size_t pos, UnaryOperation op)
    {
        std::vector<T1> temp {};
        temp.reserve(vec2.size());
        std::transform(vec2.begin(), vec2.end(), std::back_inserter(temp), op);
        vec1.insert(vec1.begin() + pos, std::make_move_iterator(temp.begin()),
                    std::make_move_iterator(temp.end()));
    }
    
    template <typename T1, typename T2, typename UnaryOperation>
    void shift_copy(std::vector<T1> vec1, const std::vector<T2> &vec2, size_t pos, UnaryOperation op)
    {
        auto v1_size = vec1.size();
        vec1.resize(vec1.size() + vec2.size());
        std::move(vec1.begin() + pos, vec1.begin() + v1_size, vec1.begin() + pos + vec2.size());
        std::transform(vec2.begin(), vec2.end(), vec1.begin() + pos, op);
    }
    
    template <typename T1, typename T2, typename UnaryOperation>
    void boost_transform(std::vector<T1> vec1, const std::vector<T2> &vec2, size_t pos, UnaryOperation op)
    {
        auto v2_begin = boost::make_transform_iterator(vec2.begin(), op);
        auto v2_end   = boost::make_transform_iterator(vec2.end(), op);
        vec1.insert(vec1.begin() + pos, v2_begin, v2_end);
    }
    
    template <typename T1, typename T2, typename UnaryOperation>
    void boost_adapter(std::vector<T1> vec1, const std::vector<T2> &vec2, size_t pos, UnaryOperation op)
    {
       auto transformed_range = vec2 | boost::adaptors::transformed(op);
       vec1.insert(vec1.begin() + pos, transformed_range.begin(), transformed_range.end());
    }
    
    int main(int argc, char **argv)
    {
        unsigned num_tests {1000};
        size_t vec1_size {1000000};
        size_t vec2_size {1000000};
        size_t insert_pos {vec1_size / 2};
    
        // Switch the variable names to inverse test
        auto vec2 = generate_random_ints(vec1_size);
        auto vec1 = generate_random_strings(vec2_size);
    
        //auto op = [] (const std::string& str) { return std::stoi(str); };
        auto op = [] (int i) { return std::to_string(i); };
    
        auto f_temp_transform_insert = [&vec1, &vec2, &insert_pos, &op] () {
            temp_transform(vec1, vec2, insert_pos, op);
        };
        auto f_shift_copy_insert = [&vec1, &vec2, &insert_pos, &op] () {
            shift_copy(vec1, vec2, insert_pos, op);
        };
        auto f_boost_transform_insert = [&vec1, &vec2, &insert_pos, &op] () {
            boost_transform(vec1, vec2, insert_pos, op);
        };
       auto f_boost_adapter_insert = [&vec1, &vec2, &insert_pos, &op] () {
           boost_adapter(vec1, vec2, insert_pos, op);
       };
    
        auto temp_transform_insert_time  = benchmark<std::chrono::milliseconds>(f_temp_transform_insert, num_tests).count();
        auto shift_copy_insert_time      = benchmark<std::chrono::milliseconds>(f_shift_copy_insert, num_tests).count();
        auto boost_transform_insert_time = benchmark<std::chrono::milliseconds>(f_boost_transform_insert, num_tests).count();
        auto boost_adapter_insert_time   = benchmark<std::chrono::milliseconds>(f_boost_adapter_insert, num_tests).count();
    
        std::cout << "temp_transform: " << temp_transform_insert_time << "ms" << std::endl;
        std::cout << "shift_copy: " << shift_copy_insert_time << "ms" << std::endl;
        std::cout << "boost_transform: " << boost_transform_insert_time << "ms" << std::endl;
        std::cout << "boost_adapter: " << boost_adapter_insert_time << "ms" << std::endl;
    
        return 0;
    }
    

    结果

    编译:

    g++ vector_insert.cpp -std=c++11 -O3 -o vector_insert_test
    

    平均用户运行时间是:

    |    Method   | std::string -> int (ms) | int -> std::string (ms) |
    |:-----------:|:-----------------------:|:-----------------------:|
    | 1           |            68           |           220           |
    | 3           |            67           |           222           |
    | @VaughnCato |            71           |           239           |
    | @ViktorSehr |            72           |           236           |
    

    TLDR

    • boost 方法不如std::transform 方法快。
    • std::transform 方法几乎同样出色 - 尽管在 int -> std::string 和 std::string -> int 性能之间存在难以解释的差异。

    【讨论】:

      【解决方案3】:

      使用 boost::range::adaptors::transformed 怎么样?

      std::vector<...> first_vector;
      auto transform_functor = [](...){...};
      auto transformed_range = first_vector | boost::adaptors::transformed(transform_functor):
      some_vector.insert(some_vector.end(), transformed_range.begin(), transformed_range.end());
      

      鉴于 boost::transform 和 vector::insert 函数实现得尽可能巧妙,这应该能够忽略任何不必要的容量检查。

      【讨论】:

      • 我似乎无法让这种方法发挥作用(请参阅我的答案中的编辑),如果你能指出我哪里出错了,我会更新结果以包含你的方法。
      • 您的方法现在正在运行...它的性能不如 STL 方法。
      • 在一个完美的世界中,它应该是最佳方式,但我猜在变换操作中的某处添加了不必要的循环,并具有变换范围。
      猜你喜欢
      • 2012-09-20
      • 2014-08-15
      • 1970-01-01
      • 1970-01-01
      • 2012-04-23
      • 2015-10-02
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多