'memcpy' 类函数,支持按单个位进行偏移?

'memcpy'-like function that supports offsets by individual bits?

我正在考虑解决这个问题,但它看起来是一项艰巨的任务。如果我自己拿这个,我可能会用几种不同的方式编写它并选择最好的,所以我想我会问这个问题,看看是否已经有一个很好的库可以解决这个问题,或者是否有人 thoughts/advice.

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.
}

我的应用程序时间紧迫,所以我想以最小的开销来解决这个问题。这是difficulty/complexity的来源。在我的例子中,块可能非常小,可能是 4-12 字节,所以大规模的 memcpy 东西(例如预取)并不是那么重要。对于随机未对齐的 src 缓冲区,最好的结果是在 4 到 12 之间的常量 'size' 输入中最快的结果。

有人拥有或知道类似的实现吗?或者有人想尝试写这篇文章,让它尽可能干净高效吗?

编辑:似乎人们正在投票"close" "too broad"。一些缩小的细节将是 AMD64 是首选架构,所以让我们假设。这意味着 little endian 等。实现有望很好地适应答案的大小,所以我认为这不会太宽泛。尽管有几种方法,我要求的答案一次是一个单一的实现。

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

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 可能对另一个人产生反作用。

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

  1. 输入缓冲区分配了一些填充,即在最后一个字节之后访问一些字节不会崩溃。
  2. 输出缓冲区也分配了一些填充,即使覆盖了所需结果位置之后的几个字节也没有关系。如果它确实很重要,那么您将需要做更多的事情来保留那些结束后的字节。
  3. 涉及的输入和输出范围不重叠(包括结束后的几个填充字节),就像在memcpy中一样。

如果可以,则可以增加算法的粒度。很容易更改@PaulR 的答案以在所有地方使用 uint64_t 个单词而不是 uint8_t 个字节。因此,它会工作得更快。

我们可以使用 SSE 进一步增加字数。由于在 SSE 中无法按位移位整个寄存器,我们必须对 64 位整数进行两次移位,然后将结果粘合在一起。粘合是由 SSSE3 中的 _mm_shuffle_epi8 完成的,它允许以任意方式随机排列 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 个周期的延迟,假设循环执行一次并且 caches/branches/decoding 没有问题发生。然而,在基准测试中,平均一次此类调用花费 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