Wenn Sie keine Zufälligkeit mit sehr hoher Qualität benötigen und eine nahezu gleichmäßige Verteilung ausreichend ist, können Sie sehr schnell vorgehen, insbesondere auf einer modernen CPU mit effizienten SIMD-Ganzzahlvektoren wie x86 mit SSE2 oder AVX2.
Dies ist wie die Antwort von @ NominalAnimal, da wir beide die gleiche Idee hatten, aber manuell für x86 vektorisiert haben. (Und mit Zufallszahlen von schlechterer Qualität, aber wahrscheinlich immer noch gut genug für viele Anwendungsfälle.) Dies ist ungefähr 15- bis 30-mal schneller als der Code von @ Nominal, bei einer ASCII-Ausgabe von ~ 13 GB / s auf einem 2,5-GHz-Intel-Haswell CPU mit AVX2. Das ist immer noch weniger als die theoretische maximale Hauptspeicherbandbreite (Dual-Channel-DDR3-1600 ist ungefähr 25,6 GB / s), aber ich habe das Schreiben in / dev / null geplant, so dass nur ein Puffer neu geschrieben wird, der im Cache heiß bleibt. Skylake sollte denselben Code deutlich schneller ausführen als Haswell (siehe unten in dieser Antwort).
Vorausgesetzt, Sie haben tatsächlich irgendwo einen E / A-Engpass auf der Festplatte oder leiten diesen weiter, bedeutet eine schnelle Implementierung, dass Ihre CPU nicht einmal höher takten muss als im Leerlauf. Es verbraucht viel weniger Gesamtenergie, um das Ergebnis zu erzielen. (Batterielebensdauer / Hitze / globale Erwärmung.)
Dies ist so schnell, dass Sie es wahrscheinlich nicht auf die Festplatte schreiben möchten. Generieren Sie sie einfach nach Bedarf neu (aus demselben Startwert, wenn Sie dieselben Daten erneut benötigen ). Selbst wenn Sie es einem Multithread-Prozess zuführen möchten, der alle CPUs verwenden kann, wird es beim Ausführen dieses Befehls zum Weiterleiten der Daten im L3-Cache (und im L2-Cache auf dem Kern, der es geschrieben hat) heiß belassen und daher sehr häufig verwendet wenig CPU-Zeit. (Beachten Sie jedoch, dass das Piping im /dev/null
Vergleich zum Schreiben viel Aufwand verursacht . Bei einem Skylake i7-6700k, das an ein wc -c
anderes Programm weitergeleitet wird, das nur seine Eingabe liest und verwirft, ist es ungefähr 8x langsamer als das Schreiben an/dev/null
und verbraucht nur 70% von a CPU: Aber das sind immer noch 4,0 GB / s bei einer 3,9-GHz-CPU.
Das erneute Generieren ist schneller als das erneute Lesen selbst von einer schnellen, mit PCIe verbundenen SSD, aber IDK, wenn es energieeffizienter ist (der Vektor-Integer-Multiplikator ist ziemlich beschäftigt und wahrscheinlich, zusammen mit anderen AVX2-Geräten, ziemlich leistungshungrig) 256b Vektor-ALUs). OTOH, ich weiß nicht, wie viel CPU-Zeit das Lesen von der Festplatte für etwas kostet, bei dem alle Kerne, die diese Eingabe verarbeiten, maximal waren. Ich würde vermuten, dass ein Kontextwechsel, der in 128k-Blöcken neu generiert wird, mit dem Ausführen von Dateisystem- / Pagecache-Code und dem Zuweisen von Seiten zum Lesen von Daten von der Festplatte konkurrieren kann. Wenn es im Pagecache bereits heiß ist, ist es natürlich nur im Grunde genommen memcpy. OTOH, wir schreiben schon so schnell wie memcpy! (was die Hauptspeicherbandbreite zwischen Lesen und Schreiben aufteilen muss). (Beachten Sie auch, dass das Schreiben in den Speicher, dass 'rep movsb
(optimiertes memcpy und memset im Mikrocode, das RFO vermeidet, seit Andy Glew es in P6 (Pentium Pro) implementiert hat ).
Bisher ist dies nur ein Proof of Concept und das Newline-Handling ist nur annähernd korrekt. Es ist falsch um die Enden eines Potenz-2-Puffers. Mit mehr Entwicklungszeit. Ich bin zuversichtlich, dass ich einen effizienteren Weg finden könnte, um Zeilenumbrüche einzufügen, der auch genau richtig ist, mit mindestens so geringem Overhead (verglichen mit der Ausgabe nur von Leerzeichen). Ich denke, das sind ungefähr 10 bis 20%. Ich bin nur daran interessiert zu wissen, wie schnell wir diesen Lauf machen können, und nicht daran, eine polierte Version davon zu haben. Deshalb werde ich diesen Teil als Übung für den Leser mit Kommentaren belassen, in denen einige Ideen beschrieben werden.
Auf einem Haswell i5 mit 2,5 GHz maximalem Turbo und DDR3-1600 MHz RAM wurde die Erzeugung von 100 GiB zwar zeitlich festgelegt, aber verkleinert. (Zeitlich festgelegt auf cygwin64 unter Win10 mit gcc5.4 -O3 -march=native
, weggelassen, -funroll-loops
da es mir schon schwer genug fiel, auf diesem geliehenen Laptop anständige zeitliche Abläufe zu erzielen . Hätte nur Linux über USB booten sollen).
Schreiben nach / dev / null, sofern nicht anders angegeben.
- James Hollis: (nicht getestet)
- Nominals fwrite Version: ~ 2.21s
- dies (SSE2): ~ 0,142 s (nicht skalierte Zeiten = real = 14,232 s, Benutzer = 13,999 s, sys = 0,187 s).
- dies (AVX-128): ~ 0,140s
- this (AVX2): ~ 0,073 s (nicht skaliert: real = 0 m7,291 s, user = 0 m7,125 s, sys = 0 m0,155 s).
- Diese (AVX2-) Cygwin-Piping-Funktion
wc -c
mit 128 KB Puffergröße: 0,32 s bei einer CPU mit 2,38 GHz (maximaler Dual-Core-Turbo). (unskalierte Zeiten: real = 32.466s user = 11.468s sys = 41.092s, einschließlich dieser und wc
). Allerdings wurde nur die Hälfte der Daten tatsächlich kopiert, da mein albernes Programm davon ausgeht, dass write den vollen Puffer ausführt, obwohl dies nicht der Fall ist und cygwin write () nur 64k pro Aufruf in eine Pipe ausführt.
Mit SSE2 ist dies ungefähr 15-mal schneller als der skalare Code von @Nominal Animal. Mit AVX2 ist es ungefähr 30-mal schneller. Ich habe nicht versucht , eine Version von Code der Nominal , die gerade verwendet write()
statt fwrite()
, sondern vermutlich für große Puffer stdio meist aus dem Weg bleibt. Wenn die Daten kopiert werden, führt dies zu einer starken Verlangsamung.
1 GB Daten auf einem Core2Duo E6600 (Merom 2,4 GHz, 32 KB privater L1, 4 MB gemeinsam genutzter L2-Caches), DDR2-533 MHz in 64-Bit-Linux 4.2 (Ubuntu 15.10). Diese Dimension wurde noch nicht untersucht, obwohl für write () eine Puffergröße von 128 KB verwendet wurde.
Schreiben nach / dev / null, sofern nicht anders angegeben.
- (SSE2) dies mit Zeilenumbruchbehandlung und 4 Vektoren von Ziffern aus jedem Vektor von Zufallsbytes: 0,183 s (zeitgesteuert mit 100 GiB in 18,3 s, aber ähnlichen Ergebnissen für 1 GiB-Läufe). 1,85 Anweisungen pro Zyklus.
- (SSE2) Dies, Weiterleiten an
wc -c
: 0,593 s (nicht skaliert: real = 59,266 s Benutzer = 20,148 s sys = 1 m 6,548 s, einschließlich der CPU-Zeit von wc). Die gleiche Anzahl von write () - Systemaufrufen wie bei cygwin, jedoch werden alle Daten per Piping übertragen, da Linux alle 128.000 write () -Aufrufe an eine Pipe verarbeitet.
- NominalAnimals
fwrite()
Version (gcc5.2 -O3 -march=native
), ausgeführt mit ./decdig 100 $((1024*1024*1024/200)) > /dev/null
: 3,19s +/- 0,1%, mit 1,40 Anweisungen pro Zyklus. -Funroll-Loops machten vielleicht einen winzigen Unterschied. clang-3.8 -O3 -march=native
: 3,42 s +/- 0,1%
- Nominale Weiterleitung
fwrite
an wc -c
: real = 3.980s user = 3.176s sys = 2.080s
- James Hollis 'Line-at-Time-Version (
clang++-3.8 -O3 -march=native
): 22.885s +/- 0.07%, mit 0.84 Anweisungen pro Zyklus. (g ++ 5.2 war etwas langsamer: 22,98s). Das Schreiben von jeweils nur einer Zeile hat wahrscheinlich erheblich geschadet.
- Stéphane Chazelas
tr < /dev/urandom | ...
: real = 41.430s user = 26.832s sys = 40.120s. tr
Ich habe die meiste Zeit den gesamten CPU-Kern auf sich gestellt und fast die gesamte Zeit im Kernel-Treiber verbracht, um zufällige Bytes zu generieren und sie in eine Pipe zu kopieren. Der andere Kern dieser Dual-Core-Maschine war der Rest der Pipeline.
time LC_ALL=C head -c512M </dev/urandom >/dev/null
: dh nur so viel Zufall ohne Pipe lesen: real = 35.018s user = 0.036s sys = 34.940s.
- Lưu Vĩnh Phúcs Perl-Programm (Perl v5.20.2 von Ubuntu15.10)::
LANG=en_CA.UTF-8
real = 4m32.634s user = 4m3.288s sys = 0m29.364.
LC_ALL=C LANG=C
: real = 4m18.637s user = 3m50.324s sys = 0m29.356s. Immer noch sehr langsam.
- (SSE2) dies ohne Zeilenumbruchbehandlung und entweder 3 oder 4 Vektoren von Ziffern aus jedem Vektor von zufälligen Bytes (fast genau die gleiche Geschwindigkeit: der
dig3 = v%10
Schritt ist auf dieser HW ungefähr ausgeglichen): 0,166 s (1,82 Anweisungen pro Zyklus) . Dies ist im Grunde die Untergrenze für das, was wir mit einem perfekt effizienten Newline-Handling erreichen können.
- (SSE2) Alte Version ohne Zeilenumbruch, sondern nur mit einer Ziffer pro uint16_t-Element
v%10
, 0,222 Sekunden +/- 0,4%, 2,12 Anweisungen pro Zyklus. (Kompiliert mit gcc5.2 -march=native -O3 -funroll-loops
. Unroll-Schleifen helfen bei diesem Code auf dieser Hardware. Verwenden Sie ihn nicht blind, besonders bei großen Programmen.)
- (SSE2) Alte Version davon, Schreiben in eine Datei (auf einem RAID10f2 von 3 schnellen Magnetfestplatten, nicht sehr für Schreibvorgänge optimiert): ~ 4 Sekunden. Könnte schneller gehen, indem die Einstellungen des Kernel-E / A-Puffers angepasst werden, um viel mehr schmutzige Daten vor write () -Blöcken zuzulassen. "System" -Zeit ist immer noch ~ 1,0 Sekunden, viel höher als "Benutzer" -Zeit. Auf diesem alten System mit langsamem DDR2-533-RAM dauert es ca. 4x länger, bis der Kernel die Daten in den Pagecache kopiert und die XFS-Funktionen ausführt, als auf meinem Loop, um sie in einem Puffer neu zu schreiben, der heiß bleibt Zwischenspeicher.
Wie es gemacht wird
Ein schnelles PRNG ist offensichtlich unerlässlich. xorshift128 + kann vektorisiert werden, sodass Sie zwei oder vier 64-Bit-Generatoren parallel in Elementen eines SIMD-Vektors haben. Jeder Schritt erzeugt einen vollständigen Vektor von Zufallsbytes. ( 256b AVX2 Implementierung hier mit Intel Intrinsics ). Ich habe es wegen Nominals Wahl von xorshift * ausgewählt, da die 64-Bit-Vektor-Ganzzahl-Multiplikation nur in SSE2 / AVX2 mit Techniken mit erweiterter Genauigkeit möglich ist .
Bei einem Vektor aus zufälligen Bytes können wir jedes 16-Bit-Element in mehrere Dezimalstellen aufteilen. Wir erzeugen mehrere Vektoren von 16-Bit-Elementen, bei denen es sich jeweils um eine ASCII-Ziffer + einen ASCII-Raum handelt . Wir speichern das direkt in unserem Ausgabepuffer.
Meine ursprüngliche Version hat nur verwendet x / 6554
, um eine zufällige Ziffer von jedem uint16_t-Element eines Vektors zu erhalten. Es ist immer zwischen 0 und 9, einschließlich. Es ist voreingenommen von 9
, weil (2^16 -1 ) / 6554
es nur 9.99923 ist. (6554 = ceil ((2 ^ 16-1) / 10), wodurch sichergestellt wird, dass der Quotient immer <10 ist.)
x/6554
kann mit einer Multiplikation mit einer "magischen" Konstante ( dem Festkomma-Kehrwert ) und einer Rechtsverschiebung des Ergebnisses der hohen Hälfte berechnet werden . Dies ist der beste Fall für die Division durch eine Konstante; Einige Divisoren nehmen mehr Operationen vor, und signierte Divisionen erfordern zusätzliche Arbeit. x % 10
hat eine ähnliche Tendenz und ist nicht so billig zu berechnen. (gcc ASM Ausgang entspricht x - 10*(x/10)
, also eine zusätzliche Multiplikation und Subtraktion auf der Oberseite der Teilung eine modulare multiplikative Inverse verwendet.) Auch das niedrigste Bit der xorshift128 + ist nicht so hohe Qualität , so Dividieren Entropie aus High - Bits nehmen besser ( für Qualität sowie Geschwindigkeit) als Modulo, um Entropie von niedrigen Bits zu nehmen.
Wir können jedoch mehr von der Entropie in jedem uint16_t verwenden, indem wir uns die niedrigen Dezimalstellen ansehen, wie z. B. die digit()
Funktion von @ Nominal . Um die maximale Leistung zu erzielen, habe ich mich entschieden, die niedrigen 3 Dezimalstellen zu verwenden und x/6554
eine PMULLW und PSUBW (und wahrscheinlich einige MOVDQA) zu speichern, im Vergleich zu der Option mit der höheren Qualität, bei der die 4 niedrigen Dezimalstellen verwendet werden. x / 6554 wird geringfügig von den niedrigen 3 Dezimalstellen beeinflusst, sodass eine gewisse Korrelation zwischen den Stellen desselben Elements besteht (8- oder 16-stelliger Abstand in der ASCII-Ausgabe, abhängig von der Vektorbreite).
Ich denke, dass gcc durch 100 und durch 1000 dividiert und nicht durch eine längere Kette, die nacheinander durch 10 dividiert wird, sodass die Länge der nicht durch Schleifen übertragenen Abhängigkeitskette, die 4 Ergebnisse aus jeder PRNG-Ausgabe erzeugt, wahrscheinlich nicht wesentlich verkürzt wird. port0 (Vektormultiplikation und -verschiebung) ist der Engpass aufgrund der modularen multiplikativen Inversen und der Verschiebungen in xorshift +, daher ist es definitiv nützlich, eine Vektormultiplikation zu speichern.
xorshift + ist so schnell, dass selbst die Verwendung von nur ~ 3,3 Bit Zufälligkeit von 16 (dh 20% Wirkungsgrad) nicht viel langsamer ist als das Zerlegen in mehrere Dezimalstellen. Wir nähern uns nur der gleichmäßigen Verteilung, da diese Antwort auf Geschwindigkeit ausgerichtet ist, solange die Qualität nicht zu schlecht ist.
Jede Art von bedingtem Verhalten, das eine variable Anzahl von Elementen beibehält, würde viel mehr Arbeit erfordern. (Könnte aber mit SIMD-Links-Packing-Techniken möglicherweise noch effizienter durchgeführt werden . Dies wird jedoch bei kleinen Elementgrößen weniger effizient. Riesen-Shuffle-Mask-Lookup-Tabellen sind nicht realisierbar und es gibt keine AVX2-Lane-Crossing-Shuffle mit weniger als 32-Bit.) Bit-Elemente: Eine 128-Bit-PSHUFB-Version kann mit BMI2 PEXT / PDEP zwar wie bei AVX2 mit größeren Elementen im laufenden Betrieb eine Maske generieren , dies ist jedoch schwierig, da eine 64-Bit-Ganzzahl nur 8 Byte enthält Zu dieser Antwort gibt es einen Code, der möglicherweise für höhere Elementzahlen geeignet ist.)
Wenn die Latenz des RNG ein Engpass ist, können wir noch schneller vorgehen, indem wir zwei Vektoren von Generatoren parallel schalten und abwechselnd den von uns verwendeten verwenden. Der Compiler kann immer noch problemlos alles in Registern in einer entrollten Schleife halten, wodurch die beiden Abhängigkeitsketten parallel ausgeführt werden können.
In der aktuellen Version, in der die PRNG-Ausgabe reduziert wird, besteht tatsächlich ein Engpass beim Durchsatz von Port 0, nicht bei der PRNG-Latenz, sodass dies nicht erforderlich ist.
Der Code: AVX2-Version
Vollversion mit weiteren Kommentaren zum Godbolt-Compiler-Explorer .
Nicht sehr aufgeräumt, sorry ich muss einschlafen und will das hier posten.
Um die SSE2 Version zu erhalten, s/_mm256/_mm
, s/256/128/
, s/v16u/v8u/
, und ändern vector_size(32)
bis 16. Auch den Newline Schritt ändern von 4 * 16-4 * 8. (Wie gesagt, Code ist chaotisch und nicht für das Kompilieren von zwei Versionen geeignet. Eigentlich wollte ich keine AVX2-Version erstellen, aber dann wollte ich unbedingt eine Haswell-CPU testen, auf die ich Zugriff hatte.)
#include <immintrin.h>
#include <unistd.h>
#include <stdint.h>
#include <stdio.h>
//#include <string.h>
// This would work equally fast 128b or 256b at a time (AVX2):
// https://stackoverflow.com/questions/24001930/avx-sse-version-of-xorshift128
struct rngstate256 {
__m256i state0;
__m256i state1;
};
static inline __m256i xorshift128plus_avx2(struct rngstate256 *sp)
{
__m256i s1 = sp->state0;
const __m256i s0 = sp->state1;
sp->state0 = s0;
s1 = _mm256_xor_si256(s1, _mm256_slli_epi64(s1, 23));
__m256i state1new = _mm256_xor_si256(_mm256_xor_si256(_mm256_xor_si256(s1, s0),
_mm256_srli_epi64(s1, 18)),
_mm256_srli_epi64(s0, 5));
sp->state1 = state1new;
return _mm256_add_epi64(state1new, s0);
}
// GNU C native vectors let us get the compiler to do stuff like %10 each element
typedef unsigned short v16u __attribute__((vector_size(32)));
__m256i* vec_store_digit_and_space(__m256i vec, __m256i *restrict p)
{
v16u v = (v16u)vec;
v16u ten = (v16u)_mm256_set1_epi16(10);
v16u divisor = (v16u)_mm256_set1_epi16(6554); // ceil((2^16-1) / 10.0)
v16u div6554 = v / divisor; // Basically the entropy from the upper two decimal digits: 0..65.
// Probably some correlation with the modulo-based values, especially dig3, but we do this instead of
// dig4 for more ILP and fewer instructions total.
v16u dig1 = v % ten;
v /= ten;
v16u dig2 = v % ten;
v /= ten;
v16u dig3 = v % ten;
// dig4 would overlap much of the randomness that div6554 gets
const v16u ascii_digitspace = (v16u)_mm256_set1_epi16( (' '<<8) | '0');
v16u *vecbuf = (v16u*)p;
vecbuf[0] = div6554 | ascii_digitspace;
vecbuf[1] = dig1 | ascii_digitspace;
vecbuf[2] = dig2 | ascii_digitspace;
vecbuf[3] = dig3 | ascii_digitspace;
return p + 4; // always a constant number of full vectors
}
void random_decimal_fill_buffer(char *restrict buf, size_t len, struct rngstate256 *restrict rngstate)
{
buf = __builtin_assume_aligned(buf, 32);
// copy to a local so clang can keep state in register, even in the non-inline version
// restrict works for gcc, but apparently clang still thinks that *buf might alias *rngstate
struct rngstate256 rng_local = *rngstate;
__m256i *restrict p = (__m256i*restrict)buf;
__m256i *restrict endbuf = (__m256i*)(buf+len);
static unsigned newline_pos = 0;
do {
__m256i rvec = xorshift128plus_avx2(&rng_local);
p = vec_store_digit_and_space(rvec, p); // stores multiple ASCII vectors from the entropy in rvec
#if 1
// this is buggy at the end or start of a power-of-2 buffer:
// usually there's a too-short line, sometimes a too-long line
const unsigned ncols = 100;
newline_pos += 4*16;
if (newline_pos >= ncols) {
newline_pos -= ncols;
char *cur_pos = (char*)p;
*(cur_pos - newline_pos*2 - 1) = '\n';
}
#endif
// Turning every 100th space into a newline.
// 1) With an overlapping 1B store to a location selected by a counter. A down-counter would be more efficient
// 2) Or by using a different constant for ascii_digitspace to put a newline in one element
// lcm(200, 16) is 400 bytes, so unrolling the loop enough to produce two full lines makes a pattern of full vectors repeat
// lcm(200, 32) is 800 bytes
// a power-of-2 buffer size doesn't hold a whole number of lines :/
// I'm pretty sure this can be solved with low overhead, like maybe 10% at worst.
} while(p <= endbuf-3);
*rngstate = rng_local;
}
#define BUFFER_SIZE (128 * 1024)
const static size_t bufsz = BUFFER_SIZE;
__attribute__((aligned(64))) static char static_buf[BUFFER_SIZE];
int main(int argc, char *argv[])
{
// TODO: choose a seed properly. (Doesn't affect the speed)
struct rngstate256 xorshift_state = {
_mm256_set_epi64x(123, 456, 0x123, 0x456),
_mm256_set_epi64x(789, 101112, 0x789, 0x101112)
};
for (int i=0; i < 1024ULL*1024*1024 / bufsz * 100; i++) {
random_decimal_fill_buffer(static_buf, bufsz, &xorshift_state);
size_t written = write(1, static_buf, bufsz);
(void)written;
//fprintf(stderr, "wrote %#lx of %#lx\n", written, bufsz);
}
}
Kompilieren Sie mit gcc, clang oder ICC (oder hoffentlich mit jedem anderen Compiler, der den GNU C-Dialekt von C99 und die Intelsics von Intel versteht). GNU C-Vektorerweiterungen sind äußerst praktisch, damit der Compiler die magischen Zahlen für Division / Modulo mit modularen multiplikativen Inversen generiert, und gelegentliche __attribute__
s sind nützlich.
Dies könnte portabel geschrieben werden, aber es würde mehr Code erfordern.
Leistungsmerkmale:
Der überlappende Speicher zum Einfügen von Zeilenumbrüchen ist mit erheblichem Aufwand verbunden, um zu entscheiden, wo er platziert werden soll (Verzweigungsfehler und Frontend-Engpässe bei Core2). Der Speicher selbst hat jedoch keine Auswirkungen auf die Leistung. Wenn Sie nur diese Speicheranweisung im Compiler asm auskommentieren (wobei alle Verzweigungen gleich bleiben), blieb die Leistung auf Core2 vollständig unverändert, und wiederholte Durchläufe gaben +/- weniger als 1% dieselbe Zeit. Daraus schließe ich, dass der Speicherpuffer / Cache das in Ordnung bringt.
Die Verwendung eines rotierenden Fensters ascii_digitspace
mit einem Element, das einen Zeilenvorschub enthält, ist möglicherweise sogar noch schneller, wenn Sie das Fenster so weit ausrollen, dass alle Zähler / Verzweigungen verschwinden.
Das Schreiben in / dev / null ist im Grunde genommen ein No-Op, daher bleibt der Puffer im L2-Cache wahrscheinlich heiß (256 KB pro Kern bei Haswell). Die perfekte Beschleunigung von 128b-Vektoren auf 256b-Vektoren wird erwartet: Es gibt keine zusätzlichen Anweisungen, und alles (einschließlich der Speicher) geschieht mit der doppelten Breite. Der Zweig zum Einfügen von Zeilenumbrüchen wird jedoch doppelt so häufig verwendet. Leider habe ich bei meinem Haswell Cygwin-Setup keine Zeit dafür gehabt, dass dieser Teil ausgefallen ist #ifdef
.
2,5 GHz * 32 B / 13,7 GB / s = 5,84 Zyklen pro AVX2-Speicher auf Haswell. Das ist ziemlich gut, könnte aber schneller sein. Vielleicht gibt es in den Cygwin-Systemaufrufen etwas Overhead, als ich dachte. Ich habe nicht versucht, diese in der asm-Ausgabe des Compilers zu kommentieren (was sicherstellen würde, dass nichts wegoptimiert wird.)
Der L1-Cache kann einen 32B-Speicher pro Takt unterstützen, und L2 weist keine wesentlich geringere Bandbreite auf (jedoch eine höhere Latenz).
Als ich mir IACA vor einigen Versionen ansah (ohne Verzweigung nach Zeilenumbrüchen, aber nur einen ASCII-Vektor pro RNG-Vektor), sagte es so etwas wie einen 32B-Vektorspeicher pro 4 oder 5 Takte voraus.
Ich hatte gehofft, durch das Extrahieren von mehr Daten aus jedem RNG-Ergebnis eine Beschleunigung zu erzielen, indem ich mir den Asm selbst ansah und die Anleitungen von Agner Fog und andere Optimierungsressourcen berücksichtigte, für die ich Links im SO x86-Tag-Wiki hinzugefügt habe .)
Auf Skylake wäre dies wahrscheinlich bedeutend schneller , da die Multiplikation und Verschiebung von Vektor-Ganzzahlen auf doppelt so vielen Ports (p0 / p1) ausgeführt werden kann wie bei Haswell (nur p0). Sowohl die Xorshift- als auch die Ziffernextraktion verwenden viele Verschiebungen und Multiplikationen. ( Update: Skylake führt es mit 3.02 IPC aus, was 3,77 Zyklen pro 32-Byte-AVX2-Speicher ergibt, zeitgesteuert mit 0.030s pro 1-GB-Iteration, und schreibt /dev/null
auf Linux 4.15 auf i7-6700k mit 3.9GHz.
Es ist kein 64-Bit-Modus erforderlich, um einwandfrei zu funktionieren . Die SSE2-Version ist beim Kompilieren genauso schnell -m32
, da sie nicht sehr viele Vektorregister benötigt und die gesamte 64-Bit-Mathematik in Vektoren und nicht in Allzweckregistern ausgeführt wird.
Im 32-Bit-Modus ist es auf Core2 sogar etwas schneller, da die Makrofusion von Compare / Branch nur im 32-Bit-Modus funktioniert. Daher gibt es weniger Uops für den nicht ordnungsgemäßen Core (18.3s (1.85 Instructions Per Clock) vs 16,9 s (2,0 IPC)). Die kleinere Codegröße ohne REX-Präfix hilft auch den Core2-Decodern.
Außerdem werden einige Reg-Reg-Vektorbewegungen durch Ladevorgänge ersetzt, da nicht mehr alle Konstanten in Vektorregs festgelegt sind. Da der Ladedurchsatz aus dem L1-Cache kein Engpass ist, hilft dies tatsächlich. (z. B. Multiplikation mit einem konstanten Vektor von set1(10)
: movdqa xmm0, xmm10
/ pmullw xmm0, xmm1
wird zu movdqa xmm0, [constant]
/ pmullw xmm0, xmm1
.) Da für reg-reg MOVDQA ein ALU-Port erforderlich ist, konkurriert es mit der tatsächlich ausgeführten Arbeit, aber ein MOVDQA-Ladevorgang konkurriert nur um die Front-End-Dekodierungsbandbreite. (Wenn eine 4-Byte-Adresse in vielen Befehlen enthalten ist, wird ein Großteil des Gewinns durch das Speichern von REX-Präfixen aufgehoben.
Es würde mich nicht wundern, wenn beim Speichern von ALU MOVDQA-Ups die eigentlichen Gewinne erzielt werden, da das Frontend mit dem Durchschnitt von 2,0 IPC ziemlich gut mithalten sollte.
Alle diese Unterschiede verschwinden in Haswell, wo das Ganze vom decodierten UOP-Cache ausgeführt werden sollte, wenn nicht vom Loopback-Puffer. ALU + Branch Macro-Fusion funktioniert seit Nehalem in beiden Modi.