Speicherkopiererei

Meine Frage ist kurz aber essentiell: Warum verdammt ist memcpy so schnell? Können die zaubern?

Meine Frage ist kurz aber essentiell: Warum verdammt ist
memcpy so schnell? Können die zaubern?

Kurze Antwort: RAM ist einfach schnell.
Die Festplatte braucht nicht bemüht werden…

Bruno

Meine Frage ist kurz aber essentiell: Warum verdammt ist
memcpy so schnell? Können die zaubern?

Kurze Antwort: RAM ist einfach schnell.
Die Festplatte braucht nicht bemüht werden…

Warum ist eine Schleife die ich selbst schreibe dann so viel langsamer?

Schleife:

BYTE* Zeiger = irgendeine Zieladresse;
BYTE* Zeiger2 = irgendeine Quelladresse;

for( int a = 0; a

Warum ist eine Schleife die ich selbst schreibe dann so viel
langsamer?

Keine Ahnung, wie die memcpy() Funktion intern auf deiner Maschine implementiert wurde. Die werden das vermutlich direkt in Assembler-Code unter Ausnutzung aller Performance und Caching-Tricks gebaut haben.

Bruno

Meine Frage ist kurz aber essentiell: Warum verdammt ist
memcpy so schnell? Können die zaubern?

Die heutigen Prozessoren (d.h. eigentlich schon ab dem 386, wenn nicht schon der 286) haben hardwaremäßig implementierte Kopierbefehle (MOVSB, MOVSW, MOVSD), die naturgemäß schneller ablaufen. Siehe z.B. hier:
http://www.acc.umu.se/~john/TAE/appd/movsb.htm
Der Compiler wird memcpy darauf abgestimmt haben.

gruß

J.

Ich versuch euch mal klarzumachen, was es bedeuten würde, wenn man memcpy nochmal in dieser spitzenmäßigen Performance - Form kompilieren könnte, nachdem meine einige winzige Änderungen ( an der Zeigerarithmetik ) vorgenommen hat: Man könnte superschnelle Kopieraktionen schreiben, die auf eine bestimmte Programmsituation abgestimmt sind. Was ist z.B. wenn die Abstände zwischen den zu kopierenden Werten nicht 1 ( einfacher Zeigerinkrement ) sondern unregelmäßig sind? Ist das der Fall kann man memcpy bereits nicht mehr benutzen und man muß eigene, viel lansamere Funktionen entwerfen. Also: Könnte man memcpy entsprechend anpassen, ergäben sich ungeahnte Möglichkeiten. Bin nämlich momentan daran eine Texturierungsfunktion ( 3D ) zu schreiben. Mir ist jeder Performance - Schub zur Zeit recht. Irgendwelche Vorschläge, wie man die Idee realisieren könnte ( also die mit dem variierten memcpy )?

Ich versuch euch mal klarzumachen, was es bedeuten würde, wenn
man memcpy nochmal in dieser spitzenmäßigen Performance - Form
kompilieren könnte, nachdem meine einige winzige Änderungen (
an der Zeigerarithmetik ) vorgenommen hat:

Hmmm. Ich habe den Quellcode zu memcpy nicht hier; was ich über MOVSB geschrieben habe, war eine Vermutung („Der Compiler wird memcpy darauf abgestimmt haben.“). Wenn sie aber zutrifft, hast Du eher schlechte Karten, denn MOVSB und Derivate sind in der Hardware implementiert. Das bedeutet:
In Deiner Routine hast Du drei Variablen, die in jedem Schleifendurchlauf erhöht werden müssen (Zeiger, zeiger2 und a); bei MOVSB ist es nur ein Prozessorregister (CX, wen ich mich recht entsinne), dessen Inhalt auf die Quell- und Zielregister addiert wird. Das geht viel schneller. Aber Du kannst den Befehl MOVSB leider nicht verändern.

Ein paar Dinge kannst Du evtl. tun und ausprobieren, ob das evtl. schneller geht:

  1. Programmier Deine Schleife nach MOVSB-Manier:

    BYTE* Zeiger = irgendeine Zieladresse;
    BYTE* Zeiger2 = irgendeine Quelladresse;

    for( int a = 0; a
    Damit erhöhst Du auch nur einmal a, aber nicht die Zeiger. Der Compiler sollte hier indirekte Adressierung verwenden, dann ist das schneller.
    2. Deklariere a als register-Variable.

    Bin nämlich momentan daran eine
    Texturierungsfunktion ( 3D ) zu schreiben.

    Da fällt mir ein: Texturierungsfunktion? Arbeitest Du dabei im Speicher der Grafikkarte? Da gibt es doch ganz andere Dinge zu beachten. Ich kenne das zwar nur aus der vorsintflutlichen VGA-Karte, aber schon diese hatte einen 4x breiteren und wesentlich schnelleren Datenbus; bei internen Kopieroperationen (innerhalb des Karten-RAM) geht alles ratzfatz. Da gibt es spezielle Routinen, die im BIOS der Karte implementiert sind. Vielleicht solltest Du die Doku der Kartenhersteller zu Rate ziehen.

    Gruß

    J.

Was ist z.B. wenn die
Abstände zwischen den zu kopierenden Werten nicht 1 (
einfacher Zeigerinkrement ) sondern unregelmäßig sind? Ist das
der Fall kann man memcpy bereits nicht mehr benutzen und man
muß eigene, viel lansamere Funktionen entwerfen.

Wahrscheinlich (reine Vermutung) wäre dann der Geschwindigkeitsvorteil von memcpy auch weg, weil es eben zusammenhängende Blöcke braucht?

Wenn Performance wirklich alles ist, dann solltest du dich vielleicht tatsächlich mit der Prozessorarchitektur ud Assembler beschäftigen und die wichtigen Teile darin schreiben…

Bruno

Da fällt mir ein: Texturierungsfunktion? Arbeitest Du dabei im
Speicher der Grafikkarte? Da gibt es doch ganz andere Dinge zu
beachten. Ich kenne das zwar nur aus der vorsintflutlichen
VGA-Karte, aber schon diese hatte einen 4x breiteren und
wesentlich schnelleren Datenbus; bei internen
Kopieroperationen (innerhalb des Karten-RAM) geht alles
ratzfatz. Da gibt es spezielle Routinen, die im BIOS der Karte
implementiert sind. Vielleicht solltest Du die Doku der
Kartenhersteller zu Rate ziehen.

Da hast du absolut Recht. Ja, ich wühle im Speicher der Grafikkarte rum. Da ich das aber mit DirectX ( DirectDraw ) mache, kann ich selber bzgl. der speziellen Routinen im BIOS der Karte nichts mehr optimieren.
Manchmal wundert man sich tatsächlich, wie schnell kopiert werden kann. Auf der anderen Seite ist bei Raycasting - Techniken niemals genug Geschwindigkeit verfügbar, glaub’s mir. Somit bleibt einem nur die Möglichkeit, an den Schleifen zu optimieren. Dabei fällt auf: Zeichnet man z.B. ein Rechteck auf den Bildschirm, muß man die Grafikdaten Zeilenweise in den Speicher der Karte kopieren. Nehme ich memcpy anstatt einer eigenen Schleife, kann ich manchmal die Geschwindigkeit verdoppeln ( ! ), obwohl meine Schleife nach meinem Kenntnisstand absolut optimiert ist ( in C++ ). Will ich nun aber texturieren, zeichne ich Dreiecke und hole Grafikdaten aus „gedachten“ Dreiecken. Es ist klar, daß ich hier memcpy nicht mehr nutzen kann.

Übrigens war der Schleifentypus, den ich verwendet habe im Vergleich zu deiner Variante bei 307200 Pixel bei 16Bit Tiefe ( 640x480 ) um gute zehn Frames schneller. Aber danke für den Tipp. Ist das vielleicht compilerabhängig was schneller ist? Nur zum Test definiere ich manchmal einfach so Variablen an entsprechenden Stellen als register. Ich hab’s auch schon an dem Punkt verwendet, wo du es mir geraten hast. Ein Performance - Unterschied ist nie festzustellen.

Florian

Meine Frage ist kurz aber essentiell: Warum verdammt ist
memcpy so schnell? Können die zaubern?

Du solltest versuchen DoppelWortweise zu kopieren, dann werden nämlich 4 Bytes in einem Rutsch kopiert (der Datenbus ist ab der 386CPU 32 Bit breit).
Wenn die Start- bzw. Zieladresse nicht auf einer DWORD grenze liegt, dann must du die ersten, bzw. letzten paar bytes Byteweise kopieren.

Du solltest versuchen DoppelWortweise zu kopieren, dann werden
nämlich 4 Bytes in einem Rutsch kopiert (der Datenbus ist ab
der 386CPU 32 Bit breit).
Wenn die Start- bzw. Zieladresse nicht auf einer DWORD grenze
liegt, dann must du die ersten, bzw. letzten paar bytes
Byteweise kopieren.

Wie stelle ich fest, ob meine Start- bzw. Zieladresse am Anfang eines DWORDs liegt? Durch vier teilen und gucken ob die Rechnung glatt aufgeht?

Übrigens war der Schleifentypus, den ich verwendet habe im
Vergleich zu deiner Variante bei 307200 Pixel bei 16Bit Tiefe
( 640x480 ) um gute zehn Frames schneller. Aber danke für den
Tipp. Ist das vielleicht compilerabhängig was schneller ist?
Nur zum Test definiere ich manchmal einfach so Variablen an
entsprechenden Stellen als register. Ich hab’s auch schon an
dem Punkt verwendet, wo du es mir geraten hast. Ein
Performance - Unterschied ist nie festzustellen.

Die geschwindigkeitsunterschiede verschiedener Konstrukte ist Compilerabhängig, da viele Compiler hersteller Ihr eigenes Süppchen Kochen. Da hilft nur ein Profiler weiter.

Die Register anweisungen sind nur eine EMPFEHLUNG an den Compiler. Es kann sein das der die Variablen ohnehin schon ins Register verlagert hat, es kann aber auch sein das er es trotz Anweisung NICHT tun wird. Wie das mit Empfehlungen nun mal so ist…

Hallo Florian

Meine Frage ist kurz aber essentiell: Warum verdammt ist
memcpy so schnell? Können die zaubern?

Es ist schon so wie schon vermutet wurde: memcpy() ist in Assembler geschrieben und das lineare kopieren wird direkt durch einen einzigen Maschinen-Befehl bewerkstelligt.
Hier die etwas vereinfachte Form:

LDS SI, QuellAdresse
LES DI, ZielAdresse
MOV CX, Anzahl
REP MOVSB ; Hier wird jetzt copiert

Die schnellste Variante in C ist immer:

BYTE\* Zeiger = irgendeine Zieladresse;
BYTE\* Zeiger2 = irgendeine Quelladresse; 
for( int a = 0; a 
der Ausdruck "\*Zeiger++" wird wie folgt aufgelöst:
SpeicherAdresse = Lade: Zeiger;
Lade: SpeicherAdresse
Addiere: Zeiger + 1
Speichere: Zeiger
Total: 2\*Lade, 1\*Speichere und 1\*Addition
(Eine Addition von 1, 2 oder 4 ist normalerweise als Maschienen-Befehl vorhanden)

Die vorgeschlagene Variante mit Array ist leider die langsamste:

    
     BYTE\* Zeiger = irgendeine Zieladresse;
     BYTE\* Zeiger2 = irgendeine Quelladresse;
    
     for( int a = 0; a 
    Array-Arithmetik ist immer recht langsam, der Ausdruck "\*( Zeiger[a] )" muss wie folgt aufgelöst werden:
    SpeicherAdresse = Zieladresse + (a \* sizeof(BYTE));
    Lade: a
    Lade: ElementGrösse
    Multipliziere: a \* ElementGrösse
    Lade: Zieladresse
    Addiere: Zieladresse + (a \* ElementGrösse)
    Lade: SpeicherAdresse
    Total: 4\*Lade, 1\*Addition und 1\*Multiplikation
    
    OK. in diesem Beispiel (BYTE) enfällt die Multiplikation mit 1 aber bei WORD hast du also eine Addition und eine Multiplikation um die SpeicherAdresse zu berechnen.
    
    MfG Peter(TOO)

Hallo Florian !

Bzgl. der Kopiererei ist ja schon eine Menge gesagt worden.
Vielleicht kannst Du mit folgender Loeschfunktion (mittels FPU) was anfangen:

void g_Memclear(g_void * dst, g_int bsize)
{
__asm
{
mov ecx,bsize
mov edi,dst
shr ecx,3
fldz // mit Null laden
cloop: fst qword ptr [edi]
add edi,8
dec ecx
jnz cloop
}
}

Renderst Du die einzelnen Frames im RAM ?
Auf AGP-Grafikkarten laeuft mein Softwarerenderer mit folgender Einstellungen am schnellsten:

DDSURFACEDESC surfdesc;
surfdesc.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE | DDSCAPS_FLIP | DDSCAPS_COMPLEX | DDSCAPS_VIDEOMEMORY;

Falls Du an Source interessiert bist, maile mir !

Markus

Auf AGP-Grafikkarten laeuft mein Softwarerenderer mit
folgender Einstellungen am schnellsten:

DDSURFACEDESC surfdesc;
surfdesc.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE |
DDSCAPS_FLIP | DDSCAPS_COMPLEX | DDSCAPS_VIDEOMEMORY;

Falls Du an Source interessiert bist, maile mir !

Markus

Bei primären Oberflächen ist nur wichtig, daß sie im Videospeicher ist. Es ist viel wichtiger, daß die Oberflächen für die Grafikinformationen ( also die Textur ) im Arbeitsspeicher ist!!!

Cooler Algorithmus, muß ihn mal probieren, danke.