我对编写memcpy()
作为一种教育 Activity 感兴趣。我不会写关于我所做和未曾思考过的全部论文,但这是
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)最佳答案
我一直在研究测量具有各种操作的英特尔处理器的内存带宽,其中之一是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
的倍数。这是我当前的819t字节大小的memcpy
结果(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)]);
}
}
其中
VECNF().LOAD
是SSE的_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);
}
}
其中
stream
是SSE的_mm_stream_ps()
或AVX的_mm256_stream_ps()
这是我的E5-1620@3.6 GHz在maximum main memory bandwidth of 51.2 GB/s上具有1 GB的四个线程的4个线程上的
memcpy
结果。 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)一半的大小使用非临时存储。但是这些都是“二阶”细节,我认为应该针对理想的大小案例进行优化后再处理。如果理想情况下的性能也很差,则不必担心校正未对准或不理想的尺寸倍数。更新
根据Stephen Canon的评论,我了解到在Ivy Bridge和Haswell上,使用
rep movsb
比使用movntdqa
(非临时存储指令)更有效。英特尔将其称为增强型rep movsb(ERMSB)。在3.7.7.6增强的REP MOVSB和STOSB操作(ERMSB)部分的Intel Optimization manuals中对此进行了描述。此外,在Agner Fog的Optimizing Subroutines in Assembly手册的第17.9节“移动数据块(所有处理器)”中,他写道:
“有几种移动大型数据块的方法。最常见的方法是:
对齐。然后以最大的可用循环读取未对齐并写入对齐
寄存器大小。
对齐。
进行移位以补偿未对准。”
一般的
memcpy
应该考虑所有这些点。此外,对于Ivy Bridge和Haswell,大型阵列的点1优于点6。对于Intel和AMD以及技术的每次迭代,都必须使用不同的技术。我认为很明显,编写自己的通用有效memcpy
函数可能非常复杂。但是在特殊情况下,我已经看过比GCC内置的memcpy
或EGLIBC中的代码更好,因此认为您不能比标准库做得更好的假设是不正确的。
关于c - 此memcpy实现中缺少什么/欠佳?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/26246040/