C++ ';memcpy'-类似于支持单个位偏移的函数?
我正在考虑解决这个问题,但这看起来是一项相当艰巨的任务。如果我自己拿这本书,我可能会用几种不同的方式来写,并选择最好的,所以我想我会问这个问题,看看是否有一个好的图书馆已经解决了这个问题,或者是否有人有想法/建议C++ ';memcpy'-类似于支持单个位偏移的函数?,c++,c,optimization,bit-manipulation,memcpy,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 bu
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内容(例如预取)并不那么重要。最好的结果是,对于恒定的“大小”输入,在4到12之间,对于随机未对齐的src缓冲区,能够以最快的速度工作
- 内存应尽可能以字大小的块移动
- 对齐这些字大小的块很重要。pSrc未对齐,因此我们可能需要从前端读取几个字节,直到对齐为止
编辑:人们似乎把这个“接近”选为“太宽”。一些狭义的细节可能是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;
}
}
}
inline void OffsetMemCpy(uint8\u t*pDest、const uint8\u t*pSrc、const uint8\u t srcBitOffset、const size\u t size)
{
if(srcBitOffset==0)
{
对于(大小i=0;i0)
{
uint8_t v0=pSrc[0];
对于(大小i=0;i(字符位-srcBitOffset));
v0=v1;
}
}
}
(警告:未测试的代码!)
一旦这项功能发挥作用,然后在应用程序中对其进行分析-您可能会发现它的速度足以满足您的需求,从而避免过早优化的陷阱。如果没有,那么您有一个有用的基线参考实施,用于进一步的优化工作
请注意,对于小拷贝,对齐和字大小拷贝等的测试开销可能远远超过任何好处,因此,像上面这样简单的逐字节循环可能接近最佳
还要注意的是,优化很可能取决于体系结构-在一个CPU上带来好处的微优化可能会对另一个CPU产生相反的效果。我认为简单的逐字节解决方案(参见@PaulR的答案)是小型块的最佳方法,除非您能够满足以下附加约束:
uint64\u t
单词而不是uint8\u t
字节非常容易。因此,它的工作速度会更快
我们可以使用SSE进一步增加单词大小。由于在SSE中无法将整个寄存器按位移位,我们必须对64位整数进行两次移位,然后将结果粘合在一起。胶接由SSSE3的\u mm\u shuffle\u epi8
完成,它允许以任意方式对XMM寄存器中的字节进行混洗。对于移位,我们使用\u mm\u srl\u 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;
}
}
(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)
说整个函数在Ivy桥上需要4.5个周期的吞吐量和13个周期的延迟,因为循环只执行一次,缓存/分支/解码没有问题。然而,在基准测试中,一次这样的调用平均花费7.5个周期
以下是Ivy Bridge 3.4 Ghz吞吐量基准测试的简要结果(请参阅代码中的更多结果):
然而,请注意,在现实世界中,性能可能与基准测试结果截然不同
具有基准测试和更详细结果的完整代码是。首先编写一个干净简单的版本(应该很简单),然后在应用程序中对其进行分析-您可能会发现它足够快,足以满足您的需要,从而避免过早优化的陷阱。如果没有,那么您就有了一个有用的基线参考实现,可以进行进一步的工作。是的,这就是我现在正在做的。我不敢相信这个问题还没有解决。不过,这是针对国际象棋引擎中的哈希表,我要替换的整个代码仅在几行代码中就占用了60%的CPU。我以前从未在国际象棋程序哈希表中见过未对齐的访问。它们通常是非常仔细的缓存线对齐。请参见:选择C和C++中的一个。同意解决方案,并且我注意到,由于字节是连续的,所以可以通过使用32位或64位整数块(直到最后块)来获得进一步的加速。(验证大或小端点不重要)…如果大小不变,则可以自定义您的算法,而无需太多分支逻辑(如果有的话)。(OP说,“最好的结果是最快的板凳
(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)