【问题标题】:Is it better to use std::memcpy() or std::copy() in terms to performance?就性能而言,使用 std::memcpy() 或 std::copy() 更好吗?
【发布时间】:2011-06-10 01:42:49
【问题描述】:

在性能方面是使用memcpy 更好还是使用std::copy() 更好?为什么?

char *bits = NULL;
...

bits = new (std::nothrow) char[((int *) copyMe->bits)[0]];
if (bits == NULL)
{
    cout << "ERROR Not enough memory.\n";
    exit(1);
}

memcpy (bits, copyMe->bits, ((int *) copyMe->bits)[0]);

【问题讨论】:

  • 请注意,char 可以签名或未签名,具体取决于实现。如果字节数可以 >= 128,则将 unsigned char 用于您的字节数组。 ((int *) 演员也比(unsigned int *) 更安全。)
  • 你为什么不用std::vector&lt;char&gt;?或者既然你说bitsstd::bitset
  • 其实,你能解释一下(int*) copyMe-&gt;bits[0]是做什么的吗?
  • 不知道为什么在提供如此少的重要上下文的情况下看起来如此混乱的东西是在 +81,但是,嘿。 @user3728501 我的猜测是缓冲区的开头包含一个 int 指示其大小,但这似乎是实现定义的灾难的秘诀,就像这里的许多其他事情一样。
  • 事实上,(int *) 转换只是纯粹的未定义行为,而不是实现定义的。试图通过强制转换进行类型双关语违反了严格的别名规则,因此标准完全未定义。 (此外,在 C++ 中,虽然不是 C,但您也不能通过 union 进行类型双关。)几乎唯一的例外是如果您将 转换为 char* 的变体,但余量不是对称的。

标签: c++ performance optimization


【解决方案1】:

我将违背普遍的看法,即std::copy 将有轻微的、几乎无法察觉的性能损失。我刚刚做了一个测试,发现这不是真的:我确实注意到了性能差异。但是,获胜者是std::copy

我编写了一个 C++ SHA-2 实现。在我的测试中,我使用所有四个 SHA-2 版本(224、256、384、512)对 5 个字符串进行哈希处理,并且循环了 300 次。我使用 Boost.timer 测量时间。那 300 个循环计数器足以完全稳定我的结果。我每次运行测试 5 次,在 memcpy 版本和 std::copy 版本之间交替进行。我的代码利用尽可能大的数据块来获取数据(许多其他实现使用char/char *,而我使用T/T *(其中T是最大的类型)具有正确溢出行为的用户实现),因此对最大类型的快速内存访问是我算法性能的核心。这些是我的结果:

完成运行 SHA-2 测试的时间(以秒为单位)

std::copy   memcpy  % increase
6.11        6.29    2.86%
6.09        6.28    3.03%
6.10        6.29    3.02%
6.08        6.27    3.03%
6.08        6.27    3.03%

std::copy 相对于 memcpy 的总平均速度提升:2.99%

我的编译器是 Fedora 16 x86_64 上的 gcc 4.6.3。我的优化标志是-Ofast -march=native -funsafe-loop-optimizations

Code for my SHA-2 implementations.

我决定也对我的 MD5 实现进行测试。结果不太稳定,所以我决定运行 10 次。然而,在我最初的几次尝试之后,我得到的结果从一次运行到下一次变化很大,所以我猜测有某种操作系统活动正在进行。我决定重新开始。

相同的编译器设置和标志。 MD5 只有一个版本,它比 SHA-2 更快,所以我在一组类似的 5 个测试字符串上进行了 3000 次循环。

这是我最后的 10 个结果:

完成运行 MD5 测试的时间(以秒为单位)

std::copy   memcpy      % difference
5.52        5.56        +0.72%
5.56        5.55        -0.18%
5.57        5.53        -0.72%
5.57        5.52        -0.91%
5.56        5.57        +0.18%
5.56        5.57        +0.18%
5.56        5.53        -0.54%
5.53        5.57        +0.72%
5.59        5.57        -0.36%
5.57        5.56        -0.18%

std::copy 在 memcpy 上的总平均速度下降:0.11%

Code for my MD5 implementation

这些结果表明 std::copy 在我的 SHA-2 测试中使用了一些优化,std::copy 在我的 MD5 测试中无法使用。在 SHA-2 测试中,两个数组都是在名为 std::copy / memcpy 的同一函数中创建的。在我的 MD5 测试中,其中一个数组作为函数参数传递给函数。

我做了更多的测试,看看我可以做些什么来让std::copy 再次变得更快。答案很简单:开启链接时间优化。这些是我打开 LTO 的结果(gcc 中的选项 -flto):

使用 -flto 完成运行 MD5 测试的时间(以秒为单位)

std::copy   memcpy      % difference
5.54        5.57        +0.54%
5.50        5.53        +0.54%
5.54        5.58        +0.72%
5.50        5.57        +1.26%
5.54        5.58        +0.72%
5.54        5.57        +0.54%
5.54        5.56        +0.36%
5.54        5.58        +0.72%
5.51        5.58        +1.25%
5.54        5.57        +0.54%

std::copy 相对于 memcpy 的总平均速度提升:0.72%

总之,使用std::copy 似乎没有性能损失。事实上,似乎有性能提升。

结果说明

那么为什么std::copy 会提升性能呢?

首先,只要打开内联优化,我不希望它对任何实现都变慢。所有编译器都积极内联;它可能是最重要的优化,因为它支持许多其他优化。 std::copy 可以(我怀疑所有现实世界的实现都可以)检测到参数是可简单复制的,并且内存是按顺序排列的。这意味着在最坏的情况下,当memcpy 合法时,std::copy 的性能应该不会变差。遵循 memcpystd::copy 的简单实现应该满足编译器的“在优化速度或大小时始终内联它”的标准。

不过,std::copy 也会保留更多信息。当您调用std::copy 时,该函数会保持类型不变。 memcpyvoid * 上运行,它丢弃了几乎所有有用的信息。例如,如果我传入std::uint64_t 的数组,编译器或库实现者可能能够利用std::copy 的64 位对齐,但使用memcpy 可能更难做到这一点。像这样的算法的许多实现首先处理范围开头的未对齐部分,然后是对齐的部分,然后是末尾的未对齐部分。如果保证全部对齐,那么代码会变得更简单、更快,并且更容易让处理器中的分支预测器正确。

过早的优化?

std::copy 处于一个有趣的位置。我希望它永远不会比memcpy 慢,并且有时使用任何现代优化编译器都会更快。此外,你可以memcpy,你可以std::copymemcpy 不允许缓冲区中有任何重叠,而 std::copy 支持一个方向上的重叠(std::copy_backward 支持另一个方向的重叠)。 memcpy 仅适用于指针,std::copy 适用于任何迭代器(std::mapstd::vectorstd::deque 或我自己的自定义类型)。换句话说,当您需要复制大量数据时,您应该只使用std::copy

【讨论】:

  • 我要强调的是,这并不意味着std::copymemcpy 快2.99% 或0.72% 或-0.11%,这些时间是为整个程序执行的。但是,我通常认为真实代码中的基准比假代码中的基准更有用。我的整个程序的执行速度都发生了变化。单独来看,这两种复制方案的实际效果将比此处显示的差异更大,但这表明它们在实际代码中可以有可测量的差异。
  • 我不同意你的发现,但结果就是结果:/。但是有一个问题(我知道那是很久以前的事了,你不记得研究了,所以只是按照你的想法评论),你可能没有研究汇编代码;
  • 在我看来 memcpystd::copy 有不同的实现,所以在某些情况下编译器会将周围的代码和实际的内存复制代码优化为一个完整的代码。换句话说有时一个比另一个更好,甚至换句话说,决定使用哪个是不成熟的甚至是愚蠢的优化,因为在每种情况下你都必须进行新的研究,更重要的是,程序通常正在开发中,因此在进行一些小的更改后,可能会失去功能优于其他功能的优势。
  • @ST3:我想在最坏的情况下,std::copy 是一个微不足道的内联函数,它在合法时只调用memcpy。基本内联将消除任何负面的性能差异。我将更新帖子,解释一下为什么 std::copy 可能会更快。
  • 非常有用的分析。 Re std::copy 的速度在 memcpy 上的总平均下降:0.11%,虽然数字是正确的,但结果在统计上并不显着。均值差异的 95% 置信区间为 (-0.013s, 0.025),其中包括零。正如您所指出的,其他来源和您的数据存在差异,您可能会说性能是相同的。作为参考,另外两个结果具有统计显着性 - 您偶然看到这种极端情况的时间差异的可能性约为 1 亿分之一(第一次)和 2 万分之一(最后一次)。
【解决方案2】:

我知道的所有编译器都会在适当的时候将简单的std::copy 替换为memcpy,或者甚至更好地对副本进行矢量化,使其比memcpy 更快。

无论如何:分析并找出自己。不同的编译器会做不同的事情,而且很可能不会完全按照你的要求做。

参见this presentation on compiler optimisations (pdf)。

这里是 what GCC does,用于 POD 类型的简单 std::copy

#include <algorithm>

struct foo
{
  int x, y;    
};

void bar(foo* a, foo* b, size_t n)
{
  std::copy(a, a + n, b);
}

这是反汇编(只有-O 优化),显示对memmove 的调用:

bar(foo*, foo*, unsigned long):
    salq    $3, %rdx
    sarq    $3, %rdx
    testq   %rdx, %rdx
    je  .L5
    subq    $8, %rsp
    movq    %rsi, %rax
    salq    $3, %rdx
    movq    %rdi, %rsi
    movq    %rax, %rdi
    call    memmove
    addq    $8, %rsp
.L5:
    rep
    ret

如果将函数签名更改为

void bar(foo* __restrict a, foo* __restrict b, size_t n)

然后memmove 变为memcpy 以稍微提高性能。请注意,memcpy 本身将被大量矢量化。

【讨论】:

  • 如何进行分析。使用什么工具(在 windows 和 linux 中)?
  • @Konrad,你是对的。但是memmove 不应该更快——相反,它应该稍微慢一些,因为它必须考虑到两个数据范围重叠的可能性。我认为std::copy 允许重叠数据,因此它必须调用memmove
  • @Konrad:如果 memmove 总是比 memcpy 快,那么 memcpy 会调用 memmove。 std::copy 实际上可能发送到的(如果有的话)是实现定义的,因此在不提及实现的情况下提及细节是没有用的。
  • 虽然,一个重现此行为的简单程序,在 GCC 下使用 -O3 编译,显示memcpy。这让我相信 GCC 会检查是否存在内存重叠。
  • @Konrad:标准std::copy 允许在一个方向上重叠,但不允许在另一个方向上重叠。输出的开头不能位于输入范围内,但允许输入的开头位于输出范围内。这有点奇怪,因为定义了分配的顺序,并且调用可能是 UB,即使按该顺序定义了这些分配的效果。但我想这个限制允许向量化优化。
【解决方案3】:

始终使用 std::copy,因为 memcpy 仅限于 C 样式 POD 结构,如果目标实际上是 POD,编译器可能会将对 std::copy 的调用替换为 memcpy

另外,std::copy 可以与许多迭代器类型一起使用,而不仅仅是指针。 std::copy 更灵活,不会造成性能损失,是明显的赢家。

【讨论】:

  • 为什么要复制迭代器?
  • 您不是在复制迭代器,而是在复制由两个迭代器定义的范围。例如,std::copy(container.begin(), container.end(), destination); 会将container 的内容(beginend 之间的所有内容)复制到destination 指示的缓冲区中。 std::copy 不需要像 &amp;*container.begin()&amp;container.back() + 1 这样的恶作剧。
【解决方案4】:

理论上,memcpy 可能具有轻微难以察觉无穷小的性能优势,只是因为它没有与std::copy 相同的要求。来自memcpy的手册页:

为避免溢出, 两个目标指向的数组 和源参数,应在 最少 num 个字节,并且不应该 重叠(用于重叠记忆 块,memmove 是一种更安全的方法)。

换句话说,memcpy 可以忽略重叠数据的可能性。 (将重叠数组传递给memcpy 是未定义的行为。)因此memcpy 不需要显式检查此条件,而只要OutputIterator 参数不在源范围内,就可以使用std::copy。请注意,这与说源范围和目标范围不能重叠。

所以由于std::copy 有一些不同的要求,理论上它应该稍微(特别强调稍微)慢一些,因为它可能会检查重叠C 数组,或者将 C 数组的复制委托给需要执行检查的memmove。但在实践中,您(和大多数分析器)可能甚至不会检测到任何差异。

当然,如果您不使用PODs,则不能无论如何使用memcpy

【讨论】:

  • std::copy&lt;char&gt; 是这样。但是std::copy&lt;int&gt; 可以假设它的输入是整数对齐的。这将产生更大的差异,因为它会影响每个元素。重叠是一次性检查。
  • @MSalters,是的,但memcpy 的大多数实现我见过检查对齐并尝试复制单词而不是逐字节复制。
  • std::copy() 也可以忽略重叠内存。如果你想支持重叠内存,你必须自己编写逻辑在适当的情况下调用std::reverse_copy()。
  • 可以提出相反的论点:当通过memcpy 接口时,它会丢失对齐信息。因此,memcpy 必须在运行时进行对齐检查以处理未对齐的开头和结尾。这些支票可能很便宜,但它们不是免费的。而std::copy 可以避免这些检查和矢量化。此外,编译器可能会证明源数组和目标数组不重叠并再次向量化,而无需用户在memcpymemmove 之间进行选择。
【解决方案5】:

我的规则很简单。如果你使用 C++ 更喜欢 C++ 库而不是 C :)

【讨论】:

  • C++ 被明确设计为允许使用 C 库。这不是意外。在 C++ 中使用 std::copy 通常比使用 memcpy 更好,但这与哪个是 C 无关,而且这种说法通常是错误的方法。
  • @FredNurk 通常你想避免 C++ 提供更安全替代方案的 C 薄弱环节。
  • @Phil1970 我不确定在这种情况下 C++ 是否更安全。我们仍然必须传递不会溢出的有效迭代器等。我能够使用std::end(c_arr) 而不是c_arr + i_hope_this_is_the_right_number_of elements 更安全?也许更重要的是,更清晰。这就是我在这个特定情况下强调的一点:std::copy() 更惯用,如果迭代器的类型稍后更改,则更易于维护,导致更清晰的语法等。
  • @underscore_d std::copy 更安全,因为它可以正确复制传递的数据,以防它们不是 POD 类型。 memcpy 会愉快地将std::string 对象逐字节复制到新的表示中。
【解决方案6】:

只是一个小补充:memcpy()std::copy() 之间的速度差异可能会有很大差异,具体取决于启用或禁用优化。使用 g++ 6.2.0 并且没有优化 memcpy() 显然胜出:

Benchmark             Time           CPU Iterations
---------------------------------------------------
bm_memcpy            17 ns         17 ns   40867738
bm_stdcopy           62 ns         62 ns   11176219
bm_stdcopy_n         72 ns         72 ns    9481749

启用优化后 (-O3),一切看起来都差不多了:

Benchmark             Time           CPU Iterations
---------------------------------------------------
bm_memcpy             3 ns          3 ns  274527617
bm_stdcopy            3 ns          3 ns  272663990
bm_stdcopy_n          3 ns          3 ns  274732792

数组越大,效果越不明显,但即使在N=1000 memcpy() 上,在未启用优化时也快两倍。

源代码(需要 Google Benchmark):

#include <string.h>
#include <algorithm>
#include <vector>
#include <benchmark/benchmark.h>

constexpr int N = 10;

void bm_memcpy(benchmark::State& state)
{
  std::vector<int> a(N);
  std::vector<int> r(N);

  while (state.KeepRunning())
  {
    memcpy(r.data(), a.data(), N * sizeof(int));
  }
}

void bm_stdcopy(benchmark::State& state)
{
  std::vector<int> a(N);
  std::vector<int> r(N);

  while (state.KeepRunning())
  {
    std::copy(a.begin(), a.end(), r.begin());
  }
}

void bm_stdcopy_n(benchmark::State& state)
{
  std::vector<int> a(N);
  std::vector<int> r(N);

  while (state.KeepRunning())
  {
    std::copy_n(a.begin(), N, r.begin());
  }
}

BENCHMARK(bm_memcpy);
BENCHMARK(bm_stdcopy);
BENCHMARK(bm_stdcopy_n);

BENCHMARK_MAIN()

/* EOF */

【讨论】:

  • 在禁用优化的情况下测量性能......嗯......几乎毫无意义......如果你对性能感兴趣,你不会在没有优化的情况下编译。
  • @bolov 并非总是如此。在某些情况下,拥有一个相对快速的调试程序很重要。
  • @bolov 我以前也这么认为,但实际上在调试模式下运行的游戏可能会受到严重影响。好吧,也许还有其他解决方案,例如在调试模式下内联……但这已经是一个用例了。
【解决方案7】:

如果您确实需要最大的复制性能(您可能不需要),两者都不要使用

很多可以用来优化内存复制——如果您愿意为此使用多个线程/内核,则可以做更多。参见,例如:

What's missing/sub-optimal in this memcpy implementation?

问题和部分答案都提供了建议的实现或实现的链接。

【讨论】:

  • 学究模式:通常需要注意的是“两者都不使用”意味着如果您已经证明您有一个高度具体的情况/要求,而这两个标准都不是标准您的实现提供的功能足够快;否则,我通常担心的是那些没有证明过早优化复制代码而不是程序中通常更有用的部分的人。
【解决方案8】:

性能分析表明:std::copy() 总是和memcpy() 一样快,或者更快是错误的。

我的系统:

HP-Compaq-dx7500-Microtower 3.13.0-24-generic #47-Ubuntu SMP Fri May 2 23:30:00 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux。

gcc (Ubuntu 4.8.2-19ubuntu1) 4.8.2

代码(语言:c++):

    const uint32_t arr_size = (1080 * 720 * 3); //HD image in rgb24
    const uint32_t iterations = 100000;
    uint8_t arr1[arr_size];
    uint8_t arr2[arr_size];
    std::vector<uint8_t> v;

    main(){
        {
            DPROFILE;
            memcpy(arr1, arr2, sizeof(arr1));
            printf("memcpy()\n");
        }

        v.reserve(sizeof(arr1));
        {
            DPROFILE;
            std::copy(arr1, arr1 + sizeof(arr1), v.begin());
            printf("std::copy()\n");
        }

        {
            time_t t = time(NULL);
            for(uint32_t i = 0; i < iterations; ++i)
                memcpy(arr1, arr2, sizeof(arr1));
            printf("memcpy()    elapsed %d s\n", time(NULL) - t);
        }

        {
            time_t t = time(NULL);
            for(uint32_t i = 0; i < iterations; ++i)
                std::copy(arr1, arr1 + sizeof(arr1), v.begin());
            printf("std::copy() elapsed %d s\n", time(NULL) - t);
        }
    }

g++ -O0 -o test_stdcopy test_stdcopy.cpp

memcpy() 配置文件:main:21: now:1422969084:04859 elapsed:2650 us
std::copy() 配置文件:主要:27:现在:1422969084:04862 经过:2745 我们
memcpy() 经过 44 秒 std::copy() 经过 45 秒

g++ -O3 -o test_stdcopy test_stdcopy.cpp

memcpy() 配置文件:main:21: now:1422969601:04939 elapsed:2385 us
std::copy() 配置文件:主要:28:现在:1422969601:04941 经过:2690 我们
memcpy() 经过 27 秒 std::copy() 经过 43 秒

Red Alert 指出代码使用 memcpy from array to array 和 std::copy from array to vector。这可能是加快 memcpy 的一个原因。

既然有

v.reserve(sizeof(arr1));

复制到向量或数组应该没有区别。

代码被固定为在这两种情况下都使用数组。 memcpy 更快:

{
    time_t t = time(NULL);
    for(uint32_t i = 0; i < iterations; ++i)
        memcpy(arr1, arr2, sizeof(arr1));
    printf("memcpy()    elapsed %ld s\n", time(NULL) - t);
}

{
    time_t t = time(NULL);
    for(uint32_t i = 0; i < iterations; ++i)
        std::copy(arr1, arr1 + sizeof(arr1), arr2);
    printf("std::copy() elapsed %ld s\n", time(NULL) - t);
}

memcpy()    elapsed 44 s
std::copy() elapsed 48 s 

【讨论】:

  • 错了,您的分析表明复制到数组中比复制到向量中更快。离题。
  • 我可能是错的,但是在您更正的示例中,使用 memcpy,您不是将 arr2 复制到 arr1 中,而使用 std::copy,您将 arr1 复制到 arr2 中吗?...可以做的是进行多次交替实验(一次是一批memcpy,一次是一批std::copy,然后再用memcopy等多次返回。)。然后,我会使用clock() 而不是time(),因为谁知道你的PC 除了那个程序还能做什么。不过,只是我的两分钱...... :-)
  • 那么,将std::copy 从向量切换到数组不知何故使memcpy 花费了将近两倍的时间?这个数据非常可疑。我使用带有 -O3 的 gcc 编译了您的代码,生成的程序集对于两个循环都是相同的。因此,您在机器上观察到的任何时间差异都只是偶然的。
猜你喜欢
  • 1970-01-01
  • 2013-11-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-11-09
  • 1970-01-01
  • 2015-01-05
  • 2015-08-31
相关资源
最近更新 更多