Poor memcpy Performance on Linux
我们最近购买了一些新服务器,并且内存性能不佳。与我们的笔记本电脑相比,服务器的memcpy性能要慢3倍。
好的。
服务器规格
好的。
好的。
编辑:我也在另一台具有更高规格的服务器上进行测试,并看到与上述服务器相同的结果
好的。
服务器2规格
好的。
好的。
笔记本电脑规格
好的。
好的。
操作系统
好的。
1 2 3 4 | $ cat /etc/redhat-release Scientific Linux release 6.5 (Carbon) $ uname -a Linux r113 2.6.32-431.1.2.el6.x86_64 #1 SMP Thu Dec 12 13:59:19 CST 2013 x86_64 x86_64 x86_64 GNU/Linux |
编译器(在所有系统上)
好的。
1 2 | $ gcc --version gcc (GCC) 4.6.1 |
还根据@stefan的建议使用gcc 4.8.2进行了测试。编译器之间没有性能差异。
好的。
测试代码
下面的测试代码是一个罐头测试,用于复制我在生产代码中看到的问题。我知道此基准很简单,但是它可以利用并确定我们的问题。该代码在它们之间创建了两个1GB的缓冲区和memcpys,从而对memcpy调用进行计时。您可以使用以下命令在命令行上指定备用缓冲区大小:./big_memcpy_test [SIZE_BYTES]
好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | #include <chrono> #include <cstring> #include <iostream> #include <cstdint> class Timer { public: Timer() : mStart(), mStop() { update(); } void update() { mStart = std::chrono::high_resolution_clock::now(); mStop = mStart; } double elapsedMs() { mStop = std::chrono::high_resolution_clock::now(); std::chrono::milliseconds elapsed_ms = std::chrono::duration_cast<std::chrono::milliseconds>(mStop - mStart); return elapsed_ms.count(); } private: std::chrono::high_resolution_clock::time_point mStart; std::chrono::high_resolution_clock::time_point mStop; }; std::string formatBytes(std::uint64_t bytes) { static const int num_suffix = 5; static const char* suffix[num_suffix] = {"B","KB","MB","GB","TB" }; double dbl_s_byte = bytes; int i = 0; for (; (int)(bytes / 1024.) > 0 && i < num_suffix; ++i, bytes /= 1024.) { dbl_s_byte = bytes / 1024.0; } const int buf_len = 64; char buf[buf_len]; // use snprintf so there is no buffer overrun int res = snprintf(buf, buf_len,"%0.2f%s", dbl_s_byte, suffix[i]); // snprintf returns number of characters that would have been written if n had // been sufficiently large, not counting the terminating null character. // if an encoding error occurs, a negative number is returned. if (res >= 0) { return std::string(buf); } return std::string(); } void doMemmove(void* pDest, const void* pSource, std::size_t sizeBytes) { memmove(pDest, pSource, sizeBytes); } int main(int argc, char* argv[]) { std::uint64_t SIZE_BYTES = 1073741824; // 1GB if (argc > 1) { SIZE_BYTES = std::stoull(argv[1]); std::cout <<"Using buffer size from command line:" << formatBytes(SIZE_BYTES) << std::endl; } else { std::cout <<"To specify a custom buffer size: big_memcpy_test [SIZE_BYTES] \ " <<"Using built in buffer size:" << formatBytes(SIZE_BYTES) << std::endl; } // big array to use for testing char* p_big_array = NULL; ///////////// // malloc { Timer timer; p_big_array = (char*)malloc(SIZE_BYTES * sizeof(char)); if (p_big_array == NULL) { std::cerr <<"ERROR: malloc of" << SIZE_BYTES <<" returned NULL!" << std::endl; return 1; } std::cout <<"malloc for" << formatBytes(SIZE_BYTES) <<" took" << timer.elapsedMs() <<"ms" << std::endl; } ///////////// // memset { Timer timer; // set all data in p_big_array to 0 memset(p_big_array, 0xF, SIZE_BYTES * sizeof(char)); double elapsed_ms = timer.elapsedMs(); std::cout <<"memset for" << formatBytes(SIZE_BYTES) <<" took" << elapsed_ms <<"ms" <<"(" << formatBytes(SIZE_BYTES / (elapsed_ms / 1.0e3)) <<" bytes/sec)" << std::endl; } ///////////// // memcpy { char* p_dest_array = (char*)malloc(SIZE_BYTES); if (p_dest_array == NULL) { std::cerr <<"ERROR: malloc of" << SIZE_BYTES <<" for memcpy test" <<" returned NULL!" << std::endl; return 1; } memset(p_dest_array, 0xF, SIZE_BYTES * sizeof(char)); // time only the memcpy FROM p_big_array TO p_dest_array Timer timer; memcpy(p_dest_array, p_big_array, SIZE_BYTES * sizeof(char)); double elapsed_ms = timer.elapsedMs(); std::cout <<"memcpy for" << formatBytes(SIZE_BYTES) <<" took" << elapsed_ms <<"ms" <<"(" << formatBytes(SIZE_BYTES / (elapsed_ms / 1.0e3)) <<" bytes/sec)" << std::endl; // cleanup p_dest_array free(p_dest_array); p_dest_array = NULL; } ///////////// // memmove { char* p_dest_array = (char*)malloc(SIZE_BYTES); if (p_dest_array == NULL) { std::cerr <<"ERROR: malloc of" << SIZE_BYTES <<" for memmove test" <<" returned NULL!" << std::endl; return 1; } memset(p_dest_array, 0xF, SIZE_BYTES * sizeof(char)); // time only the memmove FROM p_big_array TO p_dest_array Timer timer; // memmove(p_dest_array, p_big_array, SIZE_BYTES * sizeof(char)); doMemmove(p_dest_array, p_big_array, SIZE_BYTES * sizeof(char)); double elapsed_ms = timer.elapsedMs(); std::cout <<"memmove for" << formatBytes(SIZE_BYTES) <<" took" << elapsed_ms <<"ms" <<"(" << formatBytes(SIZE_BYTES / (elapsed_ms / 1.0e3)) <<" bytes/sec)" << std::endl; // cleanup p_dest_array free(p_dest_array); p_dest_array = NULL; } // cleanup free(p_big_array); p_big_array = NULL; return 0; } |
CMake文件生成
好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | project(big_memcpy_test) cmake_minimum_required(VERSION 2.4.0) include_directories(${CMAKE_CURRENT_SOURCE_DIR}) # create verbose makefiles that show each command line as it is issued set( CMAKE_VERBOSE_MAKEFILE ON CACHE BOOL"Verbose" FORCE ) # release mode set( CMAKE_BUILD_TYPE Release ) # grab in CXXFLAGS environment variable and append C++11 and -Wall options set( CMAKE_CXX_FLAGS"${CMAKE_CXX_FLAGS} -std=c++0x -Wall -march=native -mtune=native" ) message( INFO"CMAKE_CXX_FLAGS = ${CMAKE_CXX_FLAGS}" ) # sources to build set(big_memcpy_test_SRCS main.cpp ) # create an executable file named"big_memcpy_test" from # the source files in the variable"big_memcpy_test_SRCS". add_executable(big_memcpy_test ${big_memcpy_test_SRCS}) |
检测结果
好的。
1 2 3 4 5 6 | Buffer Size: 1GB | malloc (ms) | memset (ms) | memcpy (ms) | NUMA nodes (numactl --hardware) --------------------------------------------------------------------------------------------- Laptop 1 | 0 | 127 | 113 | 1 Laptop 2 | 0 | 180 | 120 | 1 Server 1 | 0 | 306 | 301 | 2 Server 2 | 0 | 352 | 325 | 2 |
如您所见,我们服务器上的memcpys和memsets比笔记本电脑上的memcpys和memsets慢得多。
好的。
缓冲区大小不同
好的。
我尝试了从100MB到5GB的缓冲区,但结果都差不多(服务器比笔记本电脑慢)
好的。
NUMA亲和力
好的。
我读到有关NUMA出现性能问题的人的信息,所以我尝试使用numactl设置CPU和内存的亲和力,但结果保持不变。
好的。
服务器NUMA硬件
好的。
1 2 3 4 5 6 7 8 9 10 11 12 | $ numactl --hardware available: 2 nodes (0-1) node 0 cpus: 0 1 2 3 4 5 6 7 16 17 18 19 20 21 22 23 node 0 size: 65501 MB node 0 free: 62608 MB node 1 cpus: 8 9 10 11 12 13 14 15 24 25 26 27 28 29 30 31 node 1 size: 65536 MB node 1 free: 63837 MB node distances: node 0 1 0: 10 21 1: 21 10 |
笔记本电脑NUMA硬件
好的。
1 2 3 4 5 6 7 8 | $ numactl --hardware available: 1 nodes (0) node 0 cpus: 0 1 2 3 4 5 6 7 node 0 size: 16018 MB node 0 free: 6622 MB node distances: node 0 0: 10 |
设置NUMA亲和力
好的。
1 | $ numactl --cpunodebind=0 --membind=0 ./big_memcpy_test |
任何解决此问题的帮助将不胜感激。
好的。
编辑:GCC选项
好的。
根据评论,我尝试使用不同的GCC选项进行编译:
好的。
将-march和-mtune设置为native进行编译
好的。
1 | g++ -std=c++0x -Wall -march=native -mtune=native -O3 -DNDEBUG -o big_memcpy_test main.cpp |
结果:完全一样的性能(无改善)
好的。
使用-O2而不是-O3进行编译
好的。
1 | g++ -std=c++0x -Wall -march=native -mtune=native -O2 -DNDEBUG -o big_memcpy_test main.cpp |
结果:完全一样的性能(无改善)
好的。
编辑:更改memset写入0xF而不是0,以避免NULL页(@SteveCox)
好的。
使用0以外的值进行记忆设置(在这种情况下使用0xF)没有任何改善。
好的。
编辑:Cachebench结果
好的。
为了排除我的测试程序过于简单,我下载了一个真正的基准测试程序LLCacheBench(http://icl.cs.utk.edu/projects/llcbench/cachebench.html)
好的。
我在每台计算机上分别建立了基准,以避免体系结构问题。以下是我的结果。
好的。
好的。
注意,较大的缓冲区在性能上有很大的不同。最后测试的大小(16777216)在笔记本电脑上为18849.29 MB /秒,在服务器上为6710.40。这大约是性能的3倍。您还可以注意到,服务器的性能下降比笔记本电脑要严重得多。
好的。
编辑:memmove()比服务器上的memcpy()快2倍
好的。
根据一些实验,我尝试在测试用例中使用memmove()而不是memcpy(),发现服务器上的性能提高了2倍。笔记本电脑上的Memmove()运行速度比memcpy()慢,但奇怪的是,其运行速度与服务器上的memmove()相同。这就引出了一个问题,为什么memcpy这么慢?
好的。
更新了代码以与memcpy一起测试memmove。我必须将memmove()包装在一个函数中,因为如果我将其内联,则GCC会对其进行优化并执行与memcpy()完全相同的操作(我认为gcc将其优化为memcpy,因为它知道位置不重叠)。
好的。
更新结果
好的。
1 2 3 4 5 6 | Buffer Size: 1GB | malloc (ms) | memset (ms) | memcpy (ms) | memmove() | NUMA nodes (numactl --hardware) --------------------------------------------------------------------------------------------------------- Laptop 1 | 0 | 127 | 113 | 161 | 1 Laptop 2 | 0 | 180 | 120 | 160 | 1 Server 1 | 0 | 306 | 301 | 159 | 2 Server 2 | 0 | 352 | 325 | 159 | 2 |
编辑:天真Memcpy
好的。
基于@Salgar的建议,我已经实现了自己的幼稚memcpy函数并对其进行了测试。
好的。
天真的Memcpy来源
好的。
1 2 3 4 5 6 7 8 9 | void naiveMemcpy(void* pDest, const void* pSource, std::size_t sizeBytes) { char* p_dest = (char*)pDest; const char* p_source = (const char*)pSource; for (std::size_t i = 0; i < sizeBytes; ++i) { *p_dest++ = *p_source++; } } |
天真Memcpy结果与memcpy()比较
好的。
1 2 3 4 5 | Buffer Size: 1GB | memcpy (ms) | memmove(ms) | naiveMemcpy() ------------------------------------------------------------ Laptop 1 | 113 | 161 | 160 Server 1 | 301 | 159 | 159 Server 2 | 325 | 159 | 159 |
编辑:程序集输出
好的。
简单的memcpy来源
好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | #include <cstring> #include <cstdlib> int main(int argc, char* argv[]) { size_t SIZE_BYTES = 1073741824; // 1GB char* p_big_array = (char*)malloc(SIZE_BYTES * sizeof(char)); char* p_dest_array = (char*)malloc(SIZE_BYTES * sizeof(char)); memset(p_big_array, 0xA, SIZE_BYTES * sizeof(char)); memset(p_dest_array, 0xF, SIZE_BYTES * sizeof(char)); memcpy(p_dest_array, p_big_array, SIZE_BYTES * sizeof(char)); free(p_dest_array); free(p_big_array); return 0; } |
程序集输出:这在服务器和便携式计算机上完全相同。我正在节省空间,而不是同时粘贴两者。
好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | .file "main_memcpy.cpp" .section .text.startup,"ax",@progbits .p2align 4,,15 .globl main .type main, @function main: .LFB25: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movl $1073741824, %edi pushq %rbx .cfi_def_cfa_offset 24 .cfi_offset 3, -24 subq $8, %rsp .cfi_def_cfa_offset 32 call malloc movl $1073741824, %edi movq %rax, %rbx call malloc movl $1073741824, %edx movq %rax, %rbp movl $10, %esi movq %rbx, %rdi call memset movl $1073741824, %edx movl $15, %esi movq %rbp, %rdi call memset movl $1073741824, %edx movq %rbx, %rsi movq %rbp, %rdi call memcpy movq %rbp, %rdi call free movq %rbx, %rdi call free addq $8, %rsp .cfi_def_cfa_offset 24 xorl %eax, %eax popq %rbx .cfi_def_cfa_offset 16 popq %rbp .cfi_def_cfa_offset 8 ret .cfi_endproc .LFE25: .size main, .-main .ident "GCC: (GNU) 4.6.1" .section .note.GNU-stack,"",@progbits |
进展!!!!汇编库
好的。
根据@tbenson的建议,我尝试使用memcpy的asmlib版本运行。最初我的结果很差,但是将SetMemcpyCacheLimit()更改为1GB(缓冲区的大小)后,我的运行速度与朴素的for循环相当!
好的。
坏消息是memmove的asmlib版本比glibc版本要慢,它现在的运行时间为300毫秒(与memcpy的glibc版本相当)。奇怪的是,在笔记本电脑上,当我将SetMemcpyCacheLimit()设置为大量数值时,会损害性能...
好的。
在下面的结果中,用SetCache标记的行将SetMemcpyCacheLimit设置为1073741824。没有SetCache的结果不会调用SetMemcpyCacheLimit()
好的。
使用asmlib函数的结果:
好的。
1 2 3 4 5 6 7 8 | Buffer Size: 1GB | memcpy (ms) | memmove(ms) | naiveMemcpy() ------------------------------------------------------------ Laptop | 136 | 132 | 161 Laptop SetCache | 182 | 137 | 161 Server 1 | 305 | 302 | 164 Server 1 SetCache | 162 | 303 | 164 Server 2 | 300 | 299 | 166 Server 2 SetCache | 166 | 301 | 166 |
开始倾向于缓存问题,但这会导致什么呢?
好的。
好。
[我会发表评论,但没有足够的声誉。]
我有一个类似的系统,并且看到了类似的结果,但是可以添加一些数据点:
-
如果您将天真
memcpy 的方向反向(即转换为*p_dest-- = *p_src-- ),则性能可能会比正向方向(对我而言约为637 ms)差很多。 glibc 2.12中的memcpy() 中有一个更改,它暴露了一些在重叠缓冲区上调用memcpy 的错误(http://lwn.net/Articles/414467/),我相信问题是由于切换到的版本引起的向后操作的memcpy 。因此,后向副本与正向副本可以解释memcpy() /memmove() 差异。 -
最好不要使用非临时存储。许多优化的
memcpy() 实现都针对大型缓冲区(即比上一级缓存大)切换到非临时存储(未缓存)。我测试了Agner Fog的memcpy版本(http://www.agner.org/optimize/#asmlib),发现它的速度与glibc 中的版本大致相同。但是,asmlib 具有功能(SetMemcpyCacheLimit ),该功能允许设置阈值,在该阈值之上使用非临时存储。将该限制设置为8GiB(或刚好大于1 GiB缓冲区),以避免非临时存储在我的情况下性能提高了一倍(时间降至176ms)。当然,这仅与前向天真的性能相匹配,因此它不是一流的。 - 这些系统上的BIOS允许启用/禁用四个不同的硬件预取程序(MLC Streamer预取程序,MLC空间预取程序,DCU Streamer预取程序和DCU IP预取程序)。我尝试禁用每个设置,但这样做最多只能保持一些性能的均等性并降低性能。
- 禁用运行平均功率限制(RAPL)DRAM模式没有任何影响。
-
我可以访问运行Fedora 19(glibc 2.17)的其他Supermicro系统。使用Supermicro X9DRG-HF板,Fedora 19和Xeon E5-2670 CPU,我看到的性能与上述类似。在运行Xeon E3-1275 v3(Haswell)和Fedora 19的Supermicro X10SLM-F单插座板上,
memcpy (104ms)的速度为9.6 GB / s。 Haswell系统上的RAM是DDR3-1600(与其他系统相同)。
更新
-
我将CPU电源管理设置为"最大性能",并在BIOS中禁用了超线程。然后基于
/proc/cpuinfo ,内核的时钟频率为3 GHz。但是,这奇怪地将内存性能降低了约10%。 - memtest86 + 4.10向主内存报告的带宽为9091 MB / s。我找不到这是否对应于读取,写入或复制。
- STREAM基准报告的复制速度为13422 MB / s,但它们将字节数记为已读和已写,因此如果要与上述结果进行比较,则相当于?6.5 GB / s。
对我来说这很正常。
与具有2x2GB的单个CPU相比,使用两个CPU管理8x16GB的ECC记忆棒要困难得多。您的16GB记忆棒是双面内存+它们可能具有缓冲区+ ECC(甚至在主板级别上禁用)...所有这些都使到RAM的数据路径更长。您还有2个CPU共享内存,即使您在其他CPU上不执行任何操作,内存访问也始终很少。切换此数据需要一些额外的时间。只要看看与显卡共享内存的PC所损失的巨大性能即可。
您的服务器仍然是真正强大的数据泵。我不确定在现实生活中经常会复制1GB的软件,但是我确定您的128GB比任何硬盘驱动器甚至最好的SSD都快得多,这是您可以利用服务器的地方。对3GB进行相同的测试会使笔记本电脑着火。
这看起来像一个完美的例子,说明基于商用硬件的体系结构比大型服务器更有效率。用这些大服务器上的钱买得起多少台消费PC?
感谢您提出的非常详细的问题。
编辑:(花了我这么长时间来写这个答案,我错过了图部分。)
我认为问题在于数据的存储位置。您能否比较一下:
- 测试一个:分配两个连续的500Mb内存块,然后从一个复制到另一个(已完成)
- 测试二:分配20个(或更多)500Mb内存块,并从第一个复制到最后一个,因此它们彼此相距很远(即使您不能确定它们的实际位置)。
这样,您将看到内存控制器如何处理彼此远离的内存块。我认为您的数据放在不同的内存区域中,并且需要在数据路径上的某个点进行切换操作才能与一个区域然后是另一个区域进行通信(双面存储器存在这种问题)。
另外,您是否确保线程绑定到一个CPU?
编辑2:
内存有几种"区域"定界符。 NUMA是一个,但这不是唯一的一个。例如,两侧的棍棒需要标记来寻址一侧或另一侧。在您的图表上查看,即使在笔记本电脑上(即使没有NUMA),性能也会随着大量内存而降低。
我不确定,但是memcpy可能使用硬件功能来复制ram(一种DMA),并且该芯片的缓存必须比CPU少,这可以解释为什么使用CPU的哑复制比memcpy更快。
与基于SandyBridge的服务器相比,基于IvyBridge的笔记本电脑中的某些CPU改进可能有助于实现这一目标。
跨页预取-每当您到达当前线性页的末尾时,笔记本计算机的CPU就会提前预取下一个线性页,从而每次都避免了讨厌的TLB丢失。要尝试缓解这种情况,请尝试为2M / 1G页面构建服务器代码。
缓存替换方案似乎也得到了改进(请参阅此处的有趣的反向工程)。如果确实此CPU使用动态插入策略,则可以轻松防止复制的数据试图破坏Last-Level-Cache(由于大小原因,它最终无法有效使用),并为其他有用的缓存留出了空间。如代码,堆栈,页表数据等)。为了测试这一点,您可以尝试使用流负载/存储(
我相信字符串复制(在这里)也进行了一些改进,取决于您的汇编代码的外观,它在这里可能适用也可能不适用。您可以尝试使用Dhrystone进行基准测试,以测试是否存在固有差异。这也可以解释memcpy和memmove之间的区别。
如果您可以使用基于IvyBridge的服务器或Sandy-Bridge笔记本电脑,则将所有这些一起测试将是最简单的。
我修改了基准以在Linux中使用nsec计时器,并发现在不同处理器上都有相似的变化,所有处理器都具有相似的内存。所有正在运行的RHEL6。编号在多个运行中是一致的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | Sandy Bridge E5-2648L v2 @ 1.90GHz, HT enabled, L2/L3 256K/20M, 16 GB ECC malloc for 1073741824 took 47us memset for 1073741824 took 643841us memcpy for 1073741824 took 486591us Westmere E5645 @2.40 GHz, HT not enabled, dual 6-core, L2/L3 256K/12M, 12 GB ECC malloc for 1073741824 took 54us memset for 1073741824 took 789656us memcpy for 1073741824 took 339707us Jasper Forest C5549 @ 2.53GHz, HT enabled, dual quad-core, L2 256K/8M, 12 GB ECC malloc for 1073741824 took 126us memset for 1073741824 took 280107us memcpy for 1073741824 took 272370us |
这是内联C代码-O3的结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | Sandy Bridge E5-2648L v2 @ 1.90GHz, HT enabled, 256K/20M, 16 GB malloc for 1 GB took 46 us memset for 1 GB took 478722 us memcpy for 1 GB took 262547 us Westmere E5645 @2.40 GHz, HT not enabled, dual 6-core, 256K/12M, 12 GB malloc for 1 GB took 53 us memset for 1 GB took 681733 us memcpy for 1 GB took 258147 us Jasper Forest C5549 @ 2.53GHz, HT enabled, dual quad-core, 256K/8M, 12 GB malloc for 1 GB took 67 us memset for 1 GB took 254544 us memcpy for 1 GB took 255658 us |
出于麻烦,我还尝试使内联memcpy一次执行8个字节。
在这些Intel处理器上,没有明显的区别。高速缓存将所有字节操作合并为最少数量的内存操作。我怀疑gcc库代码试图太聪明了。
这些数字对我来说很有意义。这里实际上有两个问题,我都会回答。
好的。
不过,首先,我们需要一个思维模型,说明大型内存传输在现代Intel处理器等设备上的工作量。该描述是近似的,细节可能因体系结构的不同而有所变化,但是高级思想是相当固定的。
好的。
内存子系统本身具有最大带宽限制,您可以在ARK上方便地找到它。例如,Lenovo笔记本电脑中的3720QM的限制为25.6 GB。此限制基本上是有效频率(
好的。
与其他处理器功能不同,在整个芯片上通常只有一个可能的理论带宽数,因为
它仅取决于在许多情况下通常相同的标注值
不同的芯片,甚至跨架构。这是不现实的
期望DRAM能够以准确的理论速率交付(由于各种
低层次的关注,讨论了一下
在这里),但您通常可以
大约90%或更多。
好的。
因此,(1)的主要结果是您可以将对RAM的丢失视为一种请求响应系统。对DRAM的未命中会分配一个填充缓冲区,并在请求返回时释放该缓冲区。每个CPU中只有10个缓冲区用于需求未命中,这严格限制了单个CPU可以生成的需求内存带宽(取决于其延迟)。
好的。
例如,假设您的
好的。
如果您想深入研究更多细节,则该线程几乎是纯金。您会发现约翰·麦卡平(John McCalpin)的事实和数据,又称"带宽博士将是下面的常见主题。
好的。
因此,让我们进入细节并回答两个问题...
好的。
为什么memcpy比服务器上的memmove或手动复制慢得多?
您证明便携式计算机系统在大约120毫秒内完成了
好的。
上面我们已经表明,对于单个内核,带宽受总可用并发性和延迟的限制,而不是受DRAM带宽的限制。我们希望服务器部件可能有更长的延迟,但
好的。
答案在于流式存储(又称非临时性)存储。您正在使用的
好的。
流存储损害了单个CPU编号,因为:
好的。
好的。
在上述链接的主题中,John McCalpin的引文对这两个问题进行了更好的解释。关于预取有效性和流存储,他说:
好的。
With"ordinary" stores, L2 hardware prefetcher can fetch lines in
advance and reduce the time that the Line Fill Buffers are occupied,
thus increasing sustained bandwidth. On the other hand, with
streaming (cache-bypassing) stores, the Line Fill Buffer entries for
the stores are occupied for the full time required to pass the data to
the DRAM controller. In this case, the loads can be accelerated by
hardware prefetching, but the stores cannot, so you get some speedup,
but not as much as you would get if both loads and stores were
accelerated.Ok.
...然后,对于E5上流式存储的明显更长的延迟,他说:
好的。
至强E3的更简单的"解核"可能会导致显着降低
流存储的行填充缓冲区占用率。至强E5具有
进行导航以传递更复杂的环结构
从核心缓冲区向存储控制器流式传输存储,因此
占用率的差异可能大于内存(读取)的差异
潜伏。好的。
blockquote>
尤其是,麦卡平博士测得E5的速度是"非客户端"芯片的1.8倍,但OP报告的2.5倍的速度与STREAM TRIAD的1.8倍得分一致。负载:存储的比例为2:1,而
memcpy 为1:1,存储是有问题的部分。好的。
这并没有使流媒体成为一件坏事-实际上,您在等待时间与总带宽消耗之间进行了权衡。由于使用单个内核时并发性受到限制,因此获得的带宽较少,但是避免了所有所有权的读取流量,因此,如果同时在所有内核上运行测试,则可能会看到(小的)好处。
好的。
到目前为止,其他用户使用相同的CPU报告了完全相同的速度下降,而不只是软件或硬件配置的假象。
好的。
为什么在使用普通商店时服务器部分仍然较慢?
即使更正了非临时性存储问题,您仍会在服务器部分上看到
160 / 120 = ~1.33x 缓慢的情况。是什么赋予了?好的。
嗯,常见的谬误是服务器CPU在各个方面都更快或至少等于客户端。事实并非如此-您在服务器部件上所支付的费用(通常为每片2,000美元左右)通常是(a)更多内核(b)更多内存通道(c)支持更多总RAM(d)支持"企业级"功能,例如ECC,虚拟化功能等5。
好的。
实际上,就延迟而言,服务器部分通常仅等于或慢于其客户机部分。当涉及到内存延迟时,尤其如此,因为:
好的。
服务器部分具有更高的可伸缩性,但是复杂的"非核心"通常需要支持更多的核心,因此到RAM的路径更长。 服务器部件支持更多的RAM(100 GB或几TB的RAM),这通常需要电缓冲器来支持如此大的数量。 就像在OP的情况下一样,服务器部分通常是多插槽的,这增加了跨插槽一致性问题的内存路径。 好的。
因此,服务器部分的延迟通常比客户端部分长40%至60%。对于E5,您可能会发现?80 ns是RAM的典型延迟,而客户端部分接近50 ns。
好的。
因此,受RAM延迟限制的任何内容在服务器部件上的运行速度都会变慢,事实证明,单个内核上的
memcpy 受延迟限制。这令人困惑,因为memcpy 似乎是带宽测量,对不对?如上所述,一个内核没有足够的资源来一次向飞行中的RAM发出足够的请求以接近RAM带宽6,因此性能直接取决于延迟。好的。
另一方面,客户端芯片具有较低的延迟和较低的带宽,因此一个内核更接近于饱和带宽(这通常就是为什么流存储在客户端部分上大获成功的原因-即使是单个内核也可以接近RAM带宽,流存储提供的50%的存储带宽减少帮助很大。
好的。
参考资料
有很多很好的资源可以阅读有关此内容的更多信息,这里有一些。
好的。
内存延迟组件的详细说明 跨新旧CPU的大量内存延迟结果(请参见 MemLatX86 和NewMemLat )链接详细分析Sandy Bridge(和Opteron)的内存延迟-几乎与OP使用的芯片相同。 好的。
1总的来说,我的意思是比LLC大一些。对于适合于LLC(或更高缓存级别)的副本,其行为是非常不同的。 OPs
llcachebench 图显示实际上,性能偏差仅在缓冲区开始超过LLC大小时才开始。好的。
2特别是,几代以来,行填充缓冲区的数量显然一直保持在10,包括此问题中提到的体系结构。
好的。
3当我们在这里说需求时,是指它与代码中的显式加载/存储相关联,而不是说是由预取带来的。
好的。
4这里提到服务器部分时,是指带有服务器核心的CPU。这在很大程度上意味着E5系列,因为E3系列通常使用客户端非核心功能。
好的。
5将来,您似乎可以在此列表中添加"指令集扩展",因为
AVX-512 似乎只会出现在Skylake服务器部件上。好的。
6根据80 ns延迟的普尔定律,我们需要一直在飞行中运行
(51.2 B/ns * 80 ns) == 4096 bytes 或64个高速缓存行才能达到最大带宽,但是一个内核提供的内存少于20个。好的。
好。
上面已经回答了这个问题,但是无论如何,这是使用AVX的实现,如果您担心的话,对于大型副本来说应该更快。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38 #define ALIGN(ptr, align) (((ptr) + (align) - 1) & ~((align) - 1))
void *memcpy_avx(void *dest, const void *src, size_t n)
{
char * d = static_cast<char*>(dest);
const char * s = static_cast<const char*>(src);
/* fall back to memcpy() if misaligned */
if ((reinterpret_cast<uintptr_t>(d) & 31) != (reinterpret_cast<uintptr_t>(s) & 31))
return memcpy(d, s, n);
if (reinterpret_cast<uintptr_t>(d) & 31) {
uintptr_t header_bytes = 32 - (reinterpret_cast<uintptr_t>(d) & 31);
assert(header_bytes < 32);
memcpy(d, s, min(header_bytes, n));
d = reinterpret_cast<char *>(ALIGN(reinterpret_cast<uintptr_t>(d), 32));
s = reinterpret_cast<char *>(ALIGN(reinterpret_cast<uintptr_t>(s), 32));
n -= min(header_bytes, n);
}
for (; n >= 64; s += 64, d += 64, n -= 64) {
__m256i *dest_cacheline = (__m256i *)d;
__m256i *src_cacheline = (__m256i *)s;
__m256i temp1 = _mm256_stream_load_si256(src_cacheline + 0);
__m256i temp2 = _mm256_stream_load_si256(src_cacheline + 1);
_mm256_stream_si256(dest_cacheline + 0, temp1);
_mm256_stream_si256(dest_cacheline + 1, temp2);
}
if (n > 0)
memcpy(d, s, n);
return dest;
}
Server 1 Specs
- CPU: 2x Intel Xeon E5-2680 @ 2.70 Ghz
Server 2 Specs
- CPU: 2x Intel Xeon E5-2650 v2 @ 2.6 Ghz
根据Intel ARK,E5-2650和E5-2680均具有AVX扩展名。
CMake File to Build
这是您问题的一部分。 CMake为您选择了一些较差的标志。您可以通过运行
make VERBOSE=1 进行确认。您应该将
-march=native 和-O3 都添加到CFLAGS 和CXXFLAGS 中。您可能会看到戏剧性的性能提升。它应使用AVX扩展名。如果没有-march=XXX ,则可以有效地获得最少的i686或x86_64计算机。如果没有-O3 ,则不会参与GCC的矢量化。我不确定GCC 4.6是否支持AVX(以及BMI之类的朋友)。我知道GCC 4.8或4.9能够胜任,因为当GCC将memcpy和memset外包给MMX单元时,我不得不寻找一个导致段错误的对齐错误。 AVX和AVX2允许CPU一次处理16字节和32字节的数据块。
如果GCC缺少将对齐的数据发送到MMX单元的机会,则可能缺少对齐数据的事实。如果您的数据是16字节对齐的,那么您可以尝试告诉GCC,使其知道可以在胖块上运行。为此,请参阅GCC的
__builtin_assume_aligned 。另请参阅类似的问题,例如如何告诉GCC指针参数始终是双字对齐的?由于
void* ,这看起来也有点怀疑。它是一种丢弃有关指针的信息的方法。您可能应该保留以下信息:
1
2
3
4 void doMemmove(void* pDest, const void* pSource, std::size_t sizeBytes)
{
memmove(pDest, pSource, sizeBytes);
}也许像下面这样:
1
2
3
4
5 template <typename T>
void doMemmove(T* pDest, const T* pSource, std::size_t count)
{
memmove(pDest, pSource, count*sizeof(T));
}另一个建议是使用
new ,并停止使用malloc 。它是一个C ++程序,GCC可以对new 作一些假设,而对malloc 则不能做。我相信某些假设在GCC的内置选项页中有详细说明。另一个建议是使用堆。在典型的现代系统上,它总是16字节对齐。当涉及到来自堆的指针时,GCC应该认识到它可以卸载到MMX单元(消除了潜在的
void* 和malloc 问题)。最后,有一段时间,当使用
-march=native 时,Clang没有使用本机CPU扩展。例如,请参见Ubuntu Issue 1616723,Clang 3.4仅发布SSE2,Ubuntu Issue 1616723,Clang 3.5仅发布SSE2,以及Ubuntu Issue 1616723,Clang 3.6仅广告SSE2。