我们最近购买了一些新服务器,memcpy的性能很差.与笔记本电脑相比,memcpy在服务器上的性能要慢3倍.

Server Specs

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

编辑:我还在另一台规格稍高的服务器上进行测试,结果与上述服务器相同

Server 2 Specs

  • 底盘和摩拜:超微10227GR-TRFT
  • CPU:2个Intel Xeon E5-2650 v2@2.6 GHz
  • 内存:8x 16 GB DDR3 1866 MHz

Laptop Specs

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

Operating System

$ 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

Compiler (on all systems)

$ gcc --version
gcc (GCC) 4.6.1

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

Test Code

#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;
}

CMake File to Build

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})

Test Results

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和memset比笔记本电脑上的memcpys和memset慢得多.

Varying buffer sizes

我试过100MB到5 GB的缓冲区,结果都差不多(服务器比笔记本电脑慢)

NUMA Affinity

我读到有人在使用NUMA时遇到性能问题,所以我try 使用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 

笔记本电脑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

设置NUMA关联性

$ numactl --cpunodebind=0 --membind=0 ./big_memcpy_test

非常感谢您对解决此问题的任何帮助.

Edit: GCC Options

根据我try 使用不同GCC选项编译的 comments :

在将-march和-mtune设置为native的情况下编译

g++ -std=c++0x -Wall -march=native -mtune=native -O3 -DNDEBUG -o big_memcpy_test main.cpp 

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

使用-O2而不是-O3编译

g++ -std=c++0x -Wall -march=native -mtune=native -O2 -DNDEBUG -o big_memcpy_test main.cpp

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

Edit: Changed memset to write 0xF instead of 0 to avoid NULL page (@SteveCox)

当memsetting的值不是0(在本例中使用0xF)时,没有任何改进.

Edit: Cachebench results

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

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

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

请注意,非常大的差异在于缓冲区大小较大时的性能.最后测试的大小(16777216)在笔记本电脑上以18849.29 MB/秒的速度执行,在服务器上以6710.40 MB/秒的速度执行.这大约是性能上的3倍之差.您还可以注意到,服务器的性能下降比笔记本电脑大得多.

Edit: memmove() is 2x FASTER than memcpy() on the server

基于一些实验,我try 在测试用例中使用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

Edit: Naive Memcpy

根据@salgar的建议,我实现了自己的天真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++;
  }
}

简单的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

Edit: Assembly Output

简单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;
}

汇编输出:这在服务器和笔记本电脑上都是完全相同的.我是在节省空间,而不是两个都粘贴.

        .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

PROGRESS!!!! asmlib

根据@tbenson的建议,我试着用asmlib版的memcpy运行.我的结果最初很差,但是在把StEnMcPyCaseLimeId()更改为1GB(我的缓冲区的大小)之后,我的运行速度与我的幼稚循环速度相当.

坏消息是,asmlib版本的memmove比glibc版本慢,它现在的运行速度是300ms(与glibc版本的memcpy持平).奇怪的是,在笔记本电脑上,当我将MemcpyCacheLimit()设置为大量时,会影响性能……

在下面的结果中,标有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

开始倾向于缓存问题,但这是什么原因呢?

推荐答案

[我想就此发表 comments ,但没有足够的声誉这么做.]

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

  • 如果您反转您的朴素memcpy的方向(即转换为*p_dest-- = *p_src--),那么您得到的性能可能比正向差得多(对于我来说大约637毫秒).glibc 2.12中的memcpy()中有一个更改,expose 了在重叠缓冲区(http://lwn.net/Articles/414467/)上调用memcpy的几个错误,我相信这个问题是由切换到向后操作的memcpy版本引起的.因此,向后拷贝和向前拷贝可以解释memcpy()/memmove()的差异.
  • 不使用非时态存储似乎更好.许多优化的memcpy()实现都会切换到非时态存储(不缓存)以获得较大的缓冲区(即,大于最后一级缓存).我测试了Agner Fog版本的memcpy(http://www.agner.org/optimize/#asmlib),发现它的速度与glibc版本大致相同.然而,asmlib具有允许设置阈值的功能(SetMemcpyCacheLimit),在该阈值之上使用非时态存储.将该限制设置为8GiB(或略大于1Gib缓冲区),以避免非临时存储在我的情况下性能加倍(时间降至176ms).当然,这只是符合前进方向的幼稚表现,所以它不是一流的.
  • 这些系统上的BIOS允许启用/禁用四种不同的硬件预取器(MLC拖缆预取器、MLC空间预取器、DCU拖缆预取器和DCU IP预取器).我try 禁用每种设置,但这样做最多只能维持性能对等,并降低一些设置的性能.
  • 禁用运行平均功率限制(RAPL)DRAM模式没有任何影响.
  • 我可以访问运行Fedora 19(glibc 2.17)的其他超级微系统.有了超微型X9DRG-HF板、Fedora 19和Xeon E5-2670 CPU,我看到了与上述类似的性能.在运行Xeon E3-1275 v3(Haswell)和Fedora 19的超微型X10SLM-F单插座板上,我看到了9.6 GB/s的memcpy(104ms).Haswell系统上的RAM为DDR3-1600(与其他系统相同).

UPDATES

  • 我将CPU电源管理设置为最高性能,并在BIOS中禁用了超读功能.以/proc/cpuinfo为基础,内核的时钟频率为3 GHz.然而,这奇怪地降低了大约10%的内存性能.
  • memtest86+4.10报告主内存的带宽为9091 MB/s.我找不到这是否对应于读、写或复制.
  • STREAM benchmark报告的拷贝速率为13422 MB/s,但它们将字节数计为读写字节数,因此,如果我们想与上述结果进行比较,这相当于~6.5 GB/s.

C++相关问答推荐

C sscanf没有捕获第二个参数

带双指针的2D数组

为什么在C中设置文件的位置并写入文件,填充空字符?

va_copy的使用是未定义的行为吗?

C:二进制搜索和二进制插入

以前版本的tty_ldisc_ops.ioctl()是否也需要文件参数?

为什么我一直收到分段错误?

将宏值传递给ARM链接器,该链接器将变量放置在特定位置

C在声明带有值的数组时,声明大小有用吗?

从uint8_t*转换为char*可接受

为什么我的Hello World EFI程序构建不正确?

关于";*(++p)->;t";、&++p->;t";和&q;++*p->;t";的问题

指向不同类型的指针是否与公共初始序列规则匹配?

C堆栈(使用动态数组)realloc内存泄漏问题

试图创建一个基本的Word克隆,但遇到了障碍

在C中使用无符号整数模拟有符号整数

将size_t分配给off_t会产生符号转换错误

atoi函数最大长-长误差的再创造

C/C++编译器可以在编译过程中通过按引用传递来优化按值传递吗?

在链表中插入一个值