web-dev-qa-db-ja.com

Linuxでのmemcpyパフォーマンスの低下

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

サーバーの仕様

  • シャシーとモボ:スーパーマイクロ1027GR-TRF
  • CPU:2x Intel Xeon E5-2680 @ 2.70 Ghz
  • メモリ:8x 16GB DDR3 1600MHz

編集:私はまた、わずかに高いスペックを持つ別のサーバーでテストしており、上記のサーバーと同じ結果が見られます

サーバー2仕様

  • シャシーとモボ:SUPER MICRO 10227GR-TRFT
  • CPU:2 x 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でテストしました。コンパイラ間にパフォーマンスの違いはありませんでした。

テストコード以下のテストコードは、実稼働コードで見られる問題を再現するための定型テストです。このベンチマークは非常に単純ですが、問題を悪用して特定することができました。このコードは、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

ご覧のとおり、サーバー上の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をネイティブに設定してコンパイルする

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ページ(@SteveCox)を避けるために、0ではなく0xFを書き込むようにmemsetを変更しました(@ -SteveCox)

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

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

私のテストプログラムが単純すぎることを排除するために、実際のベンチマークプログラム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倍高速です

いくつかの実験に基づいて、テストケースでmemcpy()の代わりにmemmove()を使用してみましたが、サーバーで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

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

悪いニュースは、memmoveのasmlibバージョンがglibcバージョンよりも遅いことです。現在は300msのマークで実行されています(glibcバージョンの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
nick

[私はこれをコメントにしたいが、そうするのに十分な評判を持っていない。]

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

  • 単純な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では、4つの異なるハードウェアプリフェッチャー(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です(他のシステムと同じ)。

[〜#〜] updates [〜#〜]

  • CPU電源管理を最大パフォーマンスに設定し、BIOSでハイパースレッディングを無効にしました。 _/proc/cpuinfo_に基づいて、コアは3 GHzでクロックされました。ただし、これによりメモリパフォーマンスが約10%低下しました。
  • memtest86 + 4.10は、メインメモリに9091 MB/sの帯域幅を報告します。これが読み取り、書き込み、またはコピーに対応するかどうかはわかりませんでした。
  • STREAMベンチマーク はコピーに対して13422 MB/sを報告しますが、読み取りと書き込みの両方としてバイトをカウントするため、上記の結果と比較したい場合は〜6.5 GB/sに相当します。
23
tbenson

これは私には正常に見えます。

2つのCPUを備えた8x16GB ECCメモリスティックの管理は、2x2GBを備えた単一のCPUよりもはるかに困難な作業です。 16GBスティックは、両面メモリ+バッファ+ ECC(マザーボードレベルで無効になっている場合もあります)を持っている可能性があります。すべてのデータパスをRAMより長くします。 RAMを使用し、他のCPUで何もしなくても、常にメモリアクセスはほとんどありません。このデータを切り替えるには、さらに時間が必要です。RAMをグラフィックカードと共有するPCで失われるパフォーマンスを見てください。

それでもあなたのサーバーは本当に強力なデータポンプです。実際のソフトウェアで1GBの複製が頻繁に発生するかどうかはわかりませんが、128GBはどのハードドライブよりもはるかに高速で、最高のSSDであっても、サーバーを活用できる場所であると確信しています。 3GBで同じテストを実行すると、ラップトップが起動します。

これは、コモディティハードウェアに基づいたアーキテクチャが大規模なサーバーよりもはるかに効率的である可能性のある完璧な例のように見えます。これらの大きなサーバーに費やしたお金で、何台のコンシューマPCを購入できますか?

非常に詳細な質問をありがとうございます。

編集:(この答えを書くのに時間がかかりすぎて、グラフ部分を見逃してしまいました。)

問題はデータの保存場所にあると思います。これを比較してください:

  • テスト1:500Mb RAMの2つの連続したブロックを割り当て、一方から他方にコピーします(既に行ったこと)
  • テスト2:500Mbのメモリの20(またはそれ以上)ブロックを割り当てて、最初から最後までコピーするので、(実際の位置がわからなくても)互いに遠く離れています。

これにより、メモリコントローラーが互いに遠く離れたメモリブロックを処理する方法がわかります。あなたのデータはメモリの異なるゾーンに置かれ、あるゾーンと他のゾーンと通信するためにデータパスのある時点で切り替え操作が必要だと思います(両面メモリにはこのような問題があります)。

また、スレッドが1つのCPUにバインドされていることを確認していますか?

編集2:

メモリにはいくつかの種類の「ゾーン」区切り文字があります。 NUMAは1つですが、それだけではありません。たとえば、両面スティックには、どちらかのアドレスを指定するフラグが必要です。グラフ上で、ラップトップでも(NUMAがない場合でも)大量のメモリでパフォーマンスがどのように低下​​するかを確認してください。私にはこれはわかりませんが、memcpyはハードウェア機能を使用してRAM(DMAの一種)をコピーする可能性があり、このチップのキャッシュはCPUよりも少なくなければなりません。

10
bokan

IvyBridgeベースのラップトップのCPUのいくつかの改善が、SandyBridgeベースのサーバーを超えるこの利益に貢献する可能性があります。

  1. Page-crossing Prefetch -ラップトップCPUは、現在のページの最後に到達するたびに次のリニアページを先読みして、毎回厄介なTLBミスを防ぎます。これを軽減するには、2M/1Gページ用のサーバーコードを作成してください。

  2. キャッシュ置換スキームも改善されたようです(興味深いリバースエンジニアリングを参照してください ここ )。実際にこのCPUが動的挿入ポリシーを使用する場合、コピーされたデータがLast-Level-Cache(サイズのためにとにかく効果的に使用できない)をスラッシングしようとするのを簡単に防ぎ、他の便利なキャッシングのためのスペースを節約しますコード、スタック、ページテーブルデータなど)。これをテストするために、ストリーミングのロード/ストア(movntdqまたは同様のもの、gccビルトインを使用することもできます)を使用して、単純な実装の再構築を試みることができます。この可能性は、大きなデータセットサイズの突然の低下を説明する場合があります。

  3. 文字列コピーでもいくつかの改善が行われたと思います( here )。アセンブリコードがどのように見えるかによって、ここに適用される場合と適用されない場合があります。 Dhrystone でベンチマークを試して、固有の違いがあるかどうかをテストできます。これは、memcpyとmemmoveの違いを説明する場合もあります。

IvyBridgeベースのサーバーまたはSandy-Bridgeラップトップを手に入れることができれば、これらすべてを一緒にテストするのが最も簡単です。

8
Leeor

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ライブラリのコードがあまりにも巧妙にしようとしているのではないかと疑っています。

4
stark

質問はすでに回答されました 上記 、しかし、いずれにせよ、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
Guilherme

サーバー1の仕様

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

サーバー2の仕様

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

Intel ARKによると、 E5-265E5-268 の両方にAVX拡張があります。

ビルドするCMakeファイル

これは問題の一部です。 CMakeは、かなり貧弱なフラグを選択します。 make VERBOSE=1を実行して確認できます。

-march=native-O3の両方をCFLAGSCXXFLAGSに追加する必要があります。劇的なパフォーマンスの向上が見られるでしょう。 AVX拡張機能を使用する必要があります。 -march=XXXがなければ、最小限のi686またはx86_64マシンを効率的に取得できます。 -O3がなければ、GCCのベクトル化に関与しません。

GCC 4.6がAVX(およびBMIなどの友人)に対応しているかどうかはわかりません。 GCCがmemcpyとmemsetをMMXユニットにアウトソーシングしているときにセグメンテーションフォールトを引き起こしていたアライメントバグを追跡しなければならなかったため、GCC 4.8または4.9が有効であることを知っています。 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は、ヒープからのポインターが関係している場合(潜在的なvoid*およびmallocの問題があるため)、MMXユニットにオフロードできることを認識する必要があります。

最後に、しばらくの間、Clangは-march=nativeの使用時にネイティブCPU拡張を使用していませんでした。たとえば、 buntu Issue 1616723、Clang 3.4はSSE2のみをアドバタイズしますbuntu Issue 1616723、Clang 3.5はSSE2のみをアドバタイズします 、および buntu Issue 1616723、Clang 3.6 SSE2 のみをアドバタイズします。

0
jww