在啥情况下我应该在 C++ 中使用 memcpy 而不是标准运算符?

Posted

技术标签:

【中文标题】在啥情况下我应该在 C++ 中使用 memcpy 而不是标准运算符?【英文标题】:In what cases should I use memcpy over standard operators in C++?在什么情况下我应该在 C++ 中使用 memcpy 而不是标准运算符? 【发布时间】:2011-05-31 11:55:08 【问题描述】:

我什么时候可以使用memcpy 获得更好的性能,或者我如何从使用它中受益? 例如:

float a[3]; float b[3];

是代码:

memcpy(a, b, 3*sizeof(float));

比这个更快

a[0] = b[0];
a[1] = b[1];
a[2] = b[2];

【问题讨论】:

我想即使是浮点数的赋值运算符也可以使用 memcpy 来实现。所以,直接对整个数组使用 memcpy 会更快 我不相信您的编辑。为什么第二种方法会更快。 memcpy() 专门设计用于将内存区域从一个地方复制到另一个地方,因此它应该与底层架构允许的一样高效。我敢打赌,它会在适用的情况下使用适当的程序集来进行块内存复制。 【参考方案1】:

效率不应该是您关心的问题。 编写干净的可维护代码。

这么多答案表明 memcpy() 效率低下,这让我很困扰。它被设计为复制内存块的最有效方式(对于 C 程序)。

所以我写了以下内容作为测试:

#include <algorithm>

extern float a[3];
extern float b[3];
extern void base();

int main()

    base();

#if defined(M1)
    a[0] = b[0];
    a[1] = b[1];
    a[2] = b[2];
#elif defined(M2)
    memcpy(a, b, 3*sizeof(float));    
#elif defined(M3)
    std::copy(&a[0], &a[3], &b[0]);
 #endif

    base();

然后对比代码产生:

g++ -O3 -S xr.cpp -o s0.s
g++ -O3 -S xr.cpp -o s1.s -DM1
g++ -O3 -S xr.cpp -o s2.s -DM2
g++ -O3 -S xr.cpp -o s3.s -DM3

echo "=======" >  D
diff s0.s s1.s >> D
echo "=======" >> D
diff s0.s s2.s >> D
echo "=======" >> D
diff s0.s s3.s >> D

这导致:(手动添加cmets)

=======   // Copy by hand
10a11,18
>   movq    _a@GOTPCREL(%rip), %rcx
>   movq    _b@GOTPCREL(%rip), %rdx
>   movl    (%rdx), %eax
>   movl    %eax, (%rcx)
>   movl    4(%rdx), %eax
>   movl    %eax, 4(%rcx)
>   movl    8(%rdx), %eax
>   movl    %eax, 8(%rcx)

=======    // memcpy()
10a11,16
>   movq    _a@GOTPCREL(%rip), %rcx
>   movq    _b@GOTPCREL(%rip), %rdx
>   movq    (%rdx), %rax
>   movq    %rax, (%rcx)
>   movl    8(%rdx), %eax
>   movl    %eax, 8(%rcx)

=======    // std::copy()
10a11,14
>   movq    _a@GOTPCREL(%rip), %rsi
>   movl    $12, %edx
>   movq    _b@GOTPCREL(%rip), %rdi
>   call    _memmove

添加了在 1000000000 的循环中运行上述内容的计时结果。

   g++ -c -O3 -DM1 X.cpp
   g++ -O3 X.o base.o -o m1
   g++ -c -O3 -DM2 X.cpp
   g++ -O3 X.o base.o -o m2
   g++ -c -O3 -DM3 X.cpp
   g++ -O3 X.o base.o -o m3
   time ./m1

   real 0m2.486s
   user 0m2.478s
   sys  0m0.005s
   time ./m2

   real 0m1.859s
   user 0m1.853s
   sys  0m0.004s
   time ./m3

   real 0m1.858s
   user 0m1.851s
   sys  0m0.006s

【讨论】:

+1。而且,由于您没有写下由此得出的明显结论,因此 memcpy 调用似乎生成了最有效的代码。 顺便说一句:@Martin:说“效率不应该是你的关注点,写好代码”是不合理的。人们使用 C++ 而不是像样的语言正是因为他们需要性能。这很重要。 @Yttrill:而且我从未见过编译器尚未做得更好的人工微优化。另一方面,编写可读性好的代码意味着您在算法级别上的思考更多,因为编译器不知道意图,因此人类可以在优化方面击败编译器。 附录:使用std::array&lt;float, 3&gt; 代替C 样式数组,它确实 有一个赋值运算符,结合了两全其美:readability and efficiency。并且具有不衰减到指针等额外的附加质量。此外,截至撰写本文时,GCC 5.2 和 Clang 3.7 在所有情况下都生成相同的代码,因此性能不再相关,应优先考虑可读性。 @LokiAstari 在上面的答案中引用了该程序集。除了上述需要检查指针重叠之外,对memmove 的非内联调用不可能像内联memcpy 一样快。这是假的。【参考方案2】:

只有当你复制的对象没有明确的构造函数时,你才能使用memcpy,因为它们的成员(所谓的 POD,“Plain Old Data”)。因此,可以为float 调用memcpy,但它是错误的,例如std::string

但部分工作已经为您完成:来自&lt;algorithm&gt;std::copy 专门用于内置类型(并且可能适用于所有其他 POD 类型 - 取决于 STL 实现)。所以写std::copy(a, a + 3, b)memcpy 一样快(在编译器优化之后),但更不容易出错。

【讨论】:

std::copy&lt;algorithm&gt; 中正确找到; &lt;algorithm.h&gt; 严格用于向后兼容。【参考方案3】:

编译器专门优化 memcpy 调用,至少 clang 和 gcc 可以。所以你应该尽可能喜欢它。

【讨论】:

@ismail :编译器可能会优化memcpy,但它仍然不太可能比第二种方法更快。请阅读 Simone 的帖子。 @Nawaz:我不同意。考虑到架构支持,memcpy() 可能会更快。无论如何,这是多余的,因为 std::copy (如@crazylammer 所述)可能是最好的解决方案。【参考方案4】:

使用std::copy()。作为g++ 的头文件注释:

这个内联函数将归结为尽可能调用@c memmove。

可能,Visual Studio 并没有太大的不同。采用正常方式,一旦发现瓶颈就进行优化。在简单副本的情况下,编译器可能已经为您优化了。

【讨论】:

【参考方案5】:

不要进行过早的微优化,例如像这样使用 memcpy。使用赋值更清晰,更不容易出错,任何体面的编译器都会生成适当高效的代码。当且仅当您分析了代码并发现分配是一个重大瓶颈时,您可以考虑进行某种微优化,但通常您应该始终首先编写清晰、健壮的代码。

【讨论】:

如何比单个memcpy 一个接一个地分配 N(其中 N > 2)不同的数组项? memcpy(a, b, sizeof a) 更清晰,因为如果ab 的大小发生变化,则无需添加/删除分配。 @Chris Lutz:您必须考虑代码在其整个生命周期内的健壮性,例如如果在某个时候有人更改了 a 的声明以使其成为指针而不是数组,会发生什么?在这种情况下,赋值不会中断,但 memcpy 会。 memcpy 不会破坏(sizeof a 技巧会破坏,但只有一些人会使用它)。 std::copy 也不会,这在几乎所有方面都明显优于两者。 @Chris:好吧,我宁愿看到一个 for 循环而不是单独的分配,当然,小心使用 memcpy 并不是 C 代码的禁区(虽然我不希望在 C++ 代码中看到它)。但是,如果您处理具有较长生命周期的代码,或者如果您关心诸如可移植性、移植到其他语言或编译器、使用代码分析工具、自动矢量化等,那么简单和清晰总是更重要比简洁和低级的黑客攻击。【参考方案6】:

memcpy 的好处?大概是可读性。否则,您将不得不进行大量分配或使用 for 循环进行复制,这两者都不像执行 memcpy 那样简单明了(当然,只要您的类型简单且不需要构造/破坏)。

此外,memcpy 通常针对特定平台进行了相对优化,以至于它不会比简单分配慢很多,甚至可能更快。

【讨论】:

【参考方案7】:

据说,正如 Nawaz 所说,作业版本应该在大多数平台上更快。这是因为memcpy() 会逐字节复制,而第二个版本可以一次复制 4 个字节。

与往常一样,您应该始终分析应用程序,以确保您预期的瓶颈与现实相符。

编辑 同样适用于动态数组。既然你提到了 C++,那么在这种情况下你应该使用std::copy() 算法。

编辑 这是带有 GCC 4.5.0 的 Windows XP 的代码输出,使用 -O3 标志编译:

extern "C" void cpy(float* d, float* s, size_t n)

    memcpy(d, s, sizeof(float)*n);

我已经完成了这个功能,因为 OP 也指定了动态数组。

输出汇编如下:

_cpy:
LFB393:
    pushl   %ebp
LCFI0:
    movl    %esp, %ebp
LCFI1:
    pushl   %edi
LCFI2:
    pushl   %esi
LCFI3:
    movl    8(%ebp), %eax
    movl    12(%ebp), %esi
    movl    16(%ebp), %ecx
    sall    $2, %ecx
    movl    %eax, %edi
    rep movsb
    popl    %esi
LCFI4:
    popl    %edi
LCFI5:
    leave
LCFI6:
    ret

当然,我假设这里的所有专家都知道rep movsb 的含义。

这是作业版本:

extern "C" void cpy2(float* d, float* s, size_t n)

    while (n > 0) 
        d[n] = s[n];
        n--;
    

产生以下代码:

_cpy2:
LFB394:
    pushl   %ebp
LCFI7:
    movl    %esp, %ebp
LCFI8:
    pushl   %ebx
LCFI9:
    movl    8(%ebp), %ebx
    movl    12(%ebp), %ecx
    movl    16(%ebp), %eax
    testl   %eax, %eax
    je  L2
    .p2align 2,,3
L5:
    movl    (%ecx,%eax,4), %edx
    movl    %edx, (%ebx,%eax,4)
    decl    %eax
    jne L5
L2:
    popl    %ebx
LCFI10:
    leave
LCFI11:
    ret

一次移动 4 个字节。

【讨论】:

@Simone :第一段对我来说很有意义。现在我需要验证它,因为我不确定。 :-) 我不认为 memcopy 会逐字节复制。它专门设计用于非常有效地复制大块内存。 来源好吗? POSIX 要求的唯一内容是this。顺便说一句,看看this implementation 有没有那么快。 @Simone - libc 编写者花费了大量时间来确保他们的memcpy 实现是高效的,编译器编写者也花费了同样多的时间让他们的编译器寻找可以更快地进行分配的情况memcpy,反之亦然。您关于“它可以像您希望的那样糟糕”的论点以及您的突然实施是一个红鲱鱼。看看 GCC 或其他编译器/libc 是如何实现它的。这对你来说可能已经足够快了。 通常的经验法则适用:“假设库作者没有脑损伤”。他们为什么要写一个每次只能复制一个字节的memcpy

以上是关于在啥情况下我应该在 C++ 中使用 memcpy 而不是标准运算符?的主要内容,如果未能解决你的问题,请参考以下文章

memcpy在啥情况下会失败

memcpy在啥情况下会失败

在啥情况下我可以使用 zip 和 combineLatest? RxSwift

在啥情况下我使用 Json 或 Hibernate?

在 Apple Swift 中,在啥情况下我不想要一个隐式展开的可选项?

在使用 boost 共享互斥锁时,我应该在啥情况下使用 owns_lock() 函数