理解memcpy()的实现方式

14
我在查看memcpy.c的实现时,发现了一段不同的memcpy代码。我不理解为什么要执行 (((ADDRESS) s) | ((ADDRESS) d) | c) & (sizeof(UINT) - 1)。
#if !defined(__MACHDEP_MEMFUNC)

#ifdef _MSC_VER
#pragma function(memcpy)
#undef __MEMFUNC_ARE_INLINED
#endif

#if !defined(__MEMFUNC_ARE_INLINED)
/* Copy C bytes from S to D.
 * Only works if non-overlapping, or if D < S.
 */
EXTERN_C void * __cdecl memcpy(void *d, const void *s, size_t c)
{
    if ((((ADDRESS) s) | ((ADDRESS) d) | c) & (sizeof(UINT) - 1)) {

        BYTE *pS = (BYTE *) s;
        BYTE *pD = (BYTE *) d;
        BYTE *pE = (BYTE *) (((ADDRESS) s) + c);

        while (pS != pE)
            *(pD++) = *(pS++);
    }
    else {
        UINT *pS = (UINT *) s;
        UINT *pD = (UINT *) d;
        UINT *pE = (UINT *) (BYTE *) (((ADDRESS) s) + c);

        while (pS != pE)
            *(pD++) = *(pS++);
    }
    return d;
}

#endif /* ! __MEMFUNC_ARE_INLINED */
#endif /* ! __MACHDEP_MEMFUNC */

2
这就是为什么你不想自己写“memcpy”的原因 :-)不过真正的美丽只有在查看生成的机器代码时才能看到。 - Kerrek SB
2个回答

16

这段代码在测试地址是否适合使用UINT进行复制。如果是,则使用UINT对象进行复制。否则,使用BYTE对象进行复制。

测试的方法是首先对两个地址执行按位OR运算。任何一个地址中打开的位都将出现在结果中。然后测试对sizeof(UINT) - 1执行按位AND操作。预期UINT的大小为2的幂次方。然后大小减一会使得所有低位都打开。例如,如果大小为4或8,则比大小小1的二进制表示为112或1112。如果任何一个地址不是UINT大小的倍数,则它将具有其中一个位,并且测试将指示该情况。(通常,整数对象的最佳对齐方式与其大小相同。这并非必然如此。现代实现此代码的代码应使用_Alignof(UINT) - 1而不是大小)。

使用UINT对象进行复制速度更快,因为在硬件级别上,一个加载或存储指令就可以加载或存储一个UINT(可能是四个字节)。处理器通常在使用这些指令时复制速度更快,而不是在使用四倍数量的单字节加载或存储指令时。

当然,此代码取决于实现;它需要C实现的支持,这不是基本C标准的一部分,并且它依赖于执行的处理器的特定功能。

更高级的memcpy实现可能包含其他功能,例如:

  • 如果其中一个地址对齐而另一个地址没有对齐,则使用特殊的非对齐加载指令从一个地址加载多个字节,并使用常规存储指令将其存储到另一个地址。
  • 如果处理器具有单指令多数据指令,则使用这些指令在单个指令中加载或存储许多字节(通常为16个,可能更多)。

14

这段代码

((((ADDRESS) s) | ((ADDRESS) d) | c) & (sizeof(UINT) - 1))

检查sdc是否未与 UINT 的大小对齐。

例如,如果 s = 0x7ff30b14d = 0x7ffa81d8c = 256,且 sizeof(UINT) == 4,那么:

s         = 0b1111111111100110000101100010100
d         = 0b1111111111110101000000111011000
c         = 0b0000000000000000000000100000000
s | d | c = 0b1111111111110111000101111011100
(s | d | c) & 3 =                        0b00

所以这两个指针是对齐的。如果指针对齐,那么在它们之间复制内存就更容易了,并且这只需要一个分支。

在许多架构中,如果ptr被正确对齐到UINT的宽度,则*(UINT *) ptr快得多。在某些架构中,如果ptr没有正确对齐,*(UINT *) ptr实际上将导致崩溃。


1
它也会检查长度。 - Carl Norum
哇,谈论简洁而不清晰。任何需要四组括号的代码都应该重新考虑。 - Eric Andres
3
@EricAndres 欢迎来到表演的世界。这种东西到处都有 :) - Mysticial
1
@Mysticial 我相信它是可以的。我不是C程序员,所以我往往会轻易地忽略这样的事情。这绝对是一段代码,微观优化是可以被证明合理的。 - Eric Andres
2
@EricAndres 但是括号并不重要,当然。理想情况下,您会将 ((ADDRESS) s)((ADDRESS) d) 视为不透明的块 SD,因此您有 (S | D | c) & (sizeof(UNIT) - 1),这不是太糟糕,然后您还有一个来自 if ( … ) … 的集合。表达式本身嵌套方式并不特别复杂。 - Joshua Taylor

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接