74

最近、いくつかの新しいサーバーを購入しましたが、memcpy のパフォーマンスが低下しています。memcpy のパフォーマンスは、ラップトップと比較してサーバーで 3 倍遅くなります。

サーバー仕様

  • シャーシとモボ:SUPER MICRO 1027GR-TRF
  • CPU: 2x Intel Xeon E5-2680 @ 2.70 Ghz
  • メモリ: 8x 16GB DDR3 1600MHz

編集:わずかに高い仕様の別のサーバーでもテストしており、上記のサーバーと同じ結果が表示されます

サーバー 2 の仕様

  • シャーシとモボ: SUPER MICRO 10227GR-TRFT
  • CPU: 2x Intel Xeon E5-2650 v2 @ 2.6 Ghz
  • メモリ: 8x 16GB DDR3 1866MHz

ラップトップの仕様

  • シャーシ: レノボ W530
  • CPU: 1x インテル 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 でもテストされています。コンパイラ間のパフォーマンスの違いはありませんでした。

テスト コード 以下のテスト コードは、製品コードで発生している問題を再現するための定型テストです。このベンチマークが単純であることは承知していますが、問題を悪用して特定することができました。このコードは、2 つの 1GB バッファーとそれらの間に memcpy を作成し、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

ご覧のとおり、サーバーの memcpy と memset は、ラップトップの memcpy と 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 

結果: まったく同じパフォーマンス (改善なし)

-O3 の代わりに -O2 を使用してコンパイルする

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

結果: まったく同じパフォーマンス (改善なし)

編集: NULL ページを回避するために、0 ではなく 0xF を書き込むように memset を変更しました (@SteveCox)

0 以外の値で memsetting しても改善はありません (この場合は 0xF を使用)。

編集:キャッシュベンチの結果

私のテスト プログラムが単純すぎることを除外するために、実際のベンチマーク プログラム LLCacheBench ( http://icl.cs.utk.edu/projects/llcbench/cachebench.html )をダウンロードしました。

アーキテクチャの問題を回避するために、各マシンで個別にベンチマークを構築しました。以下は私の結果です。

ラップトップとサーバーの memcpy のパフォーマンス

非常に大きな違いは、大きなバッファ サイズでのパフォーマンスにあることに注意してください。テストされた最後のサイズ (16777216) は、ラップトップで 18849.29 MB/秒、サーバーで 6710.40 で実行されました。これは、パフォーマンスの約 3 倍の違いです。また、サーバーのパフォーマンス低下がラップトップよりもはるかに急勾配であることもわかります。

編集: memmove() は、サーバー上の memcpy() よりも 2 倍高速です

いくつかの実験に基づいて、テスト ケースで memcpy() の代わりに memmove() を使用してみましたが、サーバーで 2 倍の改善が見られました。ラップトップ上の Memmove() は memcpy() よりも遅く実行されますが、奇妙なことに、サーバー上の memmove() と同じ速度で十分に実行されます。なぜ memcpy はこんなに遅いのでしょうか?

memcpy とともに memmove をテストするようにコードを更新しました。memmove() を関数内にラップする必要がありました。インラインのままにしておくと、GCC が最適化し、me​​mcpy() とまったく同じように実行したためです (場所が重複していないことを知っていたため、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 関数を実装してテストしました。

Naive 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 からの提案に基づいて、asmlibバージョンの memcpy で実行してみました。私の結果は最初は貧弱でしたが、SetMemcpyCacheLimit() を 1GB (バッファーのサイズ) に変更した後、単純な for ループと同等の速度で実行されました!

悪いニュースは、memmove の asmlib バージョンが glibc バージョンより遅いことです。現在、300ms マークで実行されています (memcpy の glibc バージョンと同等)。奇妙なことは、ラップトップで SetMemcpyCacheLimit() を多数にするとパフォーマンスが低下することです...

以下の結果では、SetCache でマークされた行に SetMemcpyCacheLimit が 1073741824 に設定されています。

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

キャッシュの問題に傾倒し始めていますが、これは何が原因でしょうか?

4

7 に答える 7

25

[私はこれをコメントしますが、そうするのに十分な評判がありません。]

同様のシステムがあり、同様の結果が表示されますが、いくつかのデータ ポイントを追加できます。

  • ナイーブの方向を逆にする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 バッファーより少し大きい) に設定して、非テンポラル ストアを回避すると、私の場合はパフォーマンスが 2 倍になりました (時間は 176 ミリ秒まで下がりました)。もちろん、それは順方向の素朴なパフォーマンスに一致しただけなので、恒星ではありません。
  • これらのシステムの BIOS では、4 つの異なるハードウェア プリフェッチャー (MLC Streamer Prefetcher、MLC Spatial Prefetcher、DCU Streamer Prefetcher、および DCU IP Prefetcher) を有効/無効にすることができます。それぞれを無効にしてみましたが、せいぜいパフォーマンスの同等性が維持され、いくつかの設定でパフォーマンスが低下しました。
  • Running Average Power Limit (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/秒を報告しますが、バイトは読み取りと書き込みの両方としてカウントされるため、上記の結果と比較する場合、これは ~6.5 GB/秒に相当します。
于 2014-04-02T21:39:05.740 に答える
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;
}
于 2015-05-21T23:51:09.377 に答える