我对编写 memcpy()
函数作为教育练习产生了兴趣。我不会写一篇关于我所思考和不思考的全部论文,但是这里有一个某人的实现:
__forceinline // Since Size is usually known,
// most useless code will be optimized out
// if the function is inlined.
void* myMemcpy(char* Dst, const char* Src, size_t Size)
{
void* start = Dst;
for ( ; Size >= sizeof(__m256i); Size -= sizeof(__m256i) )
{
__m256i ymm = _mm256_loadu_si256(((const __m256i* &)Src)++);
_mm256_storeu_si256(((__m256i* &)Dst)++, ymm);
}
#define CPY_1B *((uint8_t * &)Dst)++ = *((const uint8_t * &)Src)++
#define CPY_2B *((uint16_t* &)Dst)++ = *((const uint16_t* &)Src)++
#define CPY_4B *((uint32_t* &)Dst)++ = *((const uint32_t* &)Src)++
#if defined _M_X64 || defined _M_IA64 || defined __amd64
#define CPY_8B *((uint64_t* &)Dst)++ = *((const uint64_t* &)Src)++
#else
#define CPY_8B _mm_storel_epi64((__m128i *)Dst, _mm_loadu_si128((const __m128i *)Src)), ++(const uint64_t* &)Src, ++(uint64_t* &)Dst
#endif
#define CPY16B _mm_storeu_si128((__m128i *)Dst, _mm_loadu_si128((const __m128i *)Src)), ++(const __m128i* &)Src, ++(__m128i* &)Dst
switch (Size) {
case 0x00: break;
case 0x01: CPY_1B; break;
case 0x02: CPY_2B; break;
case 0x03: CPY_1B; CPY_2B; break;
case 0x04: CPY_4B; break;
case 0x05: CPY_1B; CPY_4B; break;
case 0x06: CPY_2B; CPY_4B; break;
case 0x07: CPY_1B; CPY_2B; CPY_4B; break;
case 0x08: CPY_8B; break;
case 0x09: CPY_1B; CPY_8B; break;
case 0x0A: CPY_2B; CPY_8B; break;
case 0x0B: CPY_1B; CPY_2B; CPY_8B; break;
case 0x0C: CPY_4B; CPY_8B; break;
case 0x0D: CPY_1B; CPY_4B; CPY_8B; break;
case 0x0E: CPY_2B; CPY_4B; CPY_8B; break;
case 0x0F: CPY_1B; CPY_2B; CPY_4B; CPY_8B; break;
case 0x10: CPY16B; break;
case 0x11: CPY_1B; CPY16B; break;
case 0x12: CPY_2B; CPY16B; break;
case 0x13: CPY_1B; CPY_2B; CPY16B; break;
case 0x14: CPY_4B; CPY16B; break;
case 0x15: CPY_1B; CPY_4B; CPY16B; break;
case 0x16: CPY_2B; CPY_4B; CPY16B; break;
case 0x17: CPY_1B; CPY_2B; CPY_4B; CPY16B; break;
case 0x18: CPY_8B; CPY16B; break;
case 0x19: CPY_1B; CPY_8B; CPY16B; break;
case 0x1A: CPY_2B; CPY_8B; CPY16B; break;
case 0x1B: CPY_1B; CPY_2B; CPY_8B; CPY16B; break;
case 0x1C: CPY_4B; CPY_8B; CPY16B; break;
case 0x1D: CPY_1B; CPY_4B; CPY_8B; CPY16B; break;
case 0x1E: CPY_2B; CPY_4B; CPY_8B; CPY16B; break;
case 0x1F: CPY_1B; CPY_2B; CPY_4B; CPY_8B; CPY16B; break;
}
#undef CPY_1B
#undef CPY_2B
#undef CPY_4B
#undef CPY_8B
#undef CPY16B
return start;
}
这条评论的意思是“通常大小在编译器优化代码时可以内联排除大部分无用内容”。
如果可能的话,我想改进一下这个实现——但也许没有太多可以改进的地方。我看到它对较大的内存块使用SSE/AVX,然后不是对最后小于32字节的循环进行循环,而是等效于手动展开,并进行了一些微调。那么,这里有我的问题:
- 为什么要展开最后几个字节的循环,而不是部分展开第一个(现在是单一的)循环?
- 对齐问题呢?它们不重要吗?我应该如何处理前几个字节,直到某个对齐量,然后在对齐的字节序列上执行256位操作?如果是这样,我如何确定适当的对齐量?
- 这个实现中最重要的缺失特性是什么(如果有的话)?
到目前为止,在答案中提到的功能/原则
- 您应该
__restrict__
您的参数。 (@chux) - 内存带宽是一个限制因素;衡量您的实现是否达到它。(@Zboson)
- 对于小数组,您可以期望接近内存带宽;对于较大的数组-不太可能。 (@Zboson)
- 多个线程(可能)需要饱和内存带宽。 (@Zboson)
- 最好为大型和小型复制大小进行不同的优化。 (@Zboson)
- (对齐确实很重要吗?没有明确解释!)
- 编译器应该更加明确地了解它可以用于优化的“显而易见的事实”(例如Size < 32在第一个循环后)。 (@chux)
- 有关展开SSE/AVX调用的论点(@BenJackson,此处),以及不这样做的论点(@PaulR)
- 非暂态传输(告诉CPU您不需要将目标位置缓存)应该对复制较大缓冲区有用。 (@Zboson)
switch
分支。看起来非常不错。满分10分,我会提交的 :) - dom0void *memcpy(void * restrict s1, const void * restrict s2, size_t n);
。 - chux - Reinstate Monicaswitch (Size)
是否与Size
范围0<=Size<32
匹配。或许可以尝试switch (Size&31)
?避免内部生成的if size > 31
。 - chux - Reinstate Monica