‘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首选的架构,所以我们假设。 这意味着小端等。实现有望完全符合答案的大小,所以我不认为这太宽泛了。 即使有一些方法,我也会一次要求单个实现的答案。

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

 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上可能会适得其反。

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

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

如果可以,则可以增加算法的粒度。 很容易改变@PaulR的答案,在任何地方使用uint64_t字而不是uint8_t字节。 结果,它会更快地工作。

我们可以使用SSE来进一步增加字大小。 由于在SSE中没有办法按位移位整个寄存器,我们必须对64位整数进行两次移位,然后将结果粘合在一起。 通过SSSE3的_mm_shuffle_epi8进行_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表示,整个function在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) 

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

这里有完整的代码,包括基准测试和更详细的结果。