memcpy
获取高带宽。ERMSB是在Ivy Bridge微架构中引入的。如果您不知道ERMSB是什么,请参阅Intel优化手册中的“增强的REP MOVSB和STOSB操作(ERMSB)”部分。
我所知道的唯一直接做法是使用内联汇编。我从https://groups.google.com/forum/#!topic/gnu.gcc.help/-Bmlm_EG_fE中获得了以下函数。
static inline void *__movsb(void *d, const void *s, size_t n) {
asm volatile ("rep movsb"
: "=D" (d),
"=S" (s),
"=c" (n)
: "0" (d),
"1" (s),
"2" (n)
: "memory");
return d;
}
当我使用这个函数时,其带宽比
memcpy
低得多。__movsb
在我的i7-6700HQ(Skylake)系统,Ubuntu 16.10,DDR4@2400 MHz双通道32 GB,GCC 6.2中达到15 GB/s,而memcpy
则为26 GB/s。为什么使用
REP MOVSB
的带宽较低?我该如何改进它?以下是我用来测试的代码。
//gcc -O3 -march=native -fopenmp foo.c
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <stddef.h>
#include <omp.h>
#include <x86intrin.h>
static inline void *__movsb(void *d, const void *s, size_t n) {
asm volatile ("rep movsb"
: "=D" (d),
"=S" (s),
"=c" (n)
: "0" (d),
"1" (s),
"2" (n)
: "memory");
return d;
}
int main(void) {
int n = 1<<30;
//char *a = malloc(n), *b = malloc(n);
char *a = _mm_malloc(n,4096), *b = _mm_malloc(n,4096);
memset(a,2,n), memset(b,1,n);
__movsb(b,a,n);
printf("%d\n", memcmp(b,a,n));
double dtime;
dtime = -omp_get_wtime();
for(int i=0; i<10; i++) __movsb(b,a,n);
dtime += omp_get_wtime();
printf("dtime %f, %.2f GB/s\n", dtime, 2.0*10*1E-9*n/dtime);
dtime = -omp_get_wtime();
for(int i=0; i<10; i++) memcpy(b,a,n);
dtime += omp_get_wtime();
printf("dtime %f, %.2f GB/s\n", dtime, 2.0*10*1E-9*n/dtime);
}
我对
rep movsb
感兴趣的原因是基于以下评论:请注意,在Ivybridge和Haswell上,由于缓冲区太大而无法适应MLC,您可以使用rep movsb击败movntdqa;movntdqa会在LLC中产生RFO,而rep movsb不会…… 与在Ivybridge和Haswell上流式传输到内存时相比,rep movsb比movntdqa快得多(但请注意,它在Ivybridge之前很慢!) 这个memcpy实现中缺少/次优的内容是什么?
这是我在tinymembnech上同一系统的测试结果。
C copy backwards : 7910.6 MB/s (1.4%)
C copy backwards (32 byte blocks) : 7696.6 MB/s (0.9%)
C copy backwards (64 byte blocks) : 7679.5 MB/s (0.7%)
C copy : 8811.0 MB/s (1.2%)
C copy prefetched (32 bytes step) : 9328.4 MB/s (0.5%)
C copy prefetched (64 bytes step) : 9355.1 MB/s (0.6%)
C 2-pass copy : 6474.3 MB/s (1.3%)
C 2-pass copy prefetched (32 bytes step) : 7072.9 MB/s (1.2%)
C 2-pass copy prefetched (64 bytes step) : 7065.2 MB/s (0.8%)
C fill : 14426.0 MB/s (1.5%)
C fill (shuffle within 16 byte blocks) : 14198.0 MB/s (1.1%)
C fill (shuffle within 32 byte blocks) : 14422.0 MB/s (1.7%)
C fill (shuffle within 64 byte blocks) : 14178.3 MB/s (1.0%)
---
standard memcpy : 12784.4 MB/s (1.9%)
standard memset : 30630.3 MB/s (1.1%)
---
MOVSB copy : 8712.0 MB/s (2.0%)
MOVSD copy : 8712.7 MB/s (1.9%)
SSE2 copy : 8952.2 MB/s (0.7%)
SSE2 nontemporal copy : 12538.2 MB/s (0.8%)
SSE2 copy prefetched (32 bytes step) : 9553.6 MB/s (0.8%)
SSE2 copy prefetched (64 bytes step) : 9458.5 MB/s (0.5%)
SSE2 nontemporal copy prefetched (32 bytes step) : 13103.2 MB/s (0.7%)
SSE2 nontemporal copy prefetched (64 bytes step) : 13179.1 MB/s (0.9%)
SSE2 2-pass copy : 7250.6 MB/s (0.7%)
SSE2 2-pass copy prefetched (32 bytes step) : 7437.8 MB/s (0.6%)
SSE2 2-pass copy prefetched (64 bytes step) : 7498.2 MB/s (0.9%)
SSE2 2-pass nontemporal copy : 3776.6 MB/s (1.4%)
SSE2 fill : 14701.3 MB/s (1.6%)
SSE2 nontemporal fill : 34188.3 MB/s (0.8%)
请注意,在我的系统上,
SSE2 copy prefetched
比MOVSB copy
更快。
在我的原始测试中,我没有禁用Turbo。我禁用了Turbo并重新进行了测试,似乎没有太大的区别。但是,更改电源管理确实会有很大的影响。
当我这样做时:
sudo cpufreq-set -r -g performance
我有时会看到超过20 GB/s的速度,使用rep movsb
。
使用
sudo cpufreq-set -r -g powersave
我看到的最佳速度约为17GB/s。但是
memcpy
似乎对电源管理不敏感。
我使用 turbostat
检查了频率(启用和禁用SpeedStep),对于空闲、1核负载和4核负载,分别使用 performance
和 powersave
。我运行了英特尔的 MKL 密集矩阵乘法来创建负载,并使用 OMP_SET_NUM_THREADS
设置线程数。以下是结果表格(以 GHz 为单位)。
SpeedStep idle 1 core 4 core
powersave OFF 0.8 2.6 2.6
performance OFF 2.6 2.6 2.6
powersave ON 0.8 3.5 3.1
performance ON 3.5 3.5 3.1
这表明即使关闭SpeedStep,使用
powersave
时CPU仍会降频至空闲频率0.8 GHz
。只有在关闭SpeedStep的情况下使用performance
时,CPU才会以恒定频率运行。我使用了例如
sudo cpufreq-set -r performance
(因为cpufreq-set
的结果很奇怪)来更改电源设置。这会重新启用Turbo,所以我必须在之后禁用Turbo。
memcpy
更好。我希望它的表现和memcpy
差不多。我使用gdb调试了memcpy
函数,发现它进入一个主循环,并使用了rep movsb
指令。所以这似乎就是memcpy
在某些情况下使用的方式。 - Z bosonmemcpy()
使用了 AVX NT 存储方式。而 NT 存储和 ERMSB 都以写合并的方式进行,因此不应需要 RFO(读取请求)。然而,在我的机器上进行基准测试显示,我的memcpy()
和我的 ERMSB 都仅达到总带宽的 2/3,就像你的memcpy()
(但不是_你的_ ERMSB)一样。因此,显然在某个地方有额外的总线事务,并且非常像 RFO。 - Iwillnotexist Idonotexistread()
和write()
)将数据复制到用户空间时:内核不能(不会)使用任何SIMD寄存器或SIMD代码,因此为了进行快速的memcpy操作,它必须使用64位的load/store指令,或者更近期它将使用rep movsb
或rep rmovd
如果在该架构上检测到其速度很快,则可以获得大块移动的许多好处,而无需明确使用xmm
或ymm
寄存器。 - BeeOnRope