7 题: Linux上的memcpy性能不佳

在...创建的问题 Thu, Apr 3, 2014 12:00 AM

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

服务器规范

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

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

服务器2规格

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

笔记本电脑规格

  • 机箱:Lenovo 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

编译器(在所有系统上)

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

改变缓冲区大小

我尝试过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进行编译设置为本机

 
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以外的值进行memset时没有改善(在这种情况下使用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这么慢?

更新了代码以测试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

编辑:天真的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

PROGRESS !!!! ASMLib程序强>

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

坏消息是memmove的asmlib版本比glibc版本慢,它现在运行在300ms标记(与glcc版本的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

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

    
69
  1. 您是否在服务器上编译测试?
    2014-04-01 18:23:17Z
  2. 你能检查一下它为memcpy调用的代码吗?我最初的猜测是服务器的malloc可能与笔记本电脑的对齐方式不同。
    2014-04-01 18:42:06Z
  3. 你似乎没有使用任何特定于arch的标志进行编译,你绝对应该对此进行公平的测试。话虽这么说,这绝对是一个内存有限的操作,看起来内存规格在服务器上并不是真的更快,所以不应该有巨大的收益。只有当它从缓存或寄存器
    工作时,服务器才能胜过笔记本电脑
    2014-04-01 18:46:15Z
  4. @ nick没有你必须memset这些页面,但是将它们设置为其他值
    2014-04-01 18:58:03Z
  5. 要做的另一件事是编写一个简单的memcpy和memmove并将它们编译下来并比较它们的组合,以查看实现中是否存在任何重大差异或在不同的机器上进行优化。
    2014-04-02 14:10:47Z
  6. 醇>
    7答案                              7 跨度>                         

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

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

    • 如果你反转你的幼稚memcpy的方向(即转换为*p_dest-- = *p_src--),那么你可能会比前进方向的性能差得多(对我来说约为637毫秒)。 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缓冲区)以避免非临时存储在我的情况下性能翻倍(时间低至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单插槽板上,我看到了memcpy(104ms)的9.6 GB /s。 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。
    23
    2014-04-05 04:00:25Z
    1. 感谢您提供的信息。我正在阅读SuperMicro手册并注意到BIOS中“能效”的几个设置。我想知道其中一个是否恰好打开了na可能会伤害性能?
      2014-04-02 22:02:48Z
    2. @ nick我将在明天切换性能/效率设置。我相信将CPU缩放调控器设置为性能模式(例如,通过芯片XX的echo "performance" > /sys/devices/system/cpu/cpuXX/cpufreq/scaling_governor)也会产生类似的影响。
      2014-04-03 01:42:54Z
    3. 我尝试使用asmlib版本的memcpy运行我的代码,并能够重现您的结果。 memcpy()的默认版本与glibc memcpy具有相似的性能。将SetMemcpyCacheLimit()更改为1GB时,服务器上的memcpy时间降至160ms!不幸的是,他的memmove()实现从160ms上升到300ms。这让我觉得它是某种缓存问题。
      2014-04-03 02:59:45Z
    4. 使用memmove和memcpy的asmlib版本更新了我的结果。
      2014-04-03 03:19:09Z
    5. memtest86+应该打印COPY速度 - memtest86 + -4.20-1.1 /init.c line 1220 使用memspeed((ulong)mapping(0x100), i*1024, 50, MS_COPY)呼叫。 memspeed()本身是用 cld; rep movsl 实现的>在内存段上进行50次迭代复制循环。
      2014-04-29 04:24:06Z
    6. 醇>

    这对我来说很正常。

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

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

    这看起来是基于商品硬件的架构如何比大型服务器更高效的完美示例。 H许多消费者的个人电脑可以用这些大型服务器上花的钱吗?

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

    编辑:(花了我很长时间才写下这个答案,我错过了图表部分。)

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

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

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

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

    编辑2:

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

        
    10
    2014-04-03 09:54:36Z
    1. ECC和缓冲开销以及可能不同的CAS延迟,对于小缓冲区大小的〜3%差异是一个很好的解释。但我认为问题的主要关注点是图表的最右侧,其中性能偏差三倍。
      2014-04-02 16:41:55Z
    2. 这并不能解释与naiveMemcpy相比较差的系统memcpy性能。 stackoverflow.com/a/10300382/414279 在Supermicro主板上使用NUMA进行了解释。我是1x I7比2x I5解释还快。前1x比2x快,I7有更好的缓存,然后是I5。
      2014-04-02 16:47:17Z
    3. @ bokan我确保一切都在使用numactl在同一个CPU和NUMA控制器上运行。这会将进程绑定到我指定的CPU和NUMA控制器。我已使用numactl --hardware命令验证它们已连接在一起。
      2014-04-02 19:18:57Z
    4. 醇>

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

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

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

    3. 我相信也会对字符串副本进行一些改进(此处) ,它可能适用于此处,也可能不适用,具体取决于汇编代码的外观。您可以尝试使用Dhrystone 测试是否存在内在差异。这也可以解释memcpy和memmove之间的区别。

    4. 醇>

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

          
    8
    2014-04-02 17:28:03Z
    1. 在我的帖子的顶部,我在两台服务器上报告规格。 Sever 1是SandyBridge E5-2680,Server 2是IvyBridge E5-2650v2。两台服务器都具有相同的性能数字。
      2014-04-02 22:06:44Z
    2. @ nick,嗯,错过了v2部分。你可能会认为他们会让这些名字更加明显......好吧,我的立场得到了纠正,虽然第二颗子弹在服务器和客户端产品之间看起来和行为看起来很不一样,因为它们有完全不同的“未知”,所以它仍然可能适用。
      2014-04-02 22:40:24Z
    3. @ Leeor - FWIW,使用2MB或1G页面无法解决预取问题:预取逻辑仍以4K粒度运行,实际上它主要是查看物理地址(即,它不知道当前流恰好位于2MB页面中,因此它不会预取超过4K边界)。也就是说,就像Ivy Bridge一样,有一个“下一页预取器”试图通过在访问进入下一页时快速重新开始预取来至少部分解决这个问题。目前尚不清楚它如何与2MB页面交互。
      2017-01-31 22:07:18Z
    4. 醇>

    我修改了基准测试以在Linux中使用nsec计时器,并在不同处理器上发现了类似的变体,所有处理器都具有相似的内存。所有正在运行的RHEL 6.数字在多次运行中都是一致的。

     
    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个字节。 在这些英特尔处理器上,它没有明显的区别。 Cache将所有字节操作合并到最小数量的内存操作中。我怀疑gcc库代码试图太聪明。

        
    4
    2014-04-02 19:33:09Z

    上面已经回答了这个问题,但无论如何,这是一个使用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
    2017-05-23 12:26:39Z

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

    首先,我们需要有一个心理模型,说明有多大 1 内存传输在现代英特尔处理器上运行。这个描述是近似,细节可能会从架构到架构有所改变,但高层次的想法是相当稳定的。

    1. L1数据高速缓存中没有加载时,会分配一个行缓冲区,它将跟踪未命中请求,直到它被填满。如果它在L2缓存中命中,可能会持续很短的时间(十几个周期左右),如果它一直错过DRAM,可能会持续很长时间(100+纳秒)。
    2. 这些行缓冲区每个核心 1 的数量有限,一旦它们已满,进一步的未命中将停止等待一个。
    3. 除了用于 demand 3 加载/存储的这些填充缓冲区之外,还有用于DRAM和L2之间的内存移动的额外缓冲区以及预取所使用的较低级别缓存。
    4. 内存子系统本身有一个最大带宽h limit ,您可以在ARK上方便地找到它。例如,联想笔记本电脑中的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%或更多。

    5. 醇>

      因此(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或hand rolled copy慢得多?

      您展示了笔记本电脑系统在 120 ms 中执行memcpy基准测试,而服务器部件需要 300 ms 。你还表明,这种缓慢主要不是根本性的,因为你能够使用memmove和你的手卷memcpy(以下简称hrm)来实现大约 160 ms 的时间,更接近(但仍然比笔记本电脑的性能慢。

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

      答案在于流媒体(又名非临时)商店。您正在使用的libc版本memcpy使用它们,但memmove不使用它们。你确认了你的“天真”memcpy也没有使用它们,以及我配置asmlib都使用流式存储(慢)而不是(快)。

      流媒体商店会损害单CPU 号码,因为:

    • (A)它们阻止预取将待存储的行引入缓存,这允许更多的并发性,因为预取硬件具有超出10 填充缓冲区的其他专用缓冲区需求加载/存储使用。
    • (B)众所周知,E5-2680是特别慢的流媒体商店。

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

      

    对于“普通”商店,L2硬件预取器可以获取行   提前并减少线路填充缓冲器占用的时间,   从而增加持续带宽。在其他方面r手,用   流(缓存旁路)存储,行填充缓冲区条目   商店被占用传递数据所需的全部时间   DRAM控制器。在这种情况下,加载可以加速   硬件预取,但商店不能,所以你得到一些加速,   但是如果装载和存储都没有那么多   加速。

    ...然后,对于E5上流媒体商店显然更长的延迟,他说

      

    Xeon E3的简单“非核心”可能导致显着降低   流式商店的线路填充缓冲区占用率。 Xeon E5有一个   更复杂的环形结构导航,以便交出   流存储从核心缓冲区到内存控制器,所以   占用率可能比内存大一些(读取)   等待时间。

    特别是,McCalpin博士测量的E5与“客户端”非核心芯片相比减少了约1.8倍,但OP报告的2.5倍减速与STREAM TRIAD报告的1.8x评分一致,负载比例为2:1,商店,memcpy为1:1,商店是问题部分。

    这不会使流式传输成为一件坏事 - 实际上,您需要通过延迟来减少总带宽消耗。您获得的带宽较少,因为在使用单核时您的并发性受限,但是您可以避免所有读取所有权流量,因此如果您在所有核心上同时运行测试,您可能会看到(小)优势。

    到目前为止,作为您的软件或硬件配置的工件,其他用户使用相同的CPU报告完全相同的减速。

    使用普通商店时,为什么服务器部件仍然更慢?

    即使纠正了非临时存储问题,您仍然仍然在服务器部件上看到大约160 / 120 = ~1.33x减速。是什么给了什么?

    这是一个常见的谬论,即服务器CPU在所有方面都更快,或者至少与客户端相同。事实并非如此 - 您在服务器部件上支付的费用(通常为每片2,000美元左右)主要是(a)更多核心(b)更多内存通道(c)支持更多总RAM(d)支持“ enterprise-ish“功能,如ECC,虚拟化功能等 5

    事实上,延迟方面,服务器部件通常只与其客户端 4 部件相同或更慢。当谈到内存延迟时,尤其如此,因为:

    • 服务器部件具有更具可扩展性但复杂的“非核心”,通常需要支持更多核心,因此RAM的路径更长。
    • 服务器部件支持更多RAM(100 GB或几TB),这通常需要电子缓冲器支持这么大的数量。
    • 在OP案例中,服务器部件通常是多插槽的,这会在内存路径中增加交叉插槽一致性问题。

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

    因此,RAM延迟受限的任何事情都会在服务器部件上运行得更慢,事实证明,单核上的memcpy 受延迟限制。那令人困惑,因为memcpy 似乎就像带宽测量一样,对吗?如上所述,单个内核没有足够的资源来一次保留足够的RAM请求以接近RAM带宽 6 ,因此性能直接取决于延迟。

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

    参考

    有很多好消息来源可以阅读更多关于这个东西,这里有几个。


    1 large 我的意思是比LLC大一些。对于适合LLC(或任何更高的缓存级别)的副本,行为是非常不同的。 OPs llcachebench图表显示,实际上性能偏差仅在缓冲区开始超过LLC大小时开始。

    2 特别是,行填充缓冲区的数量显然已经持续了几代,包括这个问题中提到的架构。

    3 当我们在这里说 demand 时,我们的意思是它与代码中的显式加载/存储相关联,而不是说是由预取引入。

    4 当我在这里引用服务器部分时,我的意思是带有服务器非核心的CPU。这在很大程度上意味着E5系列,因为E3系列通常使用客户端uncore

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

    6 按照小法律的延迟80 ns,我们在飞行中始终需要(51.2 B/ns * 80 ns) == 4096 bytes或64个缓存线才能达到最大带宽,但一个核心提供的带宽不到20个。

        
    3
    2017-01-31 17:23:01Z
      

    服务器1规格

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

    服务器2规格

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

    根据英特尔ARK, E5-2650 E5-2680 有AVX扩展程序。

      

    构建CMake文件

    这是您问题的一部分。 CMake为你选择一些相当差的旗帜。您可以通过运行make VERBOSE=1来确认它。

    您应该将-march=native-O3添加到CFLAGSCXXFLAGS。您可能会看到性能的显着提高。它应该参与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单元(没有潜在的void*malloc问题)。

    最后,有一段时间,Clang在usi时没有使用本机CPU扩展ng -march=native。例如,参见 Ubuntu Issue 1616723,Clang 3.4仅宣传SSE2 Ubuntu Issue 1616723,Clang 3.5仅宣传SSE2 Ubuntu Issue 1616723,Clang 3.6仅宣传SSE2

        
    0
    2017-05-23 10:30:58Z
来源放置 这里