我对编写memcpy()
作为教育练习感兴趣。我不会写一篇关于我所做和没想过的论文,但是这里是
some guy's implementation:
__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字节相当于手动展开,并进行了一些调整。所以,这是我的问题:
__restrict__
你的参数。 (@chux)答案 0 :(得分:33)
我一直在研究测量具有各种操作的英特尔处理器的内存带宽,其中一个是memcpy
。我在Core2,Ivy Bridge和Haswell上做过这个。我使用带内在函数的C / C ++完成了大部分测试(参见下面的代码 - 但我目前正在重写我的测试程序集)。
要编写自己的高效memcpy
功能,了解可能的绝对最佳带宽非常重要。这个带宽是要复制的数组大小的函数,因此高效memcpy
函数需要针对小和大(以及可能之间)进行不同的优化。为了简单起见,我已针对8192字节的小型数组和1 GB的大型数组进行了优化。
对于小型阵列,每个核心的最大读写带宽为:
Core2-Ivy Bridge 32 bytes/cycle
Haswell 64 bytes/cycle
这是您应该针对小型阵列的基准。对于我的测试,我假设数组与64字节对齐,并且数组大小是8*sizeof(float)*unroll_factor
的倍数。以下是我目前的memcpy
结果,大小为8192字节(Ubuntu 14.04,GCC 4.9,EGLIBC 2.19):
GB/s efficiency
Core2 (p9600@2.66 GHz)
builtin 35.2 41.3%
eglibc 39.2 46.0%
asmlib: 76.0 89.3%
copy_unroll1: 39.1 46.0%
copy_unroll8: 73.6 86.5%
Ivy Bridge (E5-1620@3.6 GHz)
builtin 102.2 88.7%
eglibc: 107.0 92.9%
asmlib: 107.6 93.4%
copy_unroll1: 106.9 92.8%
copy_unroll8: 111.3 96.6%
Haswell (i5-4250U@1.3 GHz)
builtin: 68.4 82.2%
eglibc: 39.7 47.7%
asmlib: 73.2 87.6%
copy_unroll1: 39.6 47.6%
copy_unroll8: 81.9 98.4%
asmlib
是Agner Fog's asmlib。 copy_unroll1
和copy_unroll8
函数定义如下。
从这个表中我们可以看到GCC内置memcpy
在Core2上运行不正常,而EGLIBC中memcpy
在Core2或Haswell上运行不正常。我最近检查了GLIBC的头版,并且Haswell的性能要好得多。在所有情况下,展开都会获得最佳结果。
void copy_unroll1(const float *x, float *y, const int n) {
for(int i=0; i<n/JUMP; i++) {
VECNF().LOAD(&x[JUMP*(i+0)]).STORE(&y[JUMP*(i+0)]);
}
}
void copy_unroll8(const float *x, float *y, const int n) {
for(int i=0; i<n/JUMP; i+=8) {
VECNF().LOAD(&x[JUMP*(i+0)]).STORE(&y[JUMP*(i+0)]);
VECNF().LOAD(&x[JUMP*(i+1)]).STORE(&y[JUMP*(i+1)]);
VECNF().LOAD(&x[JUMP*(i+2)]).STORE(&y[JUMP*(i+2)]);
VECNF().LOAD(&x[JUMP*(i+3)]).STORE(&y[JUMP*(i+3)]);
VECNF().LOAD(&x[JUMP*(i+4)]).STORE(&y[JUMP*(i+4)]);
VECNF().LOAD(&x[JUMP*(i+5)]).STORE(&y[JUMP*(i+5)]);
VECNF().LOAD(&x[JUMP*(i+6)]).STORE(&y[JUMP*(i+6)]);
VECNF().LOAD(&x[JUMP*(i+7)]).STORE(&y[JUMP*(i+7)]);
}
}
对于SSE VECNF().LOAD
为_mm_load_ps()
或AVX为_mm256_load_ps()
,VECNF().STORE
对于SSE为_mm_store_ps()
,对于AVX为_mm256_store_ps()
,为JUMP SSE为4,AVX为8。
对于大尺寸,通过使用non-temporal存储指令和使用多个线程获得最佳结果。与许多人可能认为的相反a single thread does NOT usually saturate the memory bandwidth。
void copy_stream(const float *x, float *y, const int n) {
#pragma omp parallel for
for(int i=0; i<n/JUMP; i++) {
VECNF v = VECNF().load_a(&x[JUMP*i]);
stream(&y[JUMP*i], v);
}
}
SSE的stream
_mm_stream_ps()
或AVX的_mm256_stream_ps()
以下是我的E5-1620@3.6 GHz上的memcpy
结果,其中四个线程为1 GB且maximum main memory bandwidth of 51.2 GB/s。
GB/s efficiency
eglibc: 23.6 46%
asmlib: 36.7 72%
copy_stream: 36.7 72%
EGLIBC再次表现不佳。这是因为它不使用非临时存储。
我修改了eglibc
和asmlib
memcpy
函数以便像这样并行运行
void COPY(const float * __restrict x, float * __restrict y, const int n) {
#pragma omp parallel
{
size_t my_start, my_size;
int id = omp_get_thread_num();
int num = omp_get_num_threads();
my_start = (id*n)/num;
my_size = ((id+1)*n)/num - my_start;
memcpy(y+my_start, x+my_start, sizeof(float)*my_size);
}
}
一般memcpy
函数需要考虑未与64字节(甚至是32或16字节)对齐的数组,并且其大小不是32字节的倍数或展开因子。另外,必须决定何时使用非临时存储。一般的经验法则是仅对大于最大缓存级别(通常为L3)的一半的大小使用非临时存储。但是论文是&#34;二阶&#34;我认为在优化大小理想情况后应该处理的细节。如果理想情况表现不佳,那么在担心纠正错位或非理想大小倍数方面没有多大意义。
<强>更新强>
根据Stephen Canon的评论我已经了解到,在Ivy Bridge和Haswell上使用rep movsb
比使用movntdqa
(非临时存储指令)更有效。英特尔称之为增强型rep movsb(ERMSB)。这在 3.7.6增强型REP MOVSB和STOSB操作(ERMSB)部分的Intel Optimization manuals中进行了描述。
此外,在Agner Fog的Optimizing Subroutines in Assembly手册的 17.9移动数据块(所有处理器)中,他写道:
&#34;有多种方法可以移动大块数据。最常见的方法是:
将军memcpy
应该考虑这些要点。此外,对于Ivy Bridge和Haswell来说,对于大型阵列来说,点1似乎优于点6。英特尔和AMD以及每次技术迭代都需要不同的技术。我认为编写自己的通用高效memcpy
函数非常复杂。但是在我看到的特殊情况下,我已经设法比GCC内置memcpy
或EGLIBC中的更好,所以假设你不能比标准库做得更好是不正确的。< / p>
答案 1 :(得分:4)
首先,主循环使用未对齐的AVX向量加载/存储来一次复制32个字节,直到&lt;要复制的32个字节:
for ( ; Size >= sizeof(__m256i); Size -= sizeof(__m256i) )
{
__m256i ymm = _mm256_loadu_si256(((const __m256i* &)Src)++);
_mm256_storeu_si256(((__m256i* &)Dst)++, ymm);
}
然后,最终的switch语句以尽可能有效的方式处理剩余的0..31个字节,并适当地使用8/4/2/1字节副本的组合。请注意,这不是一个展开的循环 - 它只是32个不同的优化代码路径,它们使用最少的加载和存储来处理剩余字节。
至于为什么主要的32字节AVX循环没有手动展开 - 有几个可能的原因:
[*]请注意,上面的最后两条注释适用于源和/或目标不在缓存中(即写入/读取DRAM)的情况,因此加载/存储延迟很高。