【问题标题】:Optimization in Memcpy implementationMemcpy 实现中的优化
【发布时间】:2018-12-31 20:23:42
【问题描述】:

以下是我在搜索优化的memcpy 实现时得到的代码。
这是link

void *memcpy(void *dst, void const *src, size_t len) {
    long *plDst = (long *)dst;
    long const *plSrc = (long const *)src;

    if (!(src & 0xFFFFFFFC) && !(dst & 0xFFFFFFFC)) {
        while (len >= 4) {
            *plDst++ = *plSrc++;
            len -= 4;
        }
    }

    char *pcDst = (char *)plDst;
    char const *pcDst = (char const *)plSrc;

    while (len--) {
         *pcDst++ = *pcSrc++;
    }

    return (dst);
}

谁能给我解释一下下面这行吗?

if (!(src & 0xFFFFFFFC) && !(dst & 0xFFFFFFFC))

在这里他们想检查srcdst 地址是否与4 byte 边界对齐。为什么他们使用!,因为它每次都会使条件false

其次,以上代码是否还有进一步优化的空间?

【问题讨论】:

  • 为什么你认为条件可能每次都是假的?
  • 看起来像是文章中的错字 - 应该是:if (!(src & ~0xFFFFFFFC) && !(dst & ~0xFFFFFFFC))
  • 为了检查对齐你可能想使用!(src&3) && !(dst&3)sn-p中的条件检查不同的东西
  • 这篇文章的第一条评论也注意到了这个错别字
  • 感谢您的评论。对齐现在很清楚。我们可以在代码中进行更多优化以使其更快吗?如果我们不检查 4 字节对齐怎么办?

标签: c optimization


【解决方案1】:

这篇文章虽然讨论了一个有趣的主题,但没有提供正确的示例。发布的代码称为 GNU 的 newlib 源代码。 GNU 项目和 newlib 团队都将惊讶于这个意想不到的融合声明! newlib 不是 GNU 项目,它的大部分源代码都没有 GPL 许可。

memcpy 的这种优化实现是不可移植的、次优的并且在许多方面不正确。

测试if (!(src & 0xFFFFFFFC) && !(dst & 0xFFFFFFFC)) 尝试检测srcdst 地址是否在long 边界上对齐。由于多种原因,它很笨重且不可移植,而且正如您所注意到的,它完全不正确:

  • void *int 的隐式转换是丑陋的并且是实现定义的。指针应转换为 (uintptr_t) 以获得更好的可移植性。
  • 0xFFFFFFFC 假定类型 long 是 4 个字节。这可能是不正确的,并且确实在 64 位 linux 和 Mac 系统上键入 long 是 8 字节长。
  • src & 0xFFFFFFFC 不是对齐检查,不太可能是 0,4 字节边界对齐的预期测试是 src & 3

此外,代码未能优化srcdst 具有相同对齐但未在long 边界上对齐的情况。

其他可能的改进包括展开循环,对len 的小值使用开关,将从src 读取的字节组合到longs 以写入dst,一旦它与long 边界对齐。 ..

这是一个改进的替代方案:

#include <stdint.h>

void *memcpy(void *dst, void const *src, size_t len) {
    unsigned char *pcDst = (unsigned char *)dst;
    unsigned char const *pcSrc = (unsigned char const *)src;

    if (len >= sizeof(long) * 2
    &&  ((uintptr_t)src & (sizeof(long) - 1)) == ((uintptr_t)dst & (sizeof(long) - 1))) {
        while (((uintptr_t)pcSrc & (sizeof(long) - 1)) != 0) {
            *pcDst++ = *pcSrc++;
            len--;
        }
        long *plDst = (long *)pcDst;
        long const *plSrc = (long const *)pcSrc;
        /* manually unroll the loop */
        while (len >= sizeof(long) * 4) {
            plDst[0] = plSrc[0];
            plDst[1] = plSrc[1];
            plDst[2] = plSrc[2];
            plDst[3] = plSrc[3];
            plSrc += 4;
            plDst += 4;
            len -= sizeof(long) * 4;
        }
        while (len >= sizeof(long)) {
            *plDst++ = *plSrc++;
            len -= sizeof(long);
        }
        pcDst = (unsigned char *)plDst;
        pcSrc = (unsigned char const *)plSrc;
    }
    while (len--) {
         *pcDst++ = *pcSrc++;
    }
    return dst;
}

请注意,void * 的强制转换在 C 中是不必要的,但在 C++ 中是必需的。

在尝试优化代码以提高速度时,请牢记以下几点:

  • 没有正确性的权衡。即使在边界情况下也会失败的快速代码是无用的。
  • 注意可移植性问题:依赖于类型大小和/或字节顺序的解决方案可能会产生可移植性问题。
  • 基准测试将告诉您哪些有效,哪些无效:您必须仔细为各种测试用例的各种备选方案计时。
  • 没有完美的解决方案:在一种架构上更快的代码可能会在不同的架构上变慢,包括未来的架构上最成问题的。不同架构的基准测试。
  • 消除复杂性:避免不会带来实质性改进的复杂解决方案,它们的正确性更难证明和维护。
  • memcpy 通常在汇编中进行优化,或者由现代编译器作为内置实现。

【讨论】:

  • 我建议在做任何其他事情之前检查len &lt;= 8,并使用switch 语句来处理简短的复制操作。这将避免需要检查您是否可以将数据复制到源和/或目标对齐的点。我建议复制数据直到目标对齐,然后如果源未对齐并且副本“大”,则主循环类似于x = *adjustedSrc++; *dest++ = (x &lt;&lt; shift1) | (y &gt;&gt; shift2); y=x;,从而避免将操作分解为更小的块.
  • @supercat:好点。关于将未对齐的源字节组合到longs 中以存储到对齐的目标地址中,很容易忽略字节顺序问题。根据架构,它甚至可能不会更快。事实上,在允许非对齐读写的 x86 目标上,通过 64 位类型的简单读/写循环可能是最快的解决方案。基准测试是评判者。
  • 更不用说优化memcpy() 是非常特定于处理器的。在 x86 上,通常 memcpy-opcode 是最好的,或者至少足够合理。
猜你喜欢
  • 2019-04-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-11-29
  • 1970-01-01
  • 2013-10-11
  • 2023-03-21
  • 2022-06-15
相关资源
最近更新 更多