在转换一个向量的元素的同时连接两个向量的最佳方法是什么?

时间:2015-02-16 15:46:39

标签: c++ vector stl

假设我有

std::vector<T1> vec1 {/* filled with T1's */};
std::vector<T2> vec2 {/* filled with T2's */};

和一些函数T1 f(T2)当然可以是一个lambda。在将vec1应用于vec2中的每个f时,连接T2vec2的最佳方法是什么?

显而易见的解决方案是std::transform,即

vec1.reserve(vec1.size() + vec2.size());
std::transform(vec2.begin(), vec2.end(), std::back_inserter(vec1), f);

但我说这是最佳,因为std::back_inserter必须对每个插入的元素进行不必要的容量检查。什么是最优的是像

vec1.insert(vec1.end(), vec2.begin(), vec2.end(), f);

可以通过单一容量检查逃脱。遗憾的是,这不是有效的C ++。基本上这与std::vector::insert最适合矢量连接的原因相同,请参阅this问题以及this中的注释,以便进一步讨论这一点。

所以:

  1. std::transform是使用STL的最佳方法吗?
  2. 如果是这样,我们可以做得更好吗?
  3. 上面描述的insert函数是否被排除在STL之外有充分的理由吗?
  4. 更新

    我已经开始验证多个容量检查是否确实有任何明显的成本。为此,我基本上只将id函数(f(x) = x)传递给答案中讨论的std::transformpush_back方法。完整的代码是:

    #include <iostream>
    #include <vector>
    #include <iterator>
    #include <algorithm>
    #include <cstdint>
    #include <chrono>
    #include <numeric>
    #include <random>
    
    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;
    }
    
    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 T>
    void std_insert(std::vector<T> vec1, const std::vector<T> &vec2)
    {
        vec1.insert(vec1.end(), vec2.begin(), vec2.end());
    }
    
    template <typename T1, typename T2, typename UnaryOperation>
    void push_back_concat(std::vector<T1> vec1, const std::vector<T2> &vec2, UnaryOperation op)
    {
        vec1.reserve(vec1.size() + vec2.size());
        for (const auto& x : vec2) {
            vec1.push_back(op(x));
        }
    }
    
    template <typename T1, typename T2, typename UnaryOperation>
    void transform_concat(std::vector<T1> vec1, const std::vector<T2> &vec2, UnaryOperation op)
    {
        vec1.reserve(vec1.size() + vec2.size());
        std::transform(vec2.begin(), vec2.end(), std::back_inserter(vec1), op);
    }
    
    int main(int argc, char **argv)
    {
        unsigned num_tests {1000};
        size_t vec1_size {10000000};
        size_t vec2_size {10000000};
    
        auto vec1 = generate_random_ints(vec1_size);
        auto vec2 = generate_random_ints(vec1_size);
    
        auto f_std_insert = [&vec1, &vec2] () {
            std_insert(vec1, vec2);
        };
        auto f_push_back_id = [&vec1, &vec2] () {
            push_back_concat(vec1, vec2, [] (int i) { return i; });
        };
        auto f_transform_id = [&vec1, &vec2] () {
            transform_concat(vec1, vec2, [] (int i) { return i; });
        };
    
        auto std_insert_time   = benchmark<std::chrono::milliseconds>(f_std_insert, num_tests).count();
        auto push_back_id_time = benchmark<std::chrono::milliseconds>(f_push_back_id, num_tests).count();
        auto transform_id_time = benchmark<std::chrono::milliseconds>(f_transform_id, num_tests).count();
    
        std::cout << "std_insert: " << std_insert_time << "ms" << std::endl;
        std::cout << "push_back_id: " << push_back_id_time << "ms" << std::endl;
        std::cout << "transform_id: " << transform_id_time << "ms" << std::endl;
    
        return 0;
    }
    

    编译:

    g++ vector_insert_demo.cpp -std=c++11 -O3 -o vector_insert_demo
    

    输出:

    std_insert: 44ms
    push_back_id: 61ms
    transform_id: 61ms
    

    编译器将内联lambda,因此可以安全地降低成本。除非其他人对这些结果有可行的解释(或者愿意检查程序集),否则我认为可以合理地断定多次容量检查有明显的成本。

3 个答案:

答案 0 :(得分:2)

更新:性能差异是由reserve()调用造成的,至少在libstdc ++中,使得容量完全符合您的要求,而不是使用指数增长因子。


我做了一些时间测试,结果很有趣。使用std::vector::insertboost::transform_iterator是我发现的最快方式:

版本1:

void
  appendTransformed1(
    std::vector<int> &vec1,
    const std::vector<float> &vec2
  )
{
  auto v2begin = boost::make_transform_iterator(vec2.begin(),f);
  auto v2end   = boost::make_transform_iterator(vec2.end(),f);
  vec1.insert(vec1.end(),v2begin,v2end);
}

第2版:

void
  appendTransformed2(
    std::vector<int> &vec1,
    const std::vector<float> &vec2
  )
{
  vec1.reserve(vec1.size()+vec2.size());
  for (auto x : vec2) {
    vec1.push_back(f(x));
  }
}

第3版:

void
  appendTransformed3(
    std::vector<int> &vec1,
    const std::vector<float> &vec2
  )
{
  vec1.reserve(vec1.size()+vec2.size());
  std::transform(vec2.begin(),vec2.end(),std::inserter(vec1,vec1.end()),f);
}

定时:

    Version 1: 0.59s
    Version 2: 8.22s
    Version 3: 8.42s

main.cpp中:

#include <algorithm>
#include <cassert>
#include <chrono>
#include <iterator>
#include <iostream>
#include <random>
#include <vector>
#include "appendtransformed.hpp"

using std::cerr;

template <typename Engine>
static std::vector<int> randomInts(Engine &engine,size_t n)
{
  auto distribution = std::uniform_int_distribution<int>(0,999);
  auto generator = [&]{return distribution(engine);};
  auto vec = std::vector<int>();
  std::generate_n(std::inserter(vec,vec.end()),n,generator);
  return vec;
}

template <typename Engine>
static std::vector<float> randomFloats(Engine &engine,size_t n)
{
  auto distribution = std::uniform_real_distribution<float>(0,1000);
  auto generator = [&]{return distribution(engine);};
  auto vec = std::vector<float>();
  std::generate_n(std::inserter(vec,vec.end()),n,generator);
  return vec;
}

static auto
  appendTransformedFunction(int version) ->
    void(*)(std::vector<int>&,const std::vector<float> &)
{
  switch (version) {
    case 1: return appendTransformed1;
    case 2: return appendTransformed2;
    case 3: return appendTransformed3;
    default:
      cerr << "Unknown version: " << version << "\n";
      exit(EXIT_FAILURE);
  }

  return 0;
}

int main(int argc,char **argv)
{
  if (argc!=2) {
    cerr << "Usage: appendtest (1|2|3)\n";
    exit(EXIT_FAILURE);
  }
  auto version = atoi(argv[1]);
  auto engine = std::default_random_engine();
  auto vec1_size = 1000000u;
  auto vec2_size = 1000000u;
  auto count = 100;
  auto vec1 = randomInts(engine,vec1_size);
  auto vec2 = randomFloats(engine,vec2_size);
  namespace chrono = std::chrono;
  using chrono::system_clock;
  auto appendTransformed = appendTransformedFunction(version);
  auto start_time = system_clock::now();
  for (auto i=0; i!=count; ++i) {
    appendTransformed(vec1,vec2);
  }
  auto end_time = system_clock::now();
  assert(vec1.size() == vec1_size+count*vec2_size);
  auto sum = std::accumulate(vec1.begin(),vec1.end(),0u);
  auto elapsed_seconds = chrono::duration<float>(end_time-start_time).count();

  cerr << "Using version " << version << ":\n";
  cerr << "  sum=" << sum << "\n";
  cerr << "  elapsed: " << elapsed_seconds << "s\n";
}

编译器:g ++ 4.9.1

选项:-std = c ++ 11 -O2

答案 1 :(得分:0)

  
      
  1. std ::使用STL转换最佳方法吗?
  2.   

我不能这么说。如果保留空间,差异应该是短暂的,因为检查可能由编译器或CPU优化。找出答案的唯一方法是衡量你的真实代码 如果您没有特殊需求,则应选择std::transform

  
      
  1. 如果是这样,我们可以做得更好吗?
  2.   

你想拥有什么:

  • 减少长度检查
  • push&#39; n _ back
  • 时利用移动语义

如果需要,您可能还想创建二进制函数。

template <typename InputIt, typename OutputIt, typename UnaryCallable>
void move_append(InputIt first, InputIt last, OutputIt firstOut, OutputIt lastOut, UnaryCallable fn)
{
       if (std::distance(first, last) < std::distance(firstOut, lastOut)
           return;

       while (first != last && firstOut != lastOut) {
              *firstOut++ = std::move( fn(*first++) );

       }
 }

电话可能是:

std::vector<T1> vec1 {/* filled with T1's */};
std::vector<T2> vec2 {/* filled with T2's */};
// ...
vec1.resize( vec1.size() + vec2.size() );
move_append( vec1.begin(), vec1.end(), vec2.begin(), vec2.end(), f );

我不确定您是否可以使用普通algorithm来执行此操作,因为back_inserter会调用Container::push_back,这会在任何情况下检查是否重新分配。此外,该元素无法从移动语义中受益。

注意:安全检查取决于您的使用情况,具体取决于您传递要追加的元素的方式。它也应该返回bool

一些测量here。我无法解释这种巨大的差异。

答案 2 :(得分:0)

我得到的结果与@VaughnCato不同 - 尽管我对std::stringint的测试略有不同。根据我的测试,push_backstd::transform方法同样出色,而boost::transform方法稍差。这是我的完整代码:

<强>更新

我添加了另一个测试用例,而不是使用reserveback_inserter,只使用resize。这基本上与@ black的答案中的方法相同,也是@ChrisDrew在问题评论中提出的方法。我还进行了“双向”测试std::string - &gt; intint - &gt; std::string

#include <iostream>
#include <vector>
#include <iterator>
#include <algorithm>
#include <cstdint>
#include <chrono>
#include <numeric>
#include <random>
#include <boost/iterator/transform_iterator.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 push_back_concat(std::vector<T1> vec1, const std::vector<T2> &vec2, UnaryOperation op)
{
    vec1.reserve(vec1.size() + vec2.size());
    for (const auto& x : vec2) {
        vec1.push_back(op(x));
    }
}

template <typename T1, typename T2, typename UnaryOperation>
void transform_concat_reserve(std::vector<T1> vec1, const std::vector<T2> &vec2, UnaryOperation op)
{
    vec1.reserve(vec1.size() + vec2.size());
    std::transform(vec2.begin(), vec2.end(), std::back_inserter(vec1), op);
}

template <typename T1, typename T2, typename UnaryOperation>
void transform_concat_resize(std::vector<T1> vec1, const std::vector<T2> &vec2, UnaryOperation op)
{
    auto vec1_size = vec1.size();
    vec1.resize(vec1.size() + vec2.size());
    std::transform(vec2.begin(), vec2.end(), vec1.begin() + vec1_size, op);
}

template <typename T1, typename T2, typename UnaryOperation>
void boost_transform_concat(std::vector<T1> vec1, const std::vector<T2> &vec2, 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.end(), v2_begin, v2_end);
}

int main(int argc, char **argv)
{
    unsigned num_tests {1000};
    size_t vec1_size {1000000};
    size_t vec2_size {1000000};

    // Switch the variable names to inverse test
    auto vec1 = generate_random_ints(vec1_size);
    auto vec2 = 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_push_back_concat = [&vec1, &vec2, &op] () {
        push_back_concat(vec1, vec2, op);
    };
    auto f_transform_concat_reserve = [&vec1, &vec2, &op] () {
        transform_concat_reserve(vec1, vec2, op);
    };
    auto f_transform_concat_resize = [&vec1, &vec2, &op] () {
        transform_concat_resize(vec1, vec2, op);
    };
    auto f_boost_transform_concat = [&vec1, &vec2, &op] () {
        boost_transform_concat(vec1, vec2, op);
    };

    auto push_back_concat_time = benchmark<std::chrono::milliseconds>(f_push_back_concat, num_tests).count();
    auto transform_concat_reserve_time = benchmark<std::chrono::milliseconds>(f_transform_concat_reserve, num_tests).count();
    auto transform_concat_resize_time = benchmark<std::chrono::milliseconds>(f_transform_concat_resize, num_tests).count();
    auto boost_transform_concat_time = benchmark<std::chrono::milliseconds>(f_boost_transform_concat, num_tests).count();

    std::cout << "push_back: " << push_back_concat_time << "ms" << std::endl;
    std::cout << "transform_reserve: " << transform_concat_reserve_time << "ms" << std::endl;
    std::cout << "transform_resize: " << transform_concat_resize_time << "ms" << std::endl;
    std::cout << "boost_transform: " << boost_transform_concat_time << "ms" << std::endl;

    return 0;
}

使用编译:

g++ vector_concat.cpp -std=c++11 -O3 -o vector_concat_test

结果(平均用户时间)为:

|          Method          | std::string -> int (ms) | int -> std::string (ms) |
|:------------------------:|:-----------------------:|:-----------------------:|
| push_back                |            68           |           206           |
| std::transform (reserve) |            68           |           202           |
| std::transform (resize)  |            67           |           218           |
| boost::transform         |            70           |           238           |

临时结论

  • 使用std::transform的{​​{1}}方法对于普通的默认构造类型可能是最优的(使用STL)。
  • 使用resizestd::transform的{​​{1}}方法最有可能是我们无法做到的最佳方式。