Schlechte memcpy-Leistung unter Linux

Wir haben kürzlich einige neue Server gekauft und stellen eine schlechte Speicherleistung fest. Die Speicherleistung ist auf den Servern im Vergleich zu unseren Laptops 3x langsamer.

Serverspezifikationen

Fahrgestell und Mobo: SUPER MICRO 1027GR-TRFCPU: 2x Intel Xeon E5-2680 @ 2.70 GhzSpeicher: 8x 16 GB DDR3 1600 MHz

Bearbeiten: Ich teste auch auf einem anderen Server mit etwas höheren Spezifikationen und sehe die gleichen Ergebnisse wie der oben genannte Server

Server 2-Spezifikationen

Fahrgestell und Mobo: SUPER MICRO 10227GR-TRFTCPU: 2x Intel Xeon E5-2650 v2 bei 2,6 GHzSpeicher: 8x 16 GB DDR3 1866 MHz

Laptop-Spezifikationen

Gehäuse: Lenovo W530CPU: 1x Intel Core i7 i7-3720QM bei 2,6 GHzSpeicher: 4x 4GB DDR3 1600MHz

Betriebssystem

$ 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

Compiler (auf allen Systemen)

$ gcc --version
gcc (GCC) 4.6.1

Auch mit gcc 4.8.2 getestet, basierend auf einem Vorschlag von @ stefan. Es gab keinen Leistungsunterschied zwischen Compilern.

Code testen Der folgende Testcode ist ein Test in Dosen, um das Problem, das in unserem Produktionscode auftritt, zu duplizieren. Ich weiß, dass dieser Benchmark simpel ist, aber er konnte unser Problem ausnutzen und identifizieren. Mit dem Code werden zwei 1-GB-Puffer und Memcpys zwischen ihnen erstellt, die den Memcpy-Aufruf zeitlich steuern. Sie können alternative Puffergrößen in der Befehlszeile angeben, indem Sie ./big_memcpy_test [SIZE_BYTES] verwenden.

#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-Datei zum Erstellen

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

Testergebnisse

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

Wie Sie sehen können, sind die Memcpys und Memsets auf unseren Servern viel langsamer als die Memcpys und Memsets auf unseren Laptops.

Unterschiedliche Puffergrößen

Ich habe versucht, Puffer von 100 MB bis 5 GB alle mit ähnlichen Ergebnissen (Server langsamer als Laptop)

NUMA-Affinität

Ich habe über Leute gelesen, die Leistungsprobleme mit NUMA haben, also habe ich versucht, die CPU- und Speicheraffinität mit numactl einzustellen, aber die Ergebnisse sind gleich geblieben.

Server-NUMA-Hardware

$ 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 

Laptop-NUMA-Hardware

$ 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

Einstellen der NUMA-Affinität

$ numactl --cpunodebind=0 --membind=0 ./big_memcpy_test

Hilfe bei der Lösung dieses Problems wird sehr geschätzt.

Bearbeiten: GCC-Optionen

Basierend auf Kommentaren habe ich versucht, mit verschiedenen GCC-Optionen zu kompilieren:

Kompilieren mit -march und -mtune auf native gesetzt

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

Ergebnis: Genau die gleiche Leistung (keine Verbesserung)

Kompilieren mit -O2 anstelle von -O3

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

Ergebnis: Genau die gleiche Leistung (keine Verbesserung)

Bearbeiten: Memset so geändert, dass 0xF anstelle von 0 geschrieben wird, um eine NULL-Seite zu vermeiden (@SteveCox)

Keine Verbesserung beim Speichern mit einem anderen Wert als 0 (in diesem Fall 0xF).

Edit: Cachebench Ergebnisse

Um auszuschließen, dass mein Testprogramm zu simpel ist, habe ich ein echtes Benchmarking-Programm LLCacheBench (http://icl.cs.utk.edu/projects/llcbench/cachebench.html)

Ich habe den Benchmark für jede Maschine separat erstellt, um Architekturprobleme zu vermeiden. Unten sind meine Ergebnisse.

Beachten Sie, dass der SEHR große Unterschied in der Leistung der größeren Puffergrößen besteht. Die zuletzt getestete Größe (16777216) wurde auf dem Laptop mit 18849,29 MB / s und auf dem Server mit 6710,40 MB / s ausgeführt. Das ist ungefähr ein dreifacher Leistungsunterschied. Sie können auch feststellen, dass der Leistungsabfall des Servers viel steiler ist als auf dem Laptop.

Edit: memmove () ist 2x SCHNELLER als memcpy () auf dem Server

Aufgrund einiger Experimente habe ich versucht, memmove () anstelle von memcpy () in meinem Testfall zu verwenden, und auf dem Server eine zweifache Verbesserung festgestellt. Memmove () auf dem Laptop läuft langsamer als memcpy (), aber seltsamerweise läuft es mit der gleichen Geschwindigkeit wie memmove () auf dem Server. Dies wirft die Frage auf, warum memcpy so langsam ist.

Der Code wurde aktualisiert, um memmove zusammen mit memcpy zu testen. Ich musste memmove () in eine Funktion einbinden, denn wenn ich es inline gelassen habe, hat GCC es optimiert und genau dasselbe ausgeführt wie memcpy () (ich gehe davon aus, dass gcc es auf memcpy optimiert hat, weil es wusste, dass sich die Positionen nicht überlappen).

Aktualisierte Ergebnisse

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

Bearbeiten: Naive Memcpy

Auf Anregung von @Salgar habe ich meine eigene naive memcpy-Funktion implementiert und getestet.

Naive Speicherquelle

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

Naive Memcpy-Ergebnisse im Vergleich zu memcpy ()

Buffer Size: 1GB | memcpy (ms) | memmove(ms) | naiveMemcpy()
------------------------------------------------------------
Laptop 1         | 113         | 161         | 160
Server 1         | 301         | 159         | 159
Server 2         | 325         | 159         | 159

Bearbeiten: Baugruppenausgabe

Einfache memcpy-Quelle

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

Assembly-Ausgabe: Dies ist sowohl auf dem Server als auch auf dem Laptop identisch. Ich spare Platz und klebe nicht beides ein.

        .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

FORTSCHRITT!!!! asmlib

Auf Anregung von @tbenson habe ich versucht, mit derasmlib Version von memcpy. Meine Ergebnisse waren anfangs schlecht, aber nachdem ich SetMemcpyCacheLimit () auf 1 GB (Größe meines Puffers) geändert hatte, lief ich mit einer Geschwindigkeit, die meiner naiven for-Schleife entsprach!

Schlechte Nachrichten sind, dass die asmlib-Version von memmove langsamer ist als die glibc-Version, sie läuft jetzt bei der 300ms-Marke (auf dem Niveau der glibc-Version von memcpy). Merkwürdige Sache ist, dass auf dem Laptop, wenn ich SetMemcpyCacheLimit () zu einer großen Zahl es Leistung verletzt ...

In den Ergebnissen unter den mit SetCache markierten Zeilen ist SetMemcpyCacheLimit auf 1073741824 festgelegt. Die Ergebnisse ohne SetCache rufen SetMemcpyCacheLimit () nicht auf.

Ergebnisse mit Funktionen aus 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

Fängt an, sich dem Cache-Problem zuzuwenden, aber was würde das verursachen?

Antworten auf die Frage(7)

Ihre Antwort auf die Frage