Schlechte memcpy Performance unter Linux
Wir haben vor kurzem kaufte ein paar neue Server und erleben, schlechte memcpy Leistung. Der memcpy-Leistung ist 3x langsamer auf den Servern im Vergleich zu unseren laptops.
Server Specs
- Gehäuse und Mobo: SUPER MIKRO-1027GR-TRF
- CPU: 2x Intel Xeon E5-2680 @ 2.70 Ghz
- Speicher: 8x 16GB DDR3 1600MHz
Edit: ich bin auch testen auf einem anderen server mit etwas höheren specs und sehen die gleichen Ergebnisse wie die oben genannten server
Server 2 Specs
- Gehäuse und Mobo: SUPER MICRO 10227GR-TRFT
- CPU: 2x Intel Xeon E5-2650 v2 @ 2.6 Ghz
- Speicher: 8x 16GB DDR3 1.866 MHz
Laptop Specs
- Chassis: Lenovo W530
- CPU: 1x Intel Core i7 i7-3720QM @ 2.6 Ghz
- Arbeitsspeicher: 4x 4GB DDR3 1600 MHz
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
Getestet auch mit gcc 4.8.2 basiert auf einem Vorschlag von @stefan. Es gab keinen performance-Unterschied zwischen Compiler.
Test-Code
Der test-code ist eine Dose testen, um das problem zu duplizieren, den ich sehe, in unserer Produktion code. Ich weiß, dass dieser benchmark ist simpel, aber es war in der Lage zu nutzen und erkennen unser problem. Der code erstellt zwei 1GB-Puffer und memcpys zwischen Ihnen, das timing der memcpy-Aufruf. Sie können angeben, Alternative buffer-Größen auf der Kommandozeile mittels: ./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-Datei 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 die memcpys und memsets auf unseren Servern ist sehr viel langsamer als die memcpys und memsets auf unseren laptops.
Unterschiedlichen puffergrößen
Habe ich versucht Puffer von 100 MB bis 5 GB, die alle ähnliche Ergebnisse (Server langsamer als laptop)
NUMA-Affinität
Ich lese über Menschen mit performance-Probleme mit NUMA so versuchte ich die Einstellung der CPU-und Speicher-Affinität mit numactl aber die Ergebnisse blieben die gleichen.
Server die 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
Einstellung NUMA-Affinität
$ numactl --cpunodebind=0 --membind=0 ./big_memcpy_test
Jede Hilfe bei der Lösung dieses sehr zu schätzen.
Edit: GCC-Optionen
Basierend auf Kommentare, die ich versucht habe zu kompilieren mit unterschiedlichen GCC-Optionen:
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 statt -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)
Edit: Geändert memset zu schreiben 0xF, anstatt von 0 zu vermeiden NULL-Seite (@SteveCox)
Keine Verbesserung, wenn memsetting mit einem anderen Wert als 0 (0xF in diesem Fall).
Edit: Cachebench Ergebnisse
Um auszuschließen, dass mein test-Programm ist zu simpel, ich heruntergeladen habe, ein echtes benchmarking-Programm LLCacheBench (http://icl.cs.utk.edu/projects/llcbench/cachebench.html)
Baute ich den benchmark auf jedem Rechner separat zu vermeiden, Architektur Themen. Hier sind meine Ergebnisse.
Beachten Sie die SEHR große Unterschied ist die Leistung auf den größeren buffer-Größen. Die Letzte Größe, die getestet (16777216) durchgeführt 18849.29 MB/s auf den laptop und 6710.40 auf dem server. Das ist etwa 3x Unterschied in der Leistung. Sie können auch feststellen, dass die Leistung Absetzung der server ist viel steiler als auf dem laptop.
Edit: memmove() ist 2x SCHNELLER als memcpy() auf dem server
Beruht auf einigen Experimenten habe ich versucht, mit memmove() anstelle von memcpy() in meinem test-Fall und habe ein 2x-Verbesserung auf dem server. Memmove() auf dem laptop läuft langsamer als memcpy (), aber merkwürdigerweise läuft mit der gleichen Geschwindigkeit wie die memmove() auf dem server. Dies wirft die Frage auf, warum ist memcpy so langsam?
Aktualisierten Code zu testen, memmove zusammen mit memcpy. Ich hatte zum wickeln des memmove() innerhalb einer Funktion, weil wenn ich es inline-GCC optimiert und durchgeführt, die genau das gleiche wie memcpy (), (ich nehme den gcc optimiert und es memcpy, weil Sie wusste, dass die Standorte nicht überschneiden).
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
Edit: Naive Memcpy
Basierend auf den Vorschlag von @Salgar implementierte ich meine eigene naive memcpy Funktion und es getestet.
Naiv Memcpy Quelle
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++;
}
}
Naiv 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
Edit: Assembly Output
Einfachen 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 Output: Dies ist genau das gleiche auf dem server und dem laptop. Ich bin platzsparend und nicht einfügen beide.
.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
Basierend auf den Vorschlag von @tbenson ich habe versucht mit mit dem asmlib version von memcpy. Meine Ergebnisse waren zunächst schlecht, doch nach dem Wechsel SetMemcpyCacheLimit() 1 GB (Größe der mein Puffer) lief ich bei der Geschwindigkeit auf Augenhöhe mit meiner naiven for-Schleife!
Schlechte Nachricht ist, dass die asmlib-version von memmove ist langsamer als die glibc-version, es läuft jetzt auf die 300ms Marke (auf Augenhöhe mit der glibc-version von memcpy). Komisch ist, dass auf dem laptop, wenn ich SetMemcpyCacheLimit (), um eine große Anzahl tut es weh, Leistung...
In den Ergebnissen unterhalb der markierten Zeilen mit SetCache haben SetMemcpyCacheLimit set zu 1073741824. Die Ergebnisse ohne SetCache nicht nennen SetMemcpyCacheLimit()
Ergebnisse mit Funktionen von 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
Ab zu lehnen in Richtung cache Problem, aber was würde das verursachen?
- Sind Sie kompilieren der test auf dem server?
- Ja, ich bin das kompilieren der test auf dem server. Beide Systeme sind mit der gleichen version von Scientific Linux. Beide haben die gleiche version von glibc (2.12) und gcc (4.6.1).
- Können Sie überprüfen die code-Aufrufe in für memcpy? Meine erste Vermutung ist, dass vielleicht die server malloc ausgerichtet ist, anders als der laptop.
- Versuchen Sie, kompilieren Sie Ihre
main.cpp
auf der Kommandozeile mitg++ -Wall -O2 -mtune=native
. Erwägen Sie, GCC 4.8.2 - Sie scheinen nicht zu kompilieren mit jedem Bogen-spezifische flags, die Sie sollten auf jeden Fall für das ein fairer test. that being said, ist dies definitiv ein Speicher-begrenzten Betrieb, und es sieht aus wie die Speicher-Spezifikationen sind nicht wirklich schneller auf den server, also es sollte nicht riesige Gewinne. sollte der server deutlich schneller als der laptops nur, wenn seine Arbeit von den cache-oder Register
- oh auch nicht all diese Seiten auf null gesetzt, dass Sie etwas anderes. linux ist copy-on-write, und memset könnte optimiert werden, dass im Sinn. es sieht aus wie Sie könnte nur zeigen, um die triviale null-Seite, die macht dieser ganze test wertlos.
- Ich bin damit einverstanden, ich sollte nicht sehen, riesige Gewinne von den Servern, aber ich bin besorgt, dass die Server langsamer sind als die laptops mit einem Faktor von 2-3x. Edit: ich werde versuchen, nicht memsetting die Seiten.
- Nein, Sie müssen memset die Seiten, sondern stellen Sie Sie auf einen anderen Wert
- hmm. es sieht aus wie Sie Messen einen Durchsatz von über 8 GB/s auf den laptops memset. Das ist ziemlich nahe an der theoretischen Grenze von 12,8. Ich würde erwarten, dass die memcpy, länger zu dauern, obwohl (nicht sicher, ob der Speicher-controller kann tun es alle auf Ihre eigene). Wenn es einen vollständigen lese-und Schreibzugriff eingebunden in die memcpy Umsetzung, dann ist das eine Bandbreite von 16 GB/s, die über die des Speichers auf die theoretischen Begrenzungen. Kann jemand bestätigen für mich, dass memcpying 1GB ist eigentlich eine 2-GB-operation?
- Habe versucht mit memset ist mit einem Wert von 0xF anstelle von 0. Bekam die gleichen Ergebnisse.
- Ist der laptop-Speicher gepuffert-ECC-DIMM ist wie die des Servers?
- Laptop-Speicher us-unbuffered-DIMMS mit 1600 MHz. Ich Teste auf zwei Servern jetzt, etwa 6 Monate alt, mit buffered DIMMS mit 1600 MHz und eine weitere über 1 Monat alt mit buffered DIMMS zu 1.866 MHz. Beide Server haben die gleiche Leistung. ECC ist nicht aktiviert auf dem Server.
- Wenn Sie reduzieren die server-Speicher-4 x 4G doe es die Geschwindigkeit verändern? Ich denke, dass die größeren Seite Tabellen haben eine Wirkung.
- Auch ist HT aktiviert für den server oder den laptop?
- ja, HT ist aktiviert auf beiden Rechnern. Ich derzeit nicht über physischen Zugriff auf den server da er sich in einem Labor über Land. Ich werde zu unserem anderen Standort, wo der server wohnt, in ein paar Tagen, werde ich versuchen, die Anpassung der RAM-Konfiguration.
- 1) Beide sind 64 bit SO?; 2) Der server ist in einer VM?
- Das klingt vielleicht albern, aber haben Sie zeitlich, wie lange Sie Ihr timing, dauert auf jedem Rechner? Es ist nicht gratis.
- Eine andere Sache zu tun ist, schreiben Sie einfachen memcpy und memmove, und kompilieren Sie Sie down und vergleichen Sie die Baugruppe der beiden, um zu sehen, ob es irgendwelche signifikanten Unterschied in der Implementierung oder Optimierung auf den verschiedenen Maschinen.
- Einmal kam ich über eine version von calloc das war nur eine for-Schleife Einstellung 4 Byte zu einem Zeitpunkt, es war tragisch langsam...
- ja, alle Maschinen sind mit der exakt gleichen 64-bit-OS, genau die gleiche patch-Level. Nicht mit einer VM überall, alle blanken Metalle. System-Last auf alle Maschinen 0,0,0, ich bin die Steuerung dieser Maschinen und können überprüfen, es ist nichts Los in den hintergrund. Dies geschieht konsequent.
- Nein, ich habe nicht getimed meine timings, aber ich wäre sehr überrascht, wenn es dauert 150 MS zum ausführen einer timer. Ich werde versuchen, eine vereinfachte memcpy und memmove Umsetzung.
- Sorry, nicht Lesen es richtig, ja, die Zeit wird unbedeutend in dieser Größe/Zeit
- Wow, das wird mehr und mehr interessant... ich kann nicht glauben, dass Sie Ihre byte-by-byte
memcpy
läuft schneller als libc ist! - Können Sie versuchen mit dem g++-4.8?
- ich bin Gebäude mit 4.6.1 (ich update Frage) gibt es signifikante memcpy Verbesserungen zwischen 4.6.1 und 4.8.2?
- Es ist möglich, ich weiß es nicht 😉 Der Punkt ist: es kann etwas sein, was der compiler tut (oder, wie der compiler konfiguriert wurde!), also wäre es toll zu testen, mit einem anderen compiler.
- Zusammengestellt und lief mit 4.4.7 (Standard-compiler auf RHEL 6.5) und keine änderung. Ich bin Download 4.8.2, Gebäude, wird eine Weile dauern.
- Geh und hol LIKWID und ausführen der performance-counter-tool wie dieses:
likwid-perfctr -C S1:0 -g MEM ./big_memcpy_test
. Auch laufenlikwid-perfctr
mitnumactl --membind=0
seitlikwid-perfctr
nicht unterstützt NUMA-Speicher binden. Es zeigt Schätzungen der Speicher-Geschwindigkeiten basierend auf CPUs von Leistungsindikatoren und unterscheidet ebenfalls zwischen lokalen und remote-memory-access. - Ich würde gerne sehen, einige Assembler-code (für jede Plattform, da Sie nativ kompilieren). Vielleicht nutzt man AVX-Schleifen und die andere verwendet
rep movs
fließt? - Ja, bitte erstellen Sie ein
main()
ist nur, dass eine einzige großememcpy
im auf und generieren Sie die Montage-Ausgang mitgcc/g++ -S
+ alle die gleichen Optimierungen, die Sie verwendet haben. Der zu langsam ist, muss etwas sehr seltsam, wenn Ihre benutzerdefinierte byte für byte memcpy ist so viel schneller. Es sollte viel langsamer. - Haben Sie überprüft, Ihre Speicher-alignment? Wenn die pipeline stalling auf dem server, dann würde man erwarten, dass memcpy Leistung zu beeinträchtigen.
- Hinzugefügt assembly Ausgänge für laptop und server. Die Montage war genau der gleiche (diffed) ich habe die Ausgabe zu zeigen, was Sie beide aussahen. Nichts Interessantes passiert.
- ich habe versucht, die Ausrichtung an 16byte Grenze mit dem link, den Sie zur Verfügung gestellt. Keine änderung in memcpy Leistung jedoch meine naive Implementierung lief ~600ms auf beiden Laptops und-server, 2x langsamer auf dem server als normale memcpy und 6x langsamer auf dem laptop (im Vergleich zu regulären memcpy).
- Basierend auf den Vorschlag von @stefan ich habe versucht, mit einer neueren version von GCC, die ich wählte, 4.8.2 (die neueste version). Die performance-zahlen sind genau die gleichen wie mit 4.6.1 in der original-test.
- Haben Sie sich alle Stromspar-Quatsch in der Server-BIOS aus?
- Ich habe auch versucht, schieben Sie diese auf die Intel-community-forum (communities.intel.com/thread/50808). Ich habe ähnliche Leistung Merkwürdigkeiten mit einigen tuned out-of-place-matrix-transpose-Funktionen. Die optimierten Versionen verwenden cache-blocking, die sollen die Leistung verbessern (und eine transpose-ähnlich wie ein memcpy), aber auf der E5-Serie Xeons Sie tatsächlich langsamer. Das ist nicht der Fall für die Westmere-Xeons oder E3 Haswell-Xeons.
- als ersten Schritt sollten Sie die Messung Speicher von Latenz und Bandbreite unter Verwendung von zum Beispiel Intel MLC oder TinyMemBench, oder lmbench oder was auch immer. Sicherlich die server-Teile haben schlechtere Latenz (wie ich es beschrieben in meiner Antwort), aber Sie wissen, wie viel helfen. Ferner mehrere dieser tools haben separate tests für die zeitliche und nicht-zeitliche speichert.
Du musst angemeldet sein, um einen Kommentar abzugeben.
[Würde ich machen, dieser Kommentar, aber nicht genug Ruf zu tun.]
Ich habe ein ähnliches system und sehen die Ergebnisse ähnlich, kann aber noch ein paar Daten Punkte:
memcpy
(d.h. konvertieren*p_dest-- = *p_src--
), dann können Sie sich viel schlechtere Leistung, als für die vorwärts-Richtung (~637 ms für mich). Es gab eine änderung inmemcpy()
in glibc 2.12 ausgesetzt, dass einige Fehler für den Aufrufmemcpy
auf überlappende Puffer (http://lwn.net/Articles/414467/) und ich glaube, das Problem wurde verursacht durch die Umstellung auf eine version vonmemcpy
arbeitet rückwärts. Also, rückwärts gegen vorwärts Kopien erklären kann, diememcpy()
/memmove()
Unterschiede.memcpy()
Implementierungen wechseln non-temporal stores (die nicht zwischengespeichert) für große Puffer (D. H. größer als die last-level-cache). Getestet habe ich Agner Fog version von memcpy (http://www.agner.org/optimize/#asmlib) und festgestellt, dass es etwa die gleiche Geschwindigkeit wie die version imglibc
. Allerdingsasmlib
hat eine Funktion (SetMemcpyCacheLimit
), die ermöglicht die Einstellung der Schwelle, ab der die non-temporal stores verwendet werden. Einstellung, die Grenze zu 8GiB (oder nur größer als die 1-GiB-Puffer) zu vermeiden, non-temporal stores verdoppelt die Leistung in meinem Fall (Zeit bis zu 176ms). Natürlich, dass nur übereinstimmen, wird die vorwärts-Richtung naive Leistung, so ist es nicht stellar.memcpy
(104ms). Der RAM auf dem Haswell system ist DDR3-1600 (der anderen Systeme).UPDATES
/proc/cpuinfo
die Kerne wurden dann getaktet auf 3 GHz. Aber seltsamerweise verminderte Gedächtnisleistung um rund 10%.echo "performance" > /sys/devices/system/cpu/cpuXX/cpufreq/scaling_governor
für core-XX) wird ähnliche Auswirkungen haben als gut.memtest86+
sollte drucken, COPY-Geschwindigkeit - memtest86+-4.20-1.1/init.c-Linie 1220 verwendetmemspeed((ulong)mapping(0x100), i*1024, 50, MS_COPY)
nennen. Undmemspeed()
selbst implementiert wird, mitcld; rep movsl
mit 50 Iterationen der Schleife kopieren über Speicher-segment.Das sieht für mich normal.
Verwaltung 8x16GB ECC-Speicher-sticks mit zwei CPUs ist eine viel härtere Arbeit, als eine einzelne CPU mit 2x2GB. Ihre 16-GB-sticks sind Double sided Speicher + Sie haben können Puffer + ECC (auch Behinderte auf motherboard-level)... alle, die Daten-Pfad zum RAM viel mehr. Sie haben auch 2 CPUs teilen der ram, und selbst wenn Sie nichts tun, auf der anderen CPU da ist immer wenig Speicher zugreifen. Wenn Sie diese Daten benötigen einige zusätzliche Zeit. Schauen Sie sich nur die enorme Leistung verloren auf PCs, die Freigabe einige ram mit der Grafikkarte.
Immer noch Ihr trennt, sind wirklich mächtig datapumps. Ich bin mir nicht sicher, duplizieren 1GB happends sehr oft im wahren Leben software, aber ich bin mir sicher, dass Ihr 128GBs sind viel schneller als jede Festplatte, auch am besten SSD und das ist, wo Sie können die Vorteile Ihrer Server. Mache den gleichen test mit 3GB setzen Sie Ihren laptop auf Feuer.
Diese sieht aus wie die perfekte Beispiel dafür, wie eine Architektur basierend auf commodity-hardware könnte viel effizienter als auf großen Servern. Wie viele consumer-PCs konnte man es sich leisten mit dem Geld auf diesen großen Server ?
Vielen Dank für Ihre sehr detaillierte Frage.
EDIT : (hat mich so lange zum schreiben dieser Antwort, habe ich die Grafik-Teil.)
Ich denke, das problem ist, wo die Daten gespeichert werden. Können Sie vergleichen Sie bitte diese :
Diese Weise werden Sie sehen, wie Speicher-controller handle memory blocks weit von einander entfernt. Ich denke, dass Ihre Daten in verschiedenen Zonen der Erinnerung, und es erfordert ein Schaltvorgang in einem Punkt auf der Daten-Pfad-to-talk mit einer zone, dann die anderen (es gibt, wie Problem mit double sided Speicher).
Außerdem werden Sie sicherstellen, dass der Faden gebunden ist, um eine CPU ?
EDIT 2:
Gibt es verschiedene Arten von "Zonen" Trennzeichen für den Speicher. NUMA ist, aber das ist nicht der einzige. Zum Beispiel zwei einseitige sticks benötigen eine Flagge, um die Adresse einer Seite oder der anderen. Schauen Sie auf Ihr Diagramm, wie sich die Leistung verschlechtern, mit großen Brocken von Speicher auch auf dem laptop (welches no NUMA).
Ich bin mir nicht sicher, aber memcpy verwenden möglicherweise ein hardware-Funktion zum kopieren von ram (eine Art DMA) und dieser chip muss weniger cache als die CPU, dies könnte erklären, warum dumme Kopie mit CPU ist schneller als memcpy.
Es ist möglich, dass einige CPU-Verbesserungen in Ihrer IvyBridge-basierten laptop-Beitrag zu diesem Gewinn die SandyBridge-basierten Servern.
Seite-überqueren Prefetch - Ihr laptop-CPU würde prefetch vor der nächsten linearen Seite, wenn Sie erreichen das Ende des aktuellen ein, speichern Sie eine böse TLB-miss jedes mal. Um zu versuchen und zu mindern, versuchen Sie den Aufbau Ihrer server-code für 2M /1G Seiten.
Cache-replacement-Systeme scheint ebenfalls verbessert worden (siehe eine interessante reverse-engineering hier). Wenn ja, diese CPU basiert auf einem dynamischen Einfügung der Politik, es wäre leicht zu verhindern, dass Ihre kopierten Daten aus versuchen, thrash Ihre Last-Level-Cache (die es nicht verwenden können, die effektiv sowieso aufgrund der Größe), und speichern Sie den Platz für andere nützliche caching wie code, stack, Seite, Tabelle, Daten, etc..). Um dies zu testen, könnten Sie versuchen, Wiederaufbau Ihrer naiven Implementierung mit Hilfe von streaming-lädt/speichert (
movntdq
oder ähnliches, können Sie es auch verwenden, gcc-builtin dafür). Diese Möglichkeit erklären kann, die plötzlichen Abfall in großen Daten-set-Größen.Ich glaube, einige Verbesserungen wurden auch mit string-Kopie (hier), kann hier nicht gelten, je nachdem, wie Sie Ihren Assembler-code aussieht. Sie könnten versuchen, benchmarking mit Dhrystone, um zu testen, ob es einen inhärenten Unterschied. Dies kann auch erklären, den Unterschied zwischen memcpy und memmove.
Wenn Sie ergattern konnte ein IvyBridge-basierten server oder ein Sandy-Bridge laptop würde es am einfachsten sein, alles zu versuchen, diese zusammen.
Ich veränderte den Maßstab zu verwenden, den nsec-timer in Linux und fand ähnliche variation auf unterschiedlichen Prozessoren, die alle mit ähnlichen Speicher. Alle laufen RHEL 6. Zahlen sind konsistent über mehrere Läufe.
Hier sind die Ergebnisse mit inline C-code -O3
Für das heck der es, ich habe auch versucht, das machen die inline-memcpy do 8 bytes zu einer Zeit.
Auf diese Intel-Prozessoren, es machte keinen spürbaren Unterschied. Cache werden alle von der byte-Operationen in die minimale Anzahl von Speicher-Operationen. Ich vermute, dass der gcc code für die Bibliothek wird versuchen, zu clever.
Die Frage schon beantwortet war oben, aber in jedem Fall, hier ist eine Implementierung mit AVX, dass sollte schneller sein bei großen Exemplaren, wenn das, was Sie sind besorgt über:
Die zahlen, die für mich Sinn machen. Es gibt eigentlich zwei Fragen hier, und ich werde Sie beantworten sowohl.
Erste, obwohl, wir brauchen, um haben ein mentales Modell davon, wie groß1 Speicher-transfers an etwas arbeiten, wie ein moderner Intel-Prozessor. Diese Beschreibung ist Ungefähre und die details kann sich ändern, etwas von Architektur zu Architektur, aber die high-level-Ideen sind ziemlich konstant.
L1
Daten-cache, eine line buffer zugewiesen, die wird verfolgen die miss-Anforderung, bis es gefüllt ist. Dies kann für eine kurze Zeit (ein Dutzend Zyklen oder so), wenn es zuvor in derL2
cache, oder viel länger (100+ Nanosekunden), wenn es findet alle den Weg zu DRAM.Dem Speicher-subsystem hat sich ein maximale Bandbreite begrenzen, die Sie finden bequem aufgeführt auf der ARCHE. Zum Beispiel, die 3720QM im Lenovo laptop zeigt ein limit von 25.6 GB. Diese Grenze ist im Grunde das Produkt der effektiven Frequenz (
1600 Mhz
) mal 8 Byte (64-bit) pro übertragung Anzahl der Kanäle (2):1600 * 8 * 2 = 25.6 GB/s
. Die server-Chips auf der hand, hat eine peak-Bandbreite von 51.2 GB/s, pro sockel, für ein Gesamt-system-Bandbreite von ~102 GB/s.Im Gegensatz zu anderen Prozessor-features, oft gibt es nur eine mögliche theoretische Bandbreite, die zahlen über die gesamte Vielfalt des chips, da
es kommt nur auf die notierten Werte, die oft das gleiche über viele
verschiedene chips, und sogar über Architekturen. Es ist unrealistisch
erwarten DRAM zu liefern, die genau den theoretischen rate (wegen der verschiedenen
low-level betrifft, diskutiert ein wenig
hier), aber Sie können oft
rund 90% oder mehr.
Also die primäre Konsequenz von (1) ist, dass man behandeln kann, findet auf RAM als eine Art von Anfrage-Antwort-system. Eine miss zu DRAM weist eine füllen Puffer und der Puffer wird freigegeben, wenn die Anfrage wieder kommt. Es gibt nur 10 von diesen Puffer, pro CPU, für die Nachfrage findet, was bringt eine strikte Begrenzung auf der Nachfrage-Speicher die Bandbreite einer single-CPU erzeugen kann, als eine Funktion von Latenz.
Zum Beispiel, können sagen, Ihre
E5-2680
hat eine latency DRAM 80ns. Jede Anforderung bringt eine 64-byte cache-Zeile, so dass Sie nur ausgestellt Anfragen Seriell zu DRAM würden Sie erwarten, den Durchsatz einer armseligen64 bytes /80 ns = 0.8 GB/s
, und man würde Sie schneiden, dass in der Hälfte wieder (mindestens) einmemcpy
Figur, da muss es Lesen und schreiben. Glücklicherweise können Sie Ihre 10-line-fill-Puffer, so können Sie sich überschneiden 10 gleichzeitige Zugriffe auf den Speicher und erhöhen die Bandbreite um einen Faktor 10, was eine theoretische Bandbreite von 8 GB/s.Wenn Sie möchten, zu Graben in noch mehr details, dieser thread ist ziemlich viel reines gold. Sie werden feststellen, dass Fakten und zahlen aus John McCalpin, aka "Dr. Bandbreite wird ein gemeinsames Thema unter.
Also lasst uns in die details und beantworten Sie die zwei Fragen...
Warum ist memcpy so viel langsamer als memmove oder hand gerollt Kopie auf dem server?
Sie zeigten, dass Sie den laptop-Systemen machen die
memcpy
benchmark in etwa 120 ms, während die server-Teile nehmen um 300 ms. Sie zeigte auch, dass diese Langsamkeit ist vor allem nicht von grundlegender Bedeutung, da Sie in der Lage waren, zu verwendenmemmove
und Ihre hand-rolled-memcpy (nachfolgendhrm
) zu erreichen, eine Zeit von etwa 160 ms, viel näher (aber immer noch langsamer als) die laptop-performance.Wir bereits oben gezeigt haben, dass für einen einzigen Kern, die Bandbreite ist begrenzt durch die verfügbare Parallelität und Latenz, anstatt die DRAM-Bandbreite. Wir erwarten, dass die server-Teile haben eine längere Latenz, aber nicht
300 /120 = 2.5x
mehr!Die Antwort liegt in streaming (aka nicht-temporal) speichert. Die libc-version von
memcpy
Sie verwenden, die Sie verwendet, abermemmove
nicht. Sie bestätigt, wie viel Sie mit Ihrem "naiv"memcpy
die auch nicht mit Ihnen, sowie meine Konfigurationasmlib
sowohl für die streaming-stores (langsam) und nicht (schnell).Streaming speichert verletzt die single-CPU - zahlen, weil:
Beide Fragen werden besser erklärt durch Zitate von John McCalpin in dem oben verlinkten thread. Zum Thema prefetch-Effektivität und streaming-stores er sagt:
... und dann für den scheinbar viel längeren Latenzzeiten bei streaming-Shops auf dem E5, er sagt:
Insbesondere Dr. McCalpin gemessen bei ~1.8 x Verlangsamung für E5 im Vergleich zu einem chip mit den "Kunden" uncore, aber die 2,5 x Verlangsamung der OP-Berichte ist im Einklang mit, dass seit dem 1.8 x Punktzahl gemeldet, die für STREAM-TRIAD, hat eine 2:1-Verhältnis von Lasten:speichert, während
memcpy
ist bei 1:1, und die Läden sind die problematischen Teil.Nicht-streaming-eine schlechte Sache - Sie sind in der Tat den Handel ab, Latenz bei kleineren Gesamt-Bandbreite. Sie bekommen weniger Bandbreite, da Sie Parallelität eingeschränkt, wenn Sie eine single-core, aber vermeiden Sie alle, die Lesen-für-Besitz-Verkehr, so würden Sie wahrscheinlich einen (kleinen) Vorteil, wenn Sie lief der test gleichzeitig auf allen cores.
Also weit davon entfernt, ein Artefakt Ihrer software-oder hardware-Konfiguration, die exakt die gleichen Schwächen haben von anderen Benutzern gemeldet worden, mit der gleichen CPU.
Warum ist der server-Teil noch langsamer als mit normalen Läden?
Selbst nach der Korrektur der non-temporal store Problem, Sie sind noch zu sehen, etwa ein
160 /120 = ~1.33x
Verlangsamung auf dem server teilen. Was gibt?Gut es ist ein verbreiteter Irrtum, dass es server-CPUs sind schneller in allen Belangen schneller oder zumindest gleich Ihrer client-Pendants. Es ist einfach nicht wahr -, was Sie bezahlen (oft $2.000-chip oder so) auf dem server teilen meist (a) mehr Kerne (b) mehr Speicher-Kanäle (c) Unterstützung für mehr RAM (Gesamt; d) die Unterstützung für "enterprise-ish" - features wie ECC, virutalization features, etc5.
In der Tat, Latenz-wise-server Teile sind in der Regel nur gleich oder langsamer, um Ihre client4 - Teile. Wenn es um die Speicher-Latenz, ist dies besonders wahr, weil:
So ist es typisch, dass die server-Teile, die haben eine Latenz von 40% bis 60% mehr als client-Teile. Für den E5 wirst du wahrscheinlich feststellen, dass ~80 ns ist eine typische Latenz RAM-Speicher, während der Kunden-die Teile sind näher an 50 ns.
Also alles, was RAM-Latenz eingeschränkt wird langsamer ausgeführt auf server teilen, und es stellt sich heraus,
memcpy
auf einem single-core - ist die Latenz eingeschränkt. das ist verwirrend, weilmemcpy
scheint wie eine Bandbreiten-Messung, richtig? Gut, wie oben beschrieben, einen single-core nicht über genügend Ressourcen, um genügend Anfragen zu RAM im Flug zu einer Zeit, um in der Nähe der RAM-Bandbreite6, so dass die Leistung hängt direkt von der Latenz.Client-chips, auf der anderen Seite, haben beide eine geringere Latenz und niedriger Bandbreite, so ein core kommt viel näher an die Sättigung der Bandbreite (dies ist oft, warum streaming-stores sind ein großer Gewinn auf client - als auch ein single-core-Ansatz kann die RAM-Bandbreite, die 50% speichern Reduzierung der Bandbreite, der stream-Shops bietet viel hilft.
Referenzen
Gibt es viele gute Quellen, um mehr zu Lesen, über diese Dinge, hier sind ein paar.
MemLatX86
undNewMemLat
) links1 Von großen ich meine nur etwas größer als die LLC. Für Kopien, die passen in die GmbH (oder einer höheren cache-Stufe) ist das Verhalten sehr unterschiedlich. Die OPs
llcachebench
Grafik zeigt, dass in der Tat die performance-Abweichung nur beginnt, wenn der Puffer zu übersteigen beginnen die LLC-Größe.2 insbesondere die Anzahl der line-fill-Puffer hat offenbar wurde konstant bei 10 für mehrere Generationen, einschließlich der Architekturen erwähnt in dieser Frage.
3, Wenn wir sagen Nachfrage hier, meinen wir, dass es im Zusammenhang mit einem expliziten laden/speichern im code, anstatt zu sagen, hereingebracht durch einen prefetch.
4 Wenn ich beziehen sich auf eine server Teil hier, meine ich, ein CPU mit einem server uncore. Dies im wesentlichen bedeutet die E5-Serie, wie die E3-Serie im Allgemeinen verwendet der client uncore.
5 In die Zukunft, wie es aussieht, können Sie Sie hinzufügen, "instruction set extensions", um diese Liste, wie es scheint, dass
AVX-512
erscheint nur auf der Skylake-server teilen.6 Pro little ' s law, bei einer Latenz von 80 ns, wir brauchen
(51.2 B/ns * 80 ns) == 4096 bytes
oder 64 cache-Zeilen im Flug zu allen Zeiten bis zum erreichen des maximalen Bandbreite, aber ein Kern weniger als 20.Laut Intel ARK, sowohl die E5-2650 und E5-2680 haben AVX-Erweiterung.
Dies ist ein Teil des Problems. CMake wählt einige eher schlechte Fahnen für Sie. Können Sie bestätigen es, indem
make VERBOSE=1
.Sollten Sie beide
-march=native
und-O3
zu IhremCFLAGS
undCXXFLAGS
. Sie werden wahrscheinlich sehen eine dramatische performance-Steigerung. Es sollten sich die AVX-Erweiterungen. Ohne-march=XXX
Sie effektiv eine minimale i686-oder x86_64-Maschine. Ohne-O3
Sie nicht engagieren GCC vectorizations.Ich bin mir nicht sicher, ob GCC 4.6 ist in der Lage AVX (und Freunde, wie BMI). Ich weiß GCC 4.8 oder 4.9 fähig ist, denn ich hatte auf die Jagd nach einem alignment-Fehler, der verursacht einen segmentation Fault beim GCC war outsourcing-memcpy und memset ist der MMX-Einheit. AVX-und AVX2-ermöglichen, den CPU zu betreiben, die auf 16-byte und 32 byte Blöcke von Daten zu einer Zeit.
Wenn GCC fehlt eine Möglichkeit zum senden von Daten ausgerichtet, um die MMX-Einheit, es fehlen möglicherweise die Tatsache, dass die Daten ausgerichtet ist. Wenn Ihre Daten in 16-byte-ausgerichtet, dann könnten Sie versuchen, sagen, GCC, damit es weiß, auf fat-Blöcke. Für, die, siehe GCC
__builtin_übernehmen_ausgerichtet
. Auch die Fragen, wie Wie man GCC sagen, dass ein Zeiger-argument ist immer double-word-aligned?Dieser sieht auch ein wenig suspekt, weil der
void*
. Seine Art des Werfens entfernt Informationen über den Zeiger. Sie sollten wahrscheinlich halten Sie die Informationen:Vielleicht so etwas wie die folgenden:
Ein weiterer Vorschlag ist die Verwendung
new
, und beenden Sie die Verwendungmalloc
. Seine ein C++ - Programm und GCC können einige Annahmen übernew
auch, dass es nicht übermalloc
. Ich glaube, dass einige der Annahmen sind detailliert in der GCC-option-Seite für die built-ins.Noch ein weiterer Vorschlag ist die Verwendung des heap. Es ist immer 16-byte-ausgerichtet auf typische moderne Systeme. GCC sollte erkennen, Sie verschieben kann, um die MMX-Einheit, wenn Sie einen Zeiger auf dem heap beteiligt ist (sans die möglichen
void*
undmalloc
Probleme).Schließlich, für eine Weile, das Geräusch war nicht mit den nativen CPU-Erweiterungen bei der Verwendung von
-march=native
. Siehe, zum Beispiel, Ubuntu Problem 1616723, Clang 3.4 nur wirbt SSE2, Ubuntu Problem 1616723, Clang 3.5 nur wirbt SSE2, und Ubuntu Problem 1616723, Clang 3.6 nur wirbt SSE2.