关于C ++:Linux上的memcpy性能不佳

Poor memcpy Performance on Linux

我们最近购买了一些新服务器,并且内存性能不佳。与我们的笔记本电脑相比,服务器的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
  • 好的。

    操作系统

    好的。

    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)

    好的。

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

    好的。

    laptop vs server memcpy performance

    好的。

    注意,较大的缓冲区在性能上有很大的不同。最后测试的大小(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(由于大小原因,它最终无法有效使用),并为其他有用的缓存留出了空间。如代码,堆栈,页表数据等)。为了测试这一点,您可以尝试使用流负载/存储(movntdq或类似的负载/存储,也可以使用内置的gcc)来重建您的幼稚实现。这种可能性可以解释大数据集大小的突然下降。

  • 我相信字符串复制(在这里)也进行了一些改进,取决于您的汇编代码的外观,它在这里可能适用也可能不适用。您可以尝试使用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处理器等设备上的工作量。该描述是近似的,细节可能因体系结构的不同而有所变化,但是高级思想是相当固定的。

    好的。

  • L1数据高速缓存中的加载未命中时,将分配一个行缓冲区,该行缓冲区将跟踪未命中请求,直到填充为止。如果它命中L2缓存,则可能会持续很短的时间(十几个周期左右),如果错过了进入DRAM的时间,则可能更长(100+纳秒)。
  • 每个core1的这些行缓冲区数量有限,一旦它们满了,更多的未命中将停止等待。
  • 除了用于需求3加载/存储的这些填充缓冲区之外,还有其他缓冲区用于DRAM和L2之间的内存移动以及预取使用的较低级缓存。
  • 内存子系统本身具有最大带宽限制,您可以在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或手动复制慢得多?

    您证明便携式计算机系统在大约120毫秒内完成了memcpy基准测试,而服务器部件大约需要300毫秒。您还证明了这种慢速并不是根本原因,因为您可以使用memmove和您的手动记忆(此后称为hrm)来实现大约160毫秒的时间,该时间要短得多(但仍比慢)笔记本电脑的性能。

    好的。

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

    好的。

    答案在于流式存储(又称非临时性)存储。您正在使用的memcpy的libc版本使用它们,而memmove则没有。您通过不使用它们的"天真" memcpy以及我配置的asmlib都使用(慢速)和不使用(快速)流媒体来确认了很多。

    好的。

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

    好的。

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

    在上述链接的主题中,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具有
    进行导航以传递更复杂的环结构
    从核心缓冲区向存储控制器流式传输存储,因此
    占用率的差异可能大于内存(读取)的差异
    潜伏。

    好的。

    尤其是,麦卡平博士测得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的大量内存延迟结果(请参见MemLatX86NewMemLat)链接
  • 详细分析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都添加到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*,这看起来也有点怀疑。它是一种丢弃有关指针的信息的方法。您可能应该保留以下信息:

    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。