我正在考虑解决这个问题,但它看起来是一项相当艰巨的任务。如果我自己拿这个,我可能会用几种不同的方式写出并选择最好的,所以我想我会问这个问题,看看是否有一个很好的图书馆可以解决这个问题,或者是否有人有想法/建议。
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之间的最快结果。
任何人拥有或知道类似的实施事物?或者有人想要写这篇文章,让它尽可能干净和高效吗?
编辑:似乎人们对“过于宽泛”的投票表示“接近”。一些缩小的细节将是AMD64的首选架构,所以我们假设。这意味着小端等等。实现有望完全符合答案的大小,所以我不认为这太宽泛了。我要求的答案一次只能是一个单一的实现,即使有一些方法。
答案 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的答案)是小块的最佳方法,除非你能满足以下附加约束:
如果可以,则可以增加算法的粒度。很容易改变@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。