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 MHzBearbeiten: 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 MHzLaptop-Spezifikationen
Gehäuse: Lenovo W530CPU: 1x Intel Core i7 i7-3720QM bei 2,6 GHzSpeicher: 4x 4GB DDR3 1600MHzBetriebssystem
$ 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?