Linux上的memcpy性能不佳


71

我们最近购买了一些新服务器,并且内存性能不佳。与我们的笔记本电脑相比,服务器的memcpy性能要慢3倍。

服务器规格

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

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

服务器2规格

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

笔记本电脑规格

  • 底盘:联想W530
  • CPU:1个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

编译器(在所有系统上)

$ gcc --version
gcc (GCC) 4.6.1

还根据@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;
}

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

试验结果

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硬件

$ 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

解决该问题的任何帮助将不胜感激。

编辑:GCC选项

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

将-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

结果:完全一样的性能(无改善)

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

使用0以外的值进行记忆设置(在这种情况下使用0xF)没有任何改善。

编辑:Cachebench结果

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

我在每台计算机上分别建立了基准,以避免体系结构问题。以下是我的结果。

笔记本电脑vs服务器性能

注意,较大的缓冲区在性能上有很大的不同。最后测试的大小(16777216)在笔记本电脑上为18849.29 MB /秒,在服务器上为6710.40。这大约是性能的3倍。您还可以注意到,服务器的性能下降比笔记本电脑要严重得多。

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

根据一些实验,我尝试在测试用例中使用memmove()而不是memcpy(),发现服务器上的性能提高了2倍。笔记本电脑上的Memmove()运行速度比memcpy()慢,但奇怪的是,其运行速度与服务器上的memmove()相同。这就引出了一个问题,为什么memcpy这么慢?

更新了代码以与memcpy一起测试memmove。我必须将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

编辑:天真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++;
  }
}

天真的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

编辑:程序集输出

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

进展!!!!asmlib

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

坏消息是memmove的asmlib版本比glibc版本要慢,它现在的运行时间为300毫秒(与memcpy的glibc版本相当)。奇怪的是,在笔记本电脑上,当我将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

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


您正在服务器上编译测试吗?
Collin Dauphinee 2014年

1
您可以检查它调用memcpy的代码吗?我最初的猜测是,服务器的malloc可能与笔记本电脑的malloc对齐方式不同。
Collin Dauphinee 2014年

2
您似乎没有使用任何特定于拱的标志进行编译,因此绝对应该对此进行公平测试。话虽这么说,这绝对是内存受限的操作,而且看起来服务器上的内存规格并不是真的更快,因此不应有太大的收获。服务器只有在通过缓存或寄存器进行工作时才应胜过笔记本电脑
Steve Cox

1
@nick否,您无需记忆页面,但将其设置为其他值
Steve Cox

1
另一件事是编写一个简单的memcpy和memmove并将其编译下来并比较它们的组装,以查看不同机器上的实现或优化是否存在显着差异。
Salgar 2014年

Answers:


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缓冲区)以避免在我的情况下非临时存储性能翻倍(时间降至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单插座板上,我看到9.6 GB / s memcpy(104毫秒)的速度。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的,如果我们要比较上述结果。

感谢您的信息。我正在阅读SuperMicro手册,并注意到BIOS中有一些“能效”设置。我想知道是否其中之一恰巧打开了na可能会损害性能?
尼克

@nick我明天将切换性能/效率设置。我相信将CPU缩放调节器设置为性能模式(例如,通过echo "performance" > /sys/devices/system/cpu/cpuXX/cpufreq/scaling_governorXX内核)也会产生类似的影响。
tbenson

我尝试使用memcpy的asmlib版本运行代码,并能够重现您的结果。memcpy()的默认版本与glibc memcpy具有相似的性能。将SetMemcpyCacheLimit()更改为1GB时,服务器上的memcpy时间减少到160ms!不幸的是,他的memmove()实现从160ms上升到300ms。这使我认为这是某种缓存问题。
尼克,

使用memmove和memcpy的asmlib版本更新了我的结果。
尼克,

1
memtest86+应该打印COPY速度-memtest86 + -4.20-1.1 / init.c第1220行使用memspeed((ulong)mapping(0x100), i*1024, 50, MS_COPY)调用。而memspeed()本身就是实现与cld; rep movsl超过内存段拷贝的循环50次迭代。
osgx 2014年

10

这对我来说似乎很正常。

与具有2x2GB的单个CPU相比,使用两个CPU管理8x16GB的ECC记忆棒要困难得多。您的16GB记忆棒是双面内存+它们可能具有缓冲区+ ECC(甚至在主板级别上处于禁用状态)...所有这些都会使到RAM的数据路径更长。您还有2个CPU共享内存,即使您在其他CPU上不执行任何操作,内存访问也始终很少。切换此数据需要一些额外的时间。只要看看与显卡共享内存的PC所损失的巨大性能即可。

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

这看起来像是一个很好的例子,说明基于商用硬件的体系结构比大型服务器更有效率。花在这些大型服务器上的钱买得起多少台家用PC?

感谢您提出的非常详细的问题。

编辑:(花了我这么长时间来写这个答案,我错过了图部分。)

我认为问题在于数据的存储位置。您能否比较一下:

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

这样,您将看到内存控制器如何处理彼此远离的内存块。我认为您的数据放在不同的内存区域中,并且它需要在数据路径上的某个点进行切换操作才能与一个区域然后是另一个区域进行通信(双面存储器存在这种问题)。

另外,您是否确保线程绑定到一个CPU?

编辑2:

有几种用于内存的“区域”定界符。NUMA是一个,但不是唯一一个。例如,两面的棍棒需要标记来标识一侧或另一侧。在您的图形上查看,即使在笔记本电脑上(即使没有NUMA),性能也会随着大量内存而降低。我不确定,但是memcpy可能使用硬件功能来复制ram(一种DMA),并且该芯片的缓存必须比CPU少,这可以解释为什么使用CPU的哑复制比memcpy更快。


3
ECC和缓冲开销以及可能不同的CAS延迟,很好地解释了在较小的缓冲区大小下约3%的差异。但是我认为问题的主要关注点是图表的最右侧,该位置的性能差异为三分之二。
Ben Voigt 2014年

2
与naiveMemcpy相比,这不能解释系统memcpy性能差的原因。stackoverflow.com/a/10300382/414279在Supermicro板上通过NUMA对其进行了说明。我赞成1x I7比2x I5的解释还快。前1x的速度比2x的速度快,而I7的缓存要比I5好。
rurban 2014年

@bokan我确保使用numactl在相同的CPU和NUMA控制器上运行的所有内容。这会将进程绑定到我指定的CPU和NUMA控制器。我已验证使用numactl --hardware命令将它们连接在一起。
尼克

8

与基于SandyBridge的服务器相比,基于IvyBridge的笔记本电脑在CPU方面的某些改进可能有助于实现这一目标。

  1. 跨页预取-每当您到达当前线性页的末尾时,笔记本计算机的CPU就会提前预取下一个线性页,从而每次都避免了讨厌的TLB丢失。要尝试缓解这种情况,请尝试为2M / 1G页面构建服务器代码。

  2. 缓存替换方案似乎也得到了改进(请参阅此处的有趣的反向工程)。如果确实该CPU使用动态插入策略,则可以轻松防止复制的数据尝试破坏您的Last-Level-Cache(由于大小原因,它最终无法有效使用),并为其他有用的缓存留出了空间例如代码,堆栈,页表数据等)。为了测试这一点,您可以尝试使用流式加载/存储(movntdq或类似的加载/存储,也可以使用gcc内置)来重建您的幼稚实现。这种可能性可以解释大数据集大小的突然下降。

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

如果您可以使用基于IvyBridge的服务器或Sandy-Bridge笔记本电脑,则将所有这些一起进行测试将是最简单的。


2
在我的帖子的顶部,我报告了两台服务器上的规格。服务器1是SandyBridge E5-2680,服务器2是IvyBridge E5-2650v2。两台服务器均具有相同的性能编号。
尼克

@nick,嗯,错过了v2部分。您可能以为它们可以使名称更易区分...好的,我的观点是正确的,尽管第二个项目符号在服务器和客户端产品之间的外观和行为可能会非常不同,因为它们的“ uncore”完全不同,因此它仍然可能适用。
Leeor 2014年

@Leeor-FWIW,使用2MB或1G页面不能解决预取问题:预取逻辑仍然以4K粒度运行,并且实际上它主要是在查看物理地址(即,它不知道当前流恰好位于在2MB的页面中,因此它不会预取超过4K的边界)。就是说,就在Ivy Bridge之前,有一个“下一页预取器”,它试图通过在访问进入下一页时快速重新启动预取来至少部分地解决此问题。目前尚不清楚它如何与2MB页面交互。
BeeOnRope

4

我修改了基准以在Linux中使用nsec计时器,并发现在不同处理器上都有相似的变化,所有处理器都具有相似的内存。所有正在运行的RHEL6。编号在多个运行中是一致的。

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的结果

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库代码试图变得太聪明了。


3

上面已经回答了这个问题,但是无论如何,这是使用AVX的实现,如果您担心的话,对于大型副本来说应该更快。

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

3

这些数字对我来说很有意义。这里实际上有两个问题,我都会回答。

不过,首先,我们需要一个思维模型,说明在现代Intel处理器之类的设备上有多大的1内存传输。该描述是近似的,细节在体系结构之间可能有所变化,但是高级思想是相当固定的。

  1. L1数据高速缓存中的加载未命中时,将分配一个行缓冲区,该行缓冲区将跟踪未命中请求,直到填充为止。如果它命中了L2高速缓存,则可能会持续很短的时间(十几个周期左右),如果完全丢失到DRAM,则可能会更长(100+纳秒)。
  2. 每个内核1的这些行缓冲区数量有限,一旦它们满了,其他丢失将等待一个。
  3. 除了用于需求3加载/存储的这些填充缓冲区之外,还有其他缓冲区用于DRAM和L2之间的内存移动以及预取使用的较低级缓存。
  4. 内存子系统本身具有最大带宽限制,您可以在ARK上方便地找到它。例如,Lenovo笔记本电脑中的3720QM的限制为25.6 GB。此限制基本上是有效频率(1600 Mhz)乘以每次传输8个字节(64位)乘以通道数(2)的乘积:1600 * 8 * 2 = 25.6 GB/s。手上的服务器芯片每个插槽的峰值带宽为51.2 GB / s,总系统带宽约为102 GB / s。

    与其他处理器功能不同,整个芯片上通常只有一个可能的理论带宽数,因为它仅取决于记录的值,这些值在许多不同的芯片甚至架构之间通常是相同的。期望DRAM以准确的理论速率交付是不现实的(由于各种低水平的关注,在这里稍作讨论),但是您通常可以获得大约90%或更多的速率 。

因此,(1)的主要结果是您可以将对RAM的丢失视为一种请求响应系统。对DRAM的未命中会分配一个填充缓冲区,并在请求返回时释放该缓冲区。每个CPU中只有10个缓冲区用于需求未命中,这严格限制了单个CPU可以生成的需求内存带宽(取决于延迟)。

例如,假设您E5-2680对DRAM的延迟为80ns。每个请求都会带来一个64字节的缓存行,因此您只是向DRAM发出了一系列请求,因此您希望获得微不足道的吞吐量64 bytes / 80 ns = 0.8 GB/s,并且memcpy由于需要读取,因此将其再次切成一半(至少)以获得一个数字。写。幸运的是,您可以使用10个行填充缓冲区,因此可以将10个并发请求重叠到内存中,并将带宽增加10倍,从而使理论带宽为8 GB / s。

如果您想深入了解更多细节,则该线程几乎是纯金。您会发现约翰·麦卡平(John McCalpin)的事实和数据,又称“带宽博士将是下面的常见主题。

因此,让我们进入细节并回答两个问题...

为什么memcpy比服务器上的memmove或手动滚动副本要慢得多?

您证明便携式计算机系统memcpy在大约120毫秒内完成了基准测试,而服务器部件大约需要300毫秒。您还表明,这种速度下降并不是根本原因,因为您可以使用memmove并且手动滚动(以下简称hrm)达到约160毫秒的时间,比笔记本电脑的性能要近得多(但仍然慢)。

上面我们已经表明,对于单个内核,带宽受总可用并发性和延迟的限制,而不是受DRAM带宽的限制。我们希望服务器部件可能具有更长的延迟,但不会300 / 120 = 2.5x更长!

答案在于流式存储(又称非临时性)存储memcpy您正在使用的libc版本会使用它们,但memmove不会。您通过memcpy不使用它们的“天真”来确认了同样多的内容,以及我配置为asmlib同时使用流式存储(慢速)和不使用(快速)。

流存储损害了单个CPU编号,因为:

  • (A)它们阻止预取将要存储的行带入缓存,这允许更多的并发,因为预取硬件除了需要加载/存储使用的10个填充缓冲区之外,还具有其他专用缓冲区。
  • (B)众所周知,E5-2680对于流媒体商店来说特别慢

在上述链接的线程中,John McCalpin的引文更好地解释了这两个问题。关于预取有效性和流存储,他说

使用“普通”存储区,L2硬件预取器可以提前获取行,并减少占用行填充缓冲区的时间,从而增加持续带宽。另一方面,对于流存储(旁路缓存),存储的行填充缓冲区条目在将数据传递到DRAM控制器所需的全部时间内都被占用。在这种情况下,可以通过硬件预取来加速负载,但是存储却不能,因此可以加快速度,但是不如同时加速负载和存储时的负载要快。

……然后,对于E5上流式存储的明显更长的延迟,他说

Xeon E3更简单的“非核心”功能可能会大大降低流媒体商店的Line Fill Buffer占用率。Xeon E5具有更复杂的环形结构来进行导航,以便将流存储从核心缓冲区切换到内存控制器,因此占用率的差异可能会大于内存(读取)延迟。

尤其是,麦卡平博士测得E5的速度是“非客户端”芯片的1.8倍,但OP报告的2.5倍的速度与STREAM TRIAD的1.8倍得分一致。负载与存储的比例为2:1,而存储与存储的比例为memcpy1:1,则存储是有问题的部分。

这并不会使流媒体成为一件坏事-实际上,您是在等待时间与总带宽消耗较小之间进行权衡。由于使用单个内核时并发性受到限制,因此获得的带宽较少,但是避免了所有对所有权的读取流量,因此,如果同时在所有内核上运行测试,则可能会看到(小的)好处。

到目前为止,其他用户使用相同的CPU报告了完全相同的速度下降,而不只是软件或硬件配置的假象。

为什么在使用普通商店时服务器部分仍然较慢?

即使更正了非临时存储问题,您仍会看到160 / 120 = ~1.33x服务器部分的运行速度明显下降。是什么赋予了?

嗯,常见的谬误是服务器CPU在各个方面都更快或至少等于客户端。事实并非如此-您在服务器部件上所支付的费用(通常为每片2,000美元左右)通常是(a)更多核心(b)更多内存通道(c)支持更多总RAM(d)支持“企业级”功能,例如ECC,虚拟化功能等5

实际上,就延迟而言,服务器部分通常仅等于或慢于其客户机4部分。当涉及到内存延迟时,尤其如此,因为:

  • 服务器部分具有更大的可伸缩性,但是复杂的“非核心”通常需要支持更多的核心,因此到RAM的路径更长。
  • 服务器部件支持更多的RAM(100 GB或几TB的RAM),这通常需要电缓冲器来支持如此大的数量。
  • 就像在OP的情况下一样,服务器部分通常是多路插座,这增加了跨路连贯性对内存路径的关注。

因此,服务器部分的延迟通常比客户端部分长40%至60%。对于E5,您可能会发现〜80 ns是典型的RAM延迟,而客户端部分接近50 ns。

因此,受RAM延迟限制的任何东西在服务器部件上的运行速度都会变慢,事实证明,memcpy 在单个内核上受延迟限制的东西。这很令人困惑,因为memcpy 好像带宽测量,对吗?如上所述,一个内核没有足够的资源来一次向飞行中的RAM发出足够的请求以接近RAM带宽6,因此性能直接取决于延迟。

另一方面,客户端芯片具有较低的延迟和较低的带宽,因此一个内核更接近于饱和带宽(这通常就是为什么流存储在客户端部分上大获胜的原因-即使是单个内核也可以接近RAM带宽,流存储提供的50%的存储带宽减少有很大帮助。

参考文献

有很多不错的资源可以阅读有关此内容的更多信息,这里有一些。


1我只是说比LLC稍大。对于适合于LLC(或更高缓存级别)的副本,其行为非常不同。OPsllcachebench图显示,实际上,性能偏差仅在缓冲区开始超过LLC大小时才开始。

2特别地,数行填充缓冲器显然在10一直恒定几代,包括在这个问题中提到的架构。

3当我们在这里说需求时,是指它与代码中的显式加载/存储相关联,而不是说是由预取带来的。

4当我在这里提到服务器部分时,我的意思是带有服务器uncore的CPU 。这在很大程度上意味着E5系列,因为E3系列通常使用client uncore

5将来,您似乎可以在此列表中添加“指令集扩展”,因为它似乎AVX-512只会出现在Skylake服务器部件上。

根据每一点定律6,在80 ns的延迟下,我们(51.2 B/ns * 80 ns) == 4096 bytes始终需要飞行中或有64条高速缓存行才能达到最大带宽,但是一个内核提供的缓存少于20条。


0

服务器1规格

  • CPU:2个Intel Xeon E5-2680 @ 2.70 Ghz

服务器2规格

  • CPU:2个Intel Xeon E5-2650 v2 @ 2.6 Ghz

根据Intel ARK,E5-2650E5-2680均具有AVX扩展名。

CMake文件生成

这是您问题的一部分。CMake为您选择了一些较差的标志。您可以通过运行进行确认make VERBOSE=1

您应该将-march=native和都添加-O3CFLAGS和中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*。它是一种丢弃有关指针的信息的方法。您可能应该保留以下信息:

void doMemmove(void* pDest, const void* pSource, std::size_t sizeBytes)
{
  memmove(pDest, pSource, sizeBytes);
}

也许像下面这样:

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单元时,从堆中涉及指针(SAN的潜在void*malloc问题)。

最后,有一阵子,Clang在使用时没有使用本机CPU扩展-march=native。例如,请参见Ubuntu Issue 1616723,Clang 3.4仅发布SSE2Ubuntu Issue 1616723,Clang 3.5仅发布SSE2,而Ubuntu Issue 1616723,Clang 3.6仅广告SSE2

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.