“memcpy”式功能,支持各个位的偏移量?

时间:2015-08-17 06:22:05

标签: c++ c optimization bit-manipulation memcpy

我正在考虑解决这个问题,但它看起来是一项相当艰巨的任务。如果我自己拿这个,我可能会用几种不同的方式写出并选择最好的,所以我想我会问这个问题,看看是否有一个很好的图书馆可以解决这个问题,或者是否有人有想法/建议。

void OffsetMemCpy(u8* pDest, u8* pSrc, u8 srcBitOffset, size size)
{
    // Or something along these lines. srcBitOffset is 0-7, so the pSrc buffer 
    // needs to be up to one byte longer than it would need to be in memcpy.
    // Maybe explicitly providing the end of the buffer is best.
    // Also note that pSrc has NO alignment assumptions at all.
}

我的应用程序非常重要,所以我想以最小的开销来解决这个问题。这是困难/复杂性的根源。在我的情况下,块可能非常小,可能是4-12个字节,因此大规模的memcpy东西(例如预取)并不重要。对于随机未对齐的src缓冲区,最好的结果是对于常量'大小'输入,在4到12之间的最快结果。

  • 只要有可能,内存应以字大小的块移动
  • 这些字大小的块的对齐很重要。 pSrc是未对齐的,因此我们可能需要从前面读取几个字节,直到它对齐为止。

任何人拥有或知道类似的实施事物?或者有人想要写这篇文章,让它尽可能干净和高效吗?

编辑:似乎人们对“过于宽泛”的投票表示“接近”。一些缩小的细节将是AMD64的首选架构,所以我们假设。这意味着小端等等。实现有望完全符合答案的大小,所以我不认为这太宽泛了。我要求的答案一次只能是一个单一的实现,即使有一些方法。

2 个答案:

答案 0 :(得分:5)

我将从一个简单的实现开始,例如:

inline void OffsetMemCpy(uint8_t* pDest, const uint8_t* pSrc, const uint8_t srcBitOffset, const size_t size)
{
    if (srcBitOffset == 0)
    {
        for (size_t i = 0; i < size; ++i)
        {
            pDest[i] = pSrc[i];
        }
    }
    else if (size > 0)
    {
        uint8_t v0 = pSrc[0];
        for (size_t i = 0; i < size; ++i)
        {
            uint8_t v1 = pSrc[i + 1];
            pDest[i] = (v0 << srcBitOffset) | (v1 >> (CHAR_BIT - srcBitOffset));
            v0 = v1;            
        }
    }
}

(警告:未经测试的代码!)。

一旦这个工作正常,然后在您的应用程序中对其进行分析 - 您可能会发现它足够快,足以满足您的需求,从而避免过早优化的陷阱。如果没有,那么您将有一个有用的基线参考实现,以进行进一步的优化工作。

请注意,对于小型副本,测试对齐和字大小副本等的开销可能会超过任何好处,因此如上所述的简单逐字节循环可能接近最佳。

另请注意,优化很可能取决于体系结构 - 微优化可以在一个CPU上产生效益,但可能会在另一个CPU上产生效果。

答案 1 :(得分:1)

我认为这个简单的逐字节解决方案(参见@PaulR的答案)是小块的最佳方法,除非你能满足以下附加约束:

  1. 输入缓冲区分配了一些填充,即在最后一个填充没有崩溃后访问一些字节。
  2. 输出缓冲区也分配了一些填充,如果覆盖了所需结果位置后的几个字节,则无关紧要。如果它确实重要,那么你需要做更多的事情来保存那些后端字节。
  3. 所涉及的输入和输出范围不重叠(包括结尾后的几个填充字节),就像在memcpy中一样。
  4. 如果可以,则可以增加算法的粒度。很容易改变@PaulR的答案,即在任何地方使用uint64_t个单词而不是uint8_t个字节。结果,它会更快地运作。

    我们可以使用SSE来进一步增加字数。由于在SSE中没有办法按位移位整个寄存器,我们必须对64位整数进行两次移位,然后将结果粘合在一起。粘合是由_mm_shuffle_epi8从SSSE3完成的,它允许以任意方式在XMM寄存器中混洗字节。对于移位,我们使用_mm_srl_epi64,因为这是通过非立即数位移位64位整数的唯一方法。我已将C(作为宏)中的restrict关键字添加到指针参数中,因为如果它们是别名,则算法无论如何都不会起作用。

    以下是代码:

    void OffsetMemCpy_stgatilov(uint8_t *RESTRICT pDest, const uint8_t *RESTRICT pSrc, const uint8_t srcBitOffset, const size_t size) {
        __m128i bits = (sizeof(size_t) == 8 ? _mm_cvtsi64_si128(srcBitOffset) : _mm_cvtsi32_si128(srcBitOffset));
        const uint8_t *pEnd = pSrc + size;
        while (pSrc < pEnd) {
            __m128i input = _mm_loadu_si128((__m128i*)pSrc);
            __m128i reg = _mm_shuffle_epi8(input, _mm_setr_epi8(0, 1, 2, 3, 4, 5, 6, 7, 7, 8, 9, 10, 11, 12, 13, 14));
            __m128i shifted = _mm_srl_epi64(reg, bits);
            __m128i comp = _mm_shuffle_epi8(shifted, _mm_setr_epi8(0, 1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14, -1, -1));
            _mm_storeu_si128((__m128i*)pDest, comp);
            pSrc += 14;  pDest += 14;
        }
    }
    

    每次迭代处理14个字节。每次迭代都相当简单,循环之前也有一些代码。这是由MSVC2013 x64生成的整个函数体的汇编代码:

        movzx   eax, r8b
        movd    xmm3, rax
        lea rax, QWORD PTR [rdx+r9]
        cmp rdx, rax
        jae SHORT $LN1@OffsetMemC
        movdqa  xmm1, XMMWORD PTR __xmm@0e0d0c0b0a0908070706050403020100
        movdqa  xmm2, XMMWORD PTR __xmm@ffff0e0d0c0b0a090806050403020100
        sub rcx, rdx
        npad    11
    $LL2@OffsetMemC:
        movdqu  xmm0, XMMWORD PTR [rdx]
        add rdx, 14
        pshufb  xmm0, xmm1
        psrlq   xmm0, xmm3
        pshufb  xmm0, xmm2
        movdqu  XMMWORD PTR [rcx+rdx-14], xmm0
        cmp rdx, rax
        jb  SHORT $LL2@OffsetMemC
    $LN1@OffsetMemC:
        ret 0
    

    IACA表示整个函数在Ivy Bridge上需要4.5个周期的吞吐量和13个周期的延迟,因为循环执行一次并且没有发生缓存/分支/解码的问题。然而,在基准测试中,平均每个调用花费了7.5个周期。

    以下是Ivy Bridge 3.4 Ghz吞吐量基准测试的简要结果(请参阅代码中的更多结果):

    (billions of calls per second)
    size = 4:
      0.132  (Paul R)
      0.248  (Paul R x64)
      0.45  (stgatilov)
    size = 8:
      0.0782  (Paul R)
      0.249  (Paul R x64)
      0.45  (stgatilov)
    size = 12:
      0.0559  (Paul R)
      0.191  (Paul R x64)
      0.453  (stgatilov)
    

    但请注意,在现实世界中,性能可能与基准测试结果大不相同。

    基准测试和更详细结果的完整代码为here

相关问题