【问题标题】:Fast copy of `std::vector<std::uint8_t>``std::vector<std::uint8_t>` 的快速复制
【发布时间】:2013-04-12 06:24:03
【问题描述】:

我有一个std::vector&lt;std::uint8_t&gt;,需要复制。这只需调用复制构造函数即可完成。

我的分析结果显示,Microsoft Visual C++ (msvc100) 实现在内部使用std::uninitialized_copy。这将一个接一个地复制每个元素。在这种情况下,可以通过一次复制整个内存块来完成更优化的复制(就像memcpy 一样)。

换句话说,这可能是一个重要的优化。有没有办法强制向量使用这种优化的方法?

注意:我尝试过使用std::basic_string&lt;std::uint8_t&gt;,它的性能确实更好,但它还有其他问题。

【问题讨论】:

  • 你试过普通的std::copy吗?
  • 您是否使用优化的构建进行了测试?
  • 为什么不使用 std::copy?
  • 没有人会错过,we're discussing this in chat right now
  • @Rapptz: std::copy 确实表现得更好。我原以为它会和复制构造函数一样,但显然它没有。

标签: c++ performance optimization stdvector memcpy


【解决方案1】:

此答案并非特定于 msvc100。

如果你使用像 in 这样的复制构造函数

std::vector<uint8_t> newVect(otherVect);

otherVect 的分配器对象也必须被复制(和使用),这需要更多的努力才能使其在 STL 实现中发挥作用。

如果你只想复制otherVect的内容,使用

std::vector<uint8_t> newVect(otherVect.begin(), otherVect.end());

它使用 newVect 的默认分配器。

另一种可能是

std::vector<uint8_t> newVect; nevVect.assign(otherVect.begin(), otherVect.end());

在这种情况下,所有这些(包括当 otherVect 使用默认分配器时的复制构造函数)都应该归结为一个好的 STL 实现中的 memmove/memcpy。请注意,otherVect 与 newVect 具有完全相同的元素类型(例如 'char' 或 'int8_t')。

使用容器的方法通常比使用泛型算法更高效,因此结合使用 vector::resize() 和 std::copy() 甚至 memmove()/memcpy() 将是一种解决方法,如果供应商没有充分优化容器。

【讨论】:

  • memmove?!我猜你的意思是memcpy。我讨厌向量的副本(它不是右值引用)导致初始数据丢失。
  • 为什么memmove会导致初始数据丢失?
  • @jcoder:我认为无法保证原始数据会被保留。我还认为 memmove 可能会通过操作地址转换表来移动页面大小的块。但是手册页确实提到了副本,所以看来我错了。尽管如此,memmove 必须确保操作方向正确,memcpy 没有,所以后者应该更快。
  • 是的,唯一真正的区别是 memmove 在 from 和 to 范围重叠时具有更可预测的特征。现在我想这个名字是相当误导:)
  • 我原来用的是std::vector&lt;uint8_t&gt; newVect(otherVect.begin(), otherVect.end());,但是和std::vector&lt;uint8_t&gt; newVect(otherVect);一样快。 std::copy 明显更快(在我的机器上大约快 40%)。
【解决方案2】:

根据建议的解决方案,我决定整理一个小型基准。

#include <cstdint>
#include <cstring>
#include <ctime>
#include <iostream>
#include <random>
#include <vector>

using namespace std;

int main()
{
  random_device seed;
  mt19937 rnd(seed());
  uniform_int_distribution<uint8_t> random_byte(0x00, 0xff);

  const size_t n = 512 * 512;

  vector<uint8_t> source;
  source.reserve(n);
  for (size_t i = 0; i < n; i++) source.push_back(random_byte(rnd));

  clock_t start;
  clock_t t_constructor1 = 0; uint8_t c_constructor1 = 0;
  clock_t t_constructor2 = 0; uint8_t c_constructor2 = 0;
  clock_t t_assign = 0;       uint8_t c_assign = 0;
  clock_t t_copy = 0;         uint8_t c_copy = 0;
  clock_t t_memcpy = 0;       uint8_t c_memcpy = 0;

  for (size_t k = 0; k < 4; k++)
  {
    start = clock();
    for (size_t i = 0; i < n/32; i++)
    {
      vector<uint8_t> destination(source);
      c_constructor1 += destination[i];
    }
    t_constructor1 += clock() - start;

    start = clock();
    for (size_t i = 0; i < n/32; i++)
    {
      vector<uint8_t> destination(source.begin(), source.end());
      c_constructor2 += destination[i];
    }
    t_constructor2 += clock() - start;

    start = clock();
    for (size_t i = 0; i < n/32; i++)
    {
      vector<uint8_t> destination;
      destination.assign(source.begin(), source.end());
      c_assign += destination[i];
    }
    t_assign += clock() - start;

    start = clock();
    for (size_t i = 0; i < n/32; i++)
    {
      vector<uint8_t> destination(source.size());
      copy(source.begin(), source.end(), destination.begin());
      c_copy += destination[i];
    }
    t_copy += clock() - start;

    start = clock();
    for (size_t i = 0; i < n/32; i++)
    {
      vector<uint8_t> destination(source.size());
      memcpy(&destination[0], &source[0], n);
      c_memcpy += destination[i];
    }
    t_memcpy += clock() - start;
  }

  // Verify that all copies are correct, but also prevent the compiler
  // from optimising away the loops
  uint8_t diff = (c_constructor1 - c_constructor2) +
                 (c_assign - c_copy) +
                 (c_memcpy - c_constructor1);

  if (diff != 0) cout << "one of the methods produces invalid copies" << endl;

  cout << "constructor (1): "    << t_constructor1 << endl;
  cout << "constructor (2): "    << t_constructor2 << endl;
  cout << "assign:          "    << t_assign << endl;
  cout << "copy             "    << t_copy << endl;
  cout << "memcpy           "    << t_memcpy << endl;

  return 0;
}

在我的 PC 上,使用 msvc100 为 x64 编译,完全优化,这会产生以下输出:

constructor (1): 22388
constructor (2): 22333
assign:          22381
copy             2142
memcpy           2146

结果很清楚:std::copy 的性能与std::memcpy 一样好,而构造函数和assign 的速度都慢了一个数量级。当然,确切的数字和比例取决于向量的大小,但 msvc100 的结论是显而易见的:作为suggested by Rapptz,使用std::copy

编辑:结论对于其他编译器并不明显。我也在 64 位 Linux 上进行了测试,Clang 3.2 的结果如下

constructor (1): 530000
constructor (2): 560000
assign:          560000
copy             840000
memcpy           860000

GCC 4.8 给出了类似的输出。对于 Windows 上的 GCC,memcpycopy 比构造函数和 assign 稍慢,尽管差异较小。但是,我的经验是 GCC 在 Windows 上的优化并不好。我也测试了msvc110,结果和msvc100差不多。

【讨论】:

  • 我在 Linux/64bit 下用 gcc 4.6.3 测量得到构造函数(1):530000,构造函数(2):530000,赋值:550000,复制 830000,memcpy 840000(不介意更大的值,CLOCKS_PER_SEC 可能不同)。所以完全相反。如果您的代码不是可移植的,那么使用复制肯定是一个很好的解决方法。
  • 太棒了!我用VS2012Express检查了这个,它基本上是一样的。不知何故,我称之为实现错误。
  • 对,没有优化!很久以前,我在 1:02:50 看了下面的视频,相信一切都已经优化好了...channel9.msdn.com/Series/…
猜你喜欢
  • 2020-12-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-12-28
  • 1970-01-01
  • 2011-02-23
  • 2011-05-19
  • 1970-01-01
相关资源
最近更新 更多