Autor Beitrag
user32
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starofftopic star
Beiträge: 55
Erhaltene Danke: 5



BeitragVerfasst: 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
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starofftopic star
Beiträge: 19272
Erhaltene Danke: 1740

W11 x64 (Chrome, Edge)
Delphi 11 Pro, Oxygene, C# (VS 2022), JS/HTML, Java (NB), PHP, Lazarus
BeitragVerfasst: 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
ontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic starofftopic star
Beiträge: 82



BeitragVerfasst: 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
ontopic starontopic starontopic starontopic starontopic starontopic starofftopic starofftopic star
Beiträge: 3661
Erhaltene Danke: 604

Win 8.1, Win 10 x64
Pascal: Lazarus Snapshot, Delphi 7,2007; PHP, JS: WebStorm
BeitragVerfasst: Mo 13.07.15 05:07 
user profile iconuser32 hat folgendes geschrieben Zum zitierten Posting springen:
Kennt sich hier jemand mit SSE aus, oder kennt ein einigermaßen aktives Forum für so Assembler Zeug?
Hier gibts einige die sowas machen, könntest du also Glück haben. Dafür gibt's ja die Sparte "Optimierung/Assembler" ;)


user profile iconuser32 hat folgendes geschrieben Zum zitierten Posting springen:
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?
Lazarus/GDB kann das, ggf. könntest du das nutzen um den Algorithmus da zu debuggen und den fertigen Code in Delphi zurück zu übernehmen. Kommt halt drauf an, wie gut sich das rausmodularisieren lässt.
Was mich grade wundert: der D7-Assembler kennt SSE-Instruktionen? Verrückt.

_________________
"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
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starofftopic star
Beiträge: 120
Erhaltene Danke: 18


D2005 Personal
BeitragVerfasst: Mo 13.07.15 18:23 
user profile iconjaenicke hat folgendes geschrieben Zum zitierten Posting springen:
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
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starofftopic star
Beiträge: 19272
Erhaltene Danke: 1740

W11 x64 (Chrome, Edge)
Delphi 11 Pro, Oxygene, C# (VS 2022), JS/HTML, Java (NB), PHP, Lazarus
BeitragVerfasst: 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 Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starofftopic star
Beiträge: 55
Erhaltene Danke: 5



BeitragVerfasst: Mi 15.07.15 16:43 
user profile iconSMO hat folgendes geschrieben Zum zitierten Posting springen:
user profile iconjaenicke hat folgendes geschrieben Zum zitierten Posting springen:
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:

ausblenden 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;
{  //delphi code ursprünglich
begin
  result.X := V1.X + V2.X + V3.X;
  result.Y := V1.Y + V2.Y + V3.Y;
  result.Z := V1.Z + V2.Z + V3.Z;
}

ASM
  // x und y
  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 // x und y tauschen
  movupd    dqword ptr[result.x], xmm0

  //z
  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:

ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
  v1 := MakeVector(1.3,  2.3,  3.3);
  v2 := MakeVector(10.320.330.3);
  v3 := MakeVector(40.350.360.3);
  v := Add3Vectors(v1,v2,v3) ;          // v1 = (1.2999994766, -2.3534384905e-185, 3.2999997164) ... äh... aha!
                                        // v = (7.5079945854e-306, 1.867629613e-307, 1.0532910603e-315)   ??????

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...

ausblenden 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
    {
    result.X := V1.X + V2.X + V3.X;
    result.Y := V1.Y + V2.Y + V3.Y;
    result.Z := V1.Z + V2.Z + V3.Z;
    }

    ASM
        // x und y
        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
        //z
        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 // x und y tauschen
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
ausblenden Delphi-Quelltext
1:
movsd     qword ptr[result.z], xmm0					
OlafSt
ontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic starofftopic star
Beiträge: 486
Erhaltene Danke: 99

Win7, Win81, Win10
Tokyo, VS2017
BeitragVerfasst: 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
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starofftopic star
Beiträge: 120
Erhaltene Danke: 18


D2005 Personal
BeitragVerfasst: Do 16.07.15 00:38 
user profile iconuser32 hat folgendes geschrieben Zum zitierten Posting springen:
Weißt du denn, ab welcher Delphi-Version es das gibt?

Leider nicht.

user profile iconuser32 hat folgendes geschrieben Zum zitierten Posting springen:

ausblenden 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.

ausblenden Delphi-Quelltext
1:
2:
3:
4:
// Aus ...
function Add3Vectors(const V1, V2, V3: TVector): TVector;
// ... wird also hinter den Kulissen 
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:
ausblenden 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;
{ //delphi code ursprünglich
  begin
  result.X := V1.X + V2.X + V3.X;
  result.Y := V1.Y + V2.Y + V3.Y;
  result.Z := V1.Z + V2.Z + V3.Z;
}

asm
  push      ebx
  mov       ebx, Result
  // x und y
  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 // x und y tauschen
  movupd    dqword ptr [TVector(ebx).x], xmm0

  // z
  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 "Add2Vectors" 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 Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starofftopic star
Beiträge: 55
Erhaltene Danke: 5



BeitragVerfasst: Do 16.07.15 03:18 
user profile iconSMO hat folgendes geschrieben Zum zitierten Posting springen:

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:
ausblenden Delphi-Quelltext
1:
...					


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 :)

user profile iconSMO hat folgendes geschrieben Zum zitierten Posting springen:

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. :)

user profile iconSMO hat folgendes geschrieben Zum zitierten Posting springen:

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
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starofftopic star
Beiträge: 120
Erhaltene Danke: 18


D2005 Personal
BeitragVerfasst: Do 16.07.15 13:09 
user profile iconuser32 hat folgendes geschrieben Zum zitierten Posting springen:
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.

user profile iconuser32 hat folgendes geschrieben Zum zitierten Posting springen:

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:

ausblenden volle Höhe 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:
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
  // Speicher für TVector-Records allozieren (16-byte-aligned von FastMM)
  New(v1);
  New(v2);
  New(v3);
  SetVector(100, v1^);
  SetVector(010, v2^);
  SetVector(001, v3^);
  v1^ := Add3VectorsAligned(v1^, v2^, v3^);
  // Speicher freigeben
  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.