Linux上的memcpy性能不佳

nic*_*ick 69 c c++ linux memcpy numa

我们最近购买了一些新的服务器,并且正在经历糟糕的memcpy性能.与我们的笔记本电脑相比,服务器上的memcpy性能要慢3倍.

服务器规格

  • 底盘和Mobo:SUPER MICRO 1027GR-TRF
  • CPU:2x Intel Xeon E5-2680 @ 2.70 Ghz
  • 内存:8x 16GB DDR3 1600MHz

编辑:我也在另一台具有更高规格的服务器上进行测试,并看到与上述服务器相同的结果

服务器2规格

  • 底盘和Mobo:SUPER MICRO 10227GR-TRFT
  • CPU:2x Intel Xeon E5-2650 v2 @ 2.6 Ghz
  • 内存:8x 16GB DDR3 1866MHz

笔记本电脑规格

  • 机箱:联想W530
  • CPU:1x Intel Core i7 i7-3720QM @ 2.6Ghz
  • 内存:4x 4GB DDR3 1600MHz

操作系统

$ 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
Run Code Online (Sandbox Code Playgroud)

编译器(在所有系统上)

$ gcc --version
gcc (GCC) 4.6.1
Run Code Online (Sandbox Code Playgroud)

还根据@stefan的建议使用gcc 4.8.2进行了测试.编译器之间没有性能差异.

测试 代码下面的测试代码是一个罐装测试,用于复制我在生产代码中看到的问题.我知道这个基准是简单的,但它能够利用和识别我们的问题.代码在它们之间创建两个1GB缓冲区和memcpys,为memcpy调用计时.您可以使用以下命令在命令行上指定备用缓冲区大小:./ big_memcpy_test [SIZE_BYTES]

#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] \n"
              << "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;
}
Run Code Online (Sandbox Code Playgroud)

构建CMake文件

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})
Run Code Online (Sandbox Code Playgroud)

检测结果

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
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,我们服务器上的memcpys和memset比我们笔记本电脑上的memcpys和memset要慢得多.

改变缓冲区大小

我尝试过从100MB到5GB的缓冲区都有类似的结果(服务器比笔记本电脑慢)

NUMA Affinity

我读到了与NUMA有性能问题的人,所以我尝试使用numactl设置CPU和内存亲和力,但结果保持不变.

服务器NUMA硬件

$ 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 
Run Code Online (Sandbox Code Playgroud)

笔记本电脑NUMA硬件

$ 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
Run Code Online (Sandbox Code Playgroud)

设置NUMA亲和力

$ numactl --cpunodebind=0 --membind=0 ./big_memcpy_test
Run Code Online (Sandbox Code Playgroud)

任何帮助解决这个问题非常感谢.

编辑:GCC选项

根据评论,我尝试使用不同的GCC选项进行编译:

使用-march和-mtune进行编译设置为native

g++ -std=c++0x -Wall -march=native -mtune=native -O3 -DNDEBUG -o big_memcpy_test main.cpp 
Run Code Online (Sandbox Code Playgroud)

结果:完全相同的性能(没有改进)

使用-O2而不是-O3进行编译

g++ -std=c++0x -Wall -march=native -mtune=native -O2 -DNDEBUG -o big_memcpy_test main.cpp
Run Code Online (Sandbox Code Playgroud)

结果:完全相同的性能(没有改进)

编辑:更改memset以写入0xF而不是0以避免NULL页面(@SteveCox)

使用0以外的值进行memset时没有改进(在这种情况下使用0xF).

编辑:Cachebench结果

为了排除我的测试程序过于简单,我下载了一个真正的基准测试程序LLCacheBench(http://icl.cs.utk.edu/projects/llcbench/cachebench.html)

我分别在每台机器上构建了基准测试,以避免架构问题.以下是我的结果.

笔记本电脑与服务器memcpy性能

请注意,在较大的缓冲区大小上,性能差异很大.测试的最后一个尺寸(16777216)在笔记本电脑上以18849.29 MB /秒的速度执行,在服务器上以6710.40的速度执行.这是性能差异的3倍.您还可以注意到服务器的性能下降比笔记本电脑更陡峭.

编辑:memmove()比服务器上的memcpy()快2倍

根据一些实验,我尝试在我的测试用例中使用memmove()而不是memcpy(),并在服务器上找到了2倍的改进.笔记本电脑上的Memmove()运行速度比memcpy()慢,但奇怪的是运行速度与服务器上的memmove()相同.这引出了一个问题,为什么memcpy这么慢?

更新了代码以测试memmove和memcpy.我必须将memmove()包装在一个函数中,因为如果我离开它内联GCC优化它并执行与memcpy()完全相同(我假设gcc将其优化为memcpy,因为它知道位置没有重叠).

更新结果

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
Run Code Online (Sandbox Code Playgroud)

编辑:天真的Memcpy

根据@Salgar的建议,我已经实现了我自己的天真memcpy功能并对其进行了测试.

天真的Memcpy来源

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++;
  }
}
Run Code Online (Sandbox Code Playgroud)

天真的Memcpy结果与memcpy()相比

Buffer Size: 1GB | memcpy (ms) | memmove(ms) | naiveMemcpy()
------------------------------------------------------------
Laptop 1         | 113         | 161         | 160
Server 1         | 301         | 159         | 159
Server 2         | 325         | 159         | 159
Run Code Online (Sandbox Code Playgroud)

编辑:装配输出

简单的memcpy源码

#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;
}
Run Code Online (Sandbox Code Playgroud)

装配输出:这在服务器和笔记本电脑上完全相同.我节省空间而不是粘贴两者.

        .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
Run Code Online (Sandbox Code Playgroud)

进展!!!!ASMLib程序

根据@tbenson的建议,我尝试使用memml的asmlib版本运行.我的结果最初很差但是在将SetMemcpyCacheLimit()更改为1GB(我的缓冲区的大小)后,我的速度与我的天真for循环相同!

坏消息是memmove的asmlib版本比glibc版本慢,它现在运行在300ms标记(与glibc版本的memcpy相同).奇怪的是,在我的SetMemcpyCacheLimit()大量的笔记本电脑上它会伤害性能......

在下面的结果中,标记为SetCache的行将SetMemcpyCacheLimit设置为1073741824.没有SetCache的结果不会调用SetMemcpyCacheLimit()

使用asmlib函数的结果:

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
Run Code Online (Sandbox Code Playgroud)

开始倾向于缓存问题,但是会导致什么呢?

小智 23

[我会将此作为评论,但没有足够的声誉这样做.]

我有一个类似的系统,看到类似的结果,但可以添加一些数据点:

  • 如果你颠倒你天真的方向memcpy(即转换为*p_dest-- = *p_src--),那么你可能会比前进方向的性能差得多(对我来说约为637毫秒).memcpy()在glibc 2.12中有一个变化,暴露了几个错误调用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缓冲区)以避免非临时存储在我的情况下性能翻倍(时间低至176毫秒).当然,那只与前向天真的表现相匹配,所以它并不是一流的.
  • 这些系统上的BIOS允许启用/禁用四个不同的硬件预取器(MLC Streamer Prefetcher,MLC Spatial Prefetcher,DCU Streamer Prefetcher和DCU IP Prefetcher).我尝试禁用每个,但这样做最好保持性能奇偶校验并降低一些设置的性能.
  • 禁用运行平均功率限制(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单插槽板上,我看到9.6 GB/s memcpy(104ms).Haswell系统的RAM是DDR3-1600(与其他系统相同).

更新

  • 我将CPU电源管理设置为Max Performance并在BIOS中禁用超线程.基于此/proc/cpuinfo,核心的时钟频率为3 GHz.然而,这奇怪地将内存性能降低了大约10%.
  • memtest86 + 4.10报告主内存的带宽为9091 MB/s.我找不到这是否与读,写或复制相对应.
  • STREAM基准报告13422 MB/s的副本,但他们数字节读取和写入,这样相当于〜6.5 GB/s的,如果我们要比较上述结果.


bok*_*kan 10

这看起来很正常.

管理带有两个CPU的8x16GB ECC记忆棒比使用2x2GB的单个CPU要困难得多.你的16GB硬盘是双面内存+它们可能有缓冲区+ ECC(甚至在主板级禁用)......所有这些都使数据路径更长时间.你也有2个CPU共享ram,即使你在另一个CPU上什么都不做,总是很少有内存访问.切换此数据需要一些额外的时间.只要看看与显卡共享一些内存的PC上的巨大性能损失.

你的服务器仍然是非常强大的数据泵.我不确定在现实生活中的软件中经常复制1GB,但我确信你的128GB比任何硬盘都快,甚至是最好的SSD,这是你可以利用你的服务器的地方.使用3GB进行相同的测试会使您的笔记本电脑着火.

这看起来是基于商用硬件的架构如何比大型服务器更有效的完美示例.在这些大型服务器上花费的钱可以买多少台消费者PC?

谢谢你提出非常详细的问题.

编辑:(花了我很长时间写这个答案,我错过了图形部分.)

我认为问题在于数据的存储位置.你能比较一下这个:

  • 测试一:分配两个500Mb ram的连续块并从一个复制到另一个(你已经完成的)
  • 测试二:分配20个(或更多)500Mb内存块并从第一个到最后一个复制,所以它们彼此相距很远(即使你不能确定它们的真实位置).

这样你就可以看到内存控制器如何处理远离彼此的内存块.我认为你的数据放在不同的内存区域,它需要在数据路径上的某个点进行切换操作,以便与一个区域进行通信,然后是另一个区域(双面内存存在这样的问题).

另外,您确定线程绑定到一个CPU吗?

编辑2:

内存有几种"区域"分隔符.NUMA是一个,但这不是唯一的.例如,双面支撑杆需要标记来指向一侧或另一侧.在图表中查看即使在笔记本电脑上(没有NUMA),性能也会因大块内存而降低.我不知道这一点,但memcpy的可使用硬件的功能来复制RAM(一种DMA)和该芯片必须小于缓存比你的CPU,这可以解释为什么与CPU哑副本比memcpy的速度更快.

  • ECC和缓冲开销,以及可能不同的CAS延迟,对于小缓冲区大小的~3%差异是一个很好的解释.但我认为问题的主要关注点是图表的最右侧,其中性能偏差三倍. (3认同)
  • 这并不能解释与naiveMemcpy相比较差的系统memcpy性能.http://stackoverflow.com/a/10300382/414279在Supermicro板上用NUMA解释了它.我是1x I7比2x I5解释还快.首先1x比2x快,I7比I5有更好的缓存. (2认同)

Lee*_*eor 8

基于IvyBridge的笔记本电脑中的某些CPU改进可能会比基于SandyBridge的服务器有所提升.

  1. 页面交叉预取 - 只要你到达当前页面的末尾,你的笔记本电脑CPU就会预先提取下一个线性页面,每次都会为你节省一个讨厌的TLB.要尝试缓解这种情况,请尝试为2M/1G页面构建服务器代码.

  2. 缓存替换方案似乎也已得到改善(看到一个有趣的逆向工程在这里).如果这个CPU确实使用了动态插入策略,那么它很容易阻止你复制的数据试图破坏你的Last-Level-Cache(由于它的大小无法有效地使用它),并为其他有用的缓存节省空间像代码,堆栈,页表数据等.).为了测试这个,您可以尝试使用流加载/存储(movntdq或类似的,您也可以使用gcc builtin)来重建您的天真实现.这种可能性可以解释大数据集大小的突然下降.

  3. 我相信一些改进也是使用字符串复制(这里),它可能适用于或不适用于此,具体取决于汇编代码的外观.您可以尝试使用Dhrystone进行基准测试,以测试是否存在固有差异.这也可以解释memcpy和memmove之间的区别.

如果您能够获得基于IvyBridge的服务器或Sandy-Bridge笔记本电脑,那么最简单的方法就是测试所有这些.

  • 在我的帖子的顶部我报告两个服务器上的规格.Sever 1是SandyBridge E5-2680,Server 2是IvyBridge E5-2650v2.两台服务器都具有相同的性能数字. (2认同)