Autor |
Beitrag |
user32
Beiträge: 55
Erhaltene Danke: 5
|
Verfasst: Sa 11.07.15 00:58
Kennt sich hier jemand mit SSE aus, oder kennt ein einigermaßen aktives Forum für so Assembler Zeug?
Bin gerade dabei, eine Prozedur zu optimieren und habe ein kleines Problemchen.
Bzw. die passende Delphi Frage. Denn damit könnte ich eventuell das Problem selbst finden:
Bei Delphi gibt das ja leider kein Debug für XMM Register. Oder ist meine Version (D7) zu alt?
Gibt es noch eine andere Möglichkeit, da reinzuschauen/debuggen?
|
|
jaenicke
Beiträge: 19285
Erhaltene Danke: 1743
W11 x64 (Chrome, Edge)
Delphi 11 Pro, Oxygene, C# (VS 2022), JS/HTML, Java (NB), PHP, Lazarus
|
Verfasst: Sa 11.07.15 22:09
Neuere Delphiversionen können das auch nicht.
Es gibt aber in der JEDI JCL ein entsprechendes Debugfenster:
Unter jcl\jcl\packages: JclSIMDViewExpert.dproj
Leider funktioniert das nur bis einschließlich Windows 7, da die benötigte API Funktion GetEnabledExtendedFeatures mit Windows 8 aus der kernel32.dll entfernt wurde.
|
|
OldGrumpy
Beiträge: 82
|
Verfasst: So 12.07.15 07:24
Hmm, sollte RtlGetEnabledExtendedFeatures (siehe MSDN) nicht die gleichen Informationen liefern? Laut MSDN gibt es die Funktion auch in 8 aufwärts.
|
|
Martok
Beiträge: 3661
Erhaltene Danke: 604
Win 8.1, Win 10 x64
Pascal: Lazarus Snapshot, Delphi 7,2007; PHP, JS: WebStorm
|
Verfasst: Mo 13.07.15 05:07
_________________ "The phoenix's price isn't inevitable. It's not part of some deep balance built into the universe. It's just the parts of the game where you haven't figured out yet how to cheat."
|
|
SMO
Beiträge: 120
Erhaltene Danke: 18
D2005 Personal
|
Verfasst: Mo 13.07.15 18:23
jaenicke hat folgendes geschrieben : | Neuere Delphiversionen können das auch nicht. |
Doch, natürlich können die XMM-Register anzeigen.
Einfach ins CPU-Fenster (Strg+Alt+C), dort Kontextmenü (rechter Mausklick) und ganz unten im Menü "View FPU".
|
|
jaenicke
Beiträge: 19285
Erhaltene Danke: 1743
W11 x64 (Chrome, Edge)
Delphi 11 Pro, Oxygene, C# (VS 2022), JS/HTML, Java (NB), PHP, Lazarus
|
Verfasst: Mo 13.07.15 22:33
Ok, das hatte ich vorhin nicht gesehen. Da ich das nicht brauchte, habe ich auch nie genauer geschaut gehabt. Das erklärt auch warum das JEDI Package nicht mehr gepflegt ist.
|
|
user32
Beiträge: 55
Erhaltene Danke: 5
|
Verfasst: Mi 15.07.15 16:43
SMO hat folgendes geschrieben : | jaenicke hat folgendes geschrieben : | Neuere Delphiversionen können das auch nicht. |
Doch, natürlich können die XMM-Register anzeigen.
Einfach ins CPU-Fenster (Strg+Alt+C), dort Kontextmenü (rechter Mausklick) und ganz unten im Menü "View FPU". |
Weißt du denn, ab welcher Delphi-Version es das gibt?
Runterscrollen für Update!!!
Also gut, hier mal etwas Code:
Delphi-Quelltext 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25:
| function Add3Vectors(V1, V2, V3: TVector) :TVector;
ASM movupd xmm0, dqword ptr[v1.x] movupd xmm1, dqword ptr[v2.x] movupd xmm2, dqword ptr[v3.x] addpd xmm0, xmm1 addpd xmm0, xmm2 shufpd xmm0, xmm0, $1 movupd dqword ptr[result.x], xmm0
movsd xmm0, qword ptr[v1.z] movsd xmm1, qword ptr[v2.z] movsd xmm2, qword ptr[v3.z] addsd xmm0, xmm1 addsd xmm0, xmm2 movsd qword ptr[result.z], xmm0 end; |
TVector ist nur ein einfaches Record mit 3 Doubles.
Dies führt zu Zugriffsverletzungen an scheinbar unterschiedlichen Stellen in meinem Programm, so wie unkontrolliertem Verhalten
(ein Integer-Counter an einer komplett anderen Stelle wird plötzlich negativ und so um die 10 Größenordnungen falsch?????). Keine Ahnung wieso.
Meines Erachtens, ist der Code absolut korrekt, aber ich bin lange kein Assembler Experte.
Ein kleiner Test:
Delphi-Quelltext 1: 2: 3: 4: 5:
| v1 := MakeVector(1.3, 2.3, 3.3); v2 := MakeVector(10.3, 20.3, 30.3); v3 := MakeVector(40.3, 50.3, 60.3); v := Add3Vectors(v1,v2,v3) ; |
Verrückt!
Das ist nicht das erste mal, dass ich mit XMM-Assembler Probleme habe/bekomme.
Vielleicht unterstützt Delphi 7 es ja doch nicht so ganz richtig.
Oder ich bin zu doof....
Edit:
Okay. Mit...
Delphi-Quelltext 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27:
| function Add3Vectors(V1, V2, V3: TVector) :TVector; var u : TVector; begin ASM movupd xmm0, dqword ptr[v1.x] movupd xmm1, dqword ptr[v2.x] movupd xmm2, dqword ptr[v3.x] addpd xmm0, xmm1 addpd xmm0, xmm2 movupd dqword ptr[u.x], xmm0 movsd xmm0, qword ptr[v1.z] movsd xmm1, qword ptr[v2.z] movsd xmm2, qword ptr[v3.z] addsd xmm0, xmm1 addsd xmm0, xmm2 movsd qword ptr[u.z], xmm0 end; result := u; end; |
..geht es.
WTF!!!!
Würde mir das bitte jemand erklären? KANN das überhaupt jemand erklären?
Warum muss ich noch extra eine zusätzliche Variable nehmen?
Keine Exceptions mehr und korrektes Ergebnis.
( Das
shufpd xmm0, xmm0, $1
oben war zwar auch falsch, aber NICHT der Auslöser der Exceptions . Die kamen immer bei den Schreibzugriffen also
movupd dqword ptr[result.x], xmm0
und
Delphi-Quelltext 1:
| movsd qword ptr[result.z], xmm0 |
|
|
OlafSt
Beiträge: 486
Erhaltene Danke: 99
Win7, Win81, Win10
Tokyo, VS2017
|
Verfasst: Mi 15.07.15 23:29
Ich meine mich zu erinnern, das einfache, ordinale Typen über die Prozessorregister zurückgeliefert werden (AL, AX, EAX, EAX:EDX). Alles andere wird als Pointer in EAX:EDX zurückgeliefert, was hauptsächlich Strings und Records meint. Somit müßtest du Result.X ganz anders adressieren - ein "movupd dqword ptr[Result.x], xmm0" kann da also nicht funktionieren und schreibt folglich "irgendwo" hin. Was damit auch erklärt, warum sich urplötzlich Variablen an den verrücktesten Stellen ohne ersichtlichen Grund verändern, weil deine Routine wild 16 Bytes in den Speicher schreibt.
Durch die Deklaration einer lokalen Variable erfolgt nun eine korrekte Adressierung und der ganze Schmonz funktioniert plötzlich.
_________________ Lies, was da steht. Denk dann drüber nach. Dann erst fragen.
|
|
SMO
Beiträge: 120
Erhaltene Danke: 18
D2005 Personal
|
Verfasst: Do 16.07.15 00:38
user32 hat folgendes geschrieben : | Weißt du denn, ab welcher Delphi-Version es das gibt? |
Leider nicht.
user32 hat folgendes geschrieben : |
Delphi-Quelltext 1:
| function Add3Vectors(V1, V2, V3: TVector) :TVector; | |
Nur mal zum Grundverständnis: wenn du wie hier Records als Parameter übergibst und in der Subroutine nicht schreibend auf die Parameter zugreifst, dann markiere sie als "const". Dann kann der Compiler eventuell etwas besser optimieren. (Also: function Add3Vectors( const V1, V2, V3: TVector): TVector)
Und da ein Record allgemein nicht einfach in edx:eax zurückgegeben werden kann wie ordinale Typen, bedient sich der Compiler eines Tricks:
er macht aus der "function" eine "procedure" und hängt den "Rückgabewert" als letzten Parameter an.
Delphi-Quelltext 1: 2: 3: 4:
| function Add3Vectors(const V1, V2, V3: TVector): TVector; procedure Add3Vectors(const V1, V2, V3: TVector; out Result: TVector); |
Oder auch "var" statt "out". Der springende Punkt jedenfalls ist, dass "Result" ein zusätzlicher Parameter und eine Referenz (Pointer) ist. Das muss im Assemblercode berücksichtigt werden.
In deinem Beispiel passen die Parameter V1, V2, V3 (ebenfalls Pointer, weil Records) gerade in die Register eax, edx, ecx (Die typische "register" Aufrufkonvention von Delphi).
Für den vierten Parameter, Result, ist kein Register mehr frei und er landet auf dem Stack ([ebp+8]).
Wenn du jetzt Code wie "movupd xmm0, dqword ptr [v1.x]" hast, dann führt Delphi eine ganz primitive Substitution durch. "V1" wurde in eax übergeben, also wird V1 einfach als Synonym von eax behandelt, und der Code in "movupd xmm0, dqword ptr [eax]" umgewandelt (bzw. [eax+0], da 0 das Offset des "x" Feldes im Record ist).
Der Pointer in eax wird automatisch dereferenziert und funktioniert alles wie gewollt.
Aber bei "movupd dqword ptr [result.x], xmm0" knallt es. Warum? Weil "Result" eben [ebp+8] entspricht. Das müsste praktisch doppelt dereferenziert werden, "movupd dqword ptr [[ebp+8 ]], xmm0".
Diese Instruktion gibt es aber nicht.
Es bleibt "movupd dqword ptr [ebp+8 ], xmm0" und das ist falsch (zerstört andere Daten auf dem Stack).
Lösung:
Delphi-Quelltext 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28:
| function Add3Vectors(V1, V2, V3: TVector): TVector;
asm push ebx mov ebx, Result movupd xmm0, dqword ptr[V1.x] movupd xmm1, dqword ptr[V2.x] movupd xmm2, dqword ptr[V3.x] addpd xmm0, xmm1 addpd xmm0, xmm2 shufpd xmm0, xmm0, 1 movupd dqword ptr [TVector(ebx).x], xmm0
movsd xmm0, qword ptr[V1.z] movsd xmm1, qword ptr[V2.z] movsd xmm2, qword ptr[V3.z] addsd xmm0, xmm1 addsd xmm0, xmm2 movsd qword ptr [TVector(ebx).z], xmm0 pop ebx end; |
Hier wird ebx als Container des Result-Pointers benutzt. Das erfordert natürlich Vorwissen: ebx wird von keinem der Parameter belegt (die wollen wir ja nicht überschreiben), darf aber nicht von einer Subroutine verändert werden und muss daher mit push/pop gesichert und wiederhergestellt werden.
Wenn der TVector-Typecast in deiner Version nicht geht, musst du die Offsets der Felder eben anders angeben, notfalls direkt ([ebx+0] und [ebx+16]).
Alternativ könntest du hier statt ebx auch ebp nehmen und dir somit push und pop sparen (ebp wird automatisch vom Compiler gesichert und wiederhergestellt), aber einen deutlichen Geschwindigkeitsvorteil wird das nicht bringen.
Dein zweites Beispiel funktioniert, weil die Hilfsvariable "u" wie alle lokalen Variablen auf dem Stack liegt und somit "direkt" zugreifbar ist.
Im obigen Fall lag ja nur ein Pointer zu Result auf dem Stack, der erst dereferenziert werden musste. Mit "Add 2Vectors" hättest du das Problem nicht gehabt, denn dann wäre "Result" in ecx gelandet.
64-bit-Versionen von Delphi unterstützen übrigens nur komplette Assembler-Subroutinen wie deine erste Implementierung. "Mischmasch" mit Inline-Assembly wie in deiner zweiten Implementierung geht da nicht mehr. Das ist nur ein Punkt warum die erste Variante vorzuziehen ist.
|
|
user32
Beiträge: 55
Erhaltene Danke: 5
|
Verfasst: Do 16.07.15 03:18
SMO hat folgendes geschrieben : |
In deinem Beispiel passen die Parameter V1, V2, V3 (ebenfalls Pointer, weil Records) gerade in die Register eax, edx, ecx (Die typische "register" Aufrufkonvention von Delphi).
Für den vierten Parameter, Result, ist kein Register mehr frei und er landet auf dem Stack ([ebp+8]).
Wenn du jetzt Code wie "movupd xmm0, dqword ptr [v1.x]" hast, dann führt Delphi eine ganz primitive Substitution durch. "V1" wurde in eax übergeben, also wird V1 einfach als Synonym von eax behandelt, und der Code in "movupd xmm0, dqword ptr [eax]" umgewandelt (bzw. [eax+0], da 0 das Offset des "x" Feldes im Record ist).
Der Pointer in eax wird automatisch dereferenziert und funktioniert alles wie gewollt.
Aber bei "movupd dqword ptr [result.x], xmm0" knallt es. Warum? Weil "Result" eben [ebp+8] entspricht. Das müsste praktisch doppelt dereferenziert werden, "movupd dqword ptr [[ebp+8]], xmm0".
Diese Instruktion gibt es aber nicht.
Es bleibt "movupd dqword ptr [ebp+8], xmm0" und das ist falsch (zerstört andere Daten auf dem Stack).
Lösung:
Delphi-Quelltext
Hier wird ebx als Container des Result-Pointers benutzt. Das erfordert natürlich Vorwissen: ebx wird von keinem der Parameter belegt (die wollen wir ja nicht überschreiben), darf aber nicht von einer Subroutine verändert werden und muss daher mit push/pop gesichert und wiederhergestellt werden. |
Oh DANKE!! Du bist die Erlösung!! Ich war schon halb am durchdrehen
SMO hat folgendes geschrieben : |
Mit "Add2Vectors" hättest du das Problem nicht gehabt, denn dann wäre "Result" in ecx gelandet. |
Ja, mit zwei Vektoren HATTE ich das Problem auch nicht. Deswegen wusste ich nicht was auf einmal los ist.
SMO hat folgendes geschrieben : |
Aber bei "movupd dqword ptr [result.x], xmm0" knallt es. Warum? Weil "Result" eben [ebp+8] entspricht. Das müsste praktisch doppelt dereferenziert werden, "movupd dqword ptr [[ebp+8]], xmm0".
Diese Instruktion gibt es aber nicht.
Es bleibt "movupd dqword ptr [ebp+8], xmm0" und das ist falsch (zerstört andere Daten auf dem Stack). |
Da hätten wir dann wohl auch direkt den Grund, warum andere Teile des Programms unvorhersehbar reagiert haben. Perfekt danke!
Aber andersrum ebp-8 könnte man doch nutzen oder? Das wäre dann ja unbenutzter Speicher unter dem Stackpointer?
Und wenn die Records ja eh alles Pointer sind, kann man doch auch sicher irgendwie dafür sorgen, dass meine Variablen 16-Byte-Aligned sind, und ich dann MOVAPD statt dem langsameren MOVUPD nehmen kann, oder?
Hab so eine ungefähre Idee wie das gehen könnte, aber weiß nicht genau. Diese Funktionen werden bei mir so an die 200.000 bis 2 Millionen mal aufgerufen.
Dann müsste ich den Variablenspeicher ja komplett manuell adressieren und die Rekords wären überflüssig oder? Also mit GetMem, und dann mit $FFFFFFF0 ANDen, damit er durch 16 teilbar ist.
|
|
SMO
Beiträge: 120
Erhaltene Danke: 18
D2005 Personal
|
Verfasst: Do 16.07.15 13:09
user32 hat folgendes geschrieben : | Da hätten wir dann wohl auch direkt den Grund, warum andere Teile des Programms unvorhersehbar reagiert haben. Perfekt danke!
Aber andersrum ebp-8 könnte man doch nutzen oder? Das wäre dann ja unbenutzter Speicher unter dem Stackpointer? |
Ja, könnte man.
user32 hat folgendes geschrieben : |
Und wenn die Records ja eh alles Pointer sind, kann man doch auch sicher irgendwie dafür sorgen, dass meine Variablen 16-Byte-Aligned sind, und ich dann MOVAPD statt dem langsameren MOVUPD nehmen kann, oder?
Hab so eine ungefähre Idee wie das gehen könnte, aber weiß nicht genau. Diese Funktionen werden bei mir so an die 200.000 bis 2 Millionen mal aufgerufen.
Dann müsste ich den Variablenspeicher ja komplett manuell adressieren und die Rekords wären überflüssig oder? Also mit GetMem, und dann mit $FFFFFFF0 ANDen, damit er durch 16 teilbar ist. |
Delphi bietet leider keine Möglichkeit, die Speicherausrichtung von Variablen direkt zu bestimmen. Die $A/$ALIGN Direktive betrifft nur die Ausrichtung von Feldern innerhalb eines Records, nicht die Adresse des Records selbst. Neuere Versionen haben auch ein $CODEALIGN zur Ausrichtung von Code.
Deine Idee sollte funktionieren, aber wenn du es einfacher haben willst, benutze FastMM als Memorymanager (ist in späteren Delphi-Versionen integriert, bei Delphi 7 noch nicht, wenn ich mich recht erinnere). Muss einfach als erste Unit eingebunden werden (Anleitung lesen), dann ersetzt FastMM GetMem usw. durch schnellere eigene Funktionen. Die liefern Speicherblöcke zurück, die immer 16-byte-aligned sein sollten (wie gesagt, Anleitung lesen, und testen).
Geht dann z.B. so:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31:
| type PVector = ^TVector; TVector = record x, y, z: Double; end; procedure SetVector(const x, y, z: Double; out Vector: TVector); begin Vector.x := x; Vector.y := y; Vector.z := z; end; procedure DoSomething; var v1, v2, v3: PVector; begin New(v1); New(v2); New(v3); SetVector(1, 0, 0, v1^); SetVector(0, 1, 0, v2^); SetVector(0, 0, 1, v3^); v1^ := Add3VectorsAligned(v1^, v2^, v3^); Dispose(v3); Dispose(v2); Dispose(v1); end; |
Etwas lästig, aber machbar. Du musst eben testen, ob das Drumherum den Geschwindigkeitsvorteil von MOVAPD usw. wieder zunichte macht.
|
|
|