ASM-Code optimierbar und kann ich Dll-Funktionen aus dem ASM-Code aufrufen?

  • Ich saß gestern den gesamten Abend dran endlich meine Berechnung in ASM zu machen statt purem AutoIt und es hat sich rausgestellt,

    dass meine Implementation gerade mal einen Vorteil von x1.28 bringt.

    Natürlich bin ich kein ASM-Profi und der Code ist sicherlich noch weiter optimierbar, also tobt euch mal bitte aus und hilft mir, den Code zu optimieren! :)

    Ich habe keine Funktion zum Berechnen der dritten Wurzel gefunden, also habe ich mir eine Approximation geschrieben.

    Idee der Approximation

    Approximation von einem Dezimalbruch N

    durch Addition von 1/(2^x), x in Natürlichen Zahlen

    Im x86-Befehlssatz habe ich zum Potenzieren keine wirklichen Funktionen gefunden bis auf F2XM1.

    F2XM1 berechnet (2^x)-1 und da ich x^(1/3) rechnen musste bietet sich da die Umrechnungsformel x^y = 2^(y * log2(x)) an,

    allerdings darf beim F2XM1 Befehl der Exponent x nur zwischen -1.0 und +1.0 liegen.

    Damit konnte ich leider nicht den gesamten Wertebereich von 0.0 bis 1.0 hoch 1/3 abdecken -> 2^(1/3 * log2(0.0 bis 1.0)) da bei einem x von 0.125 (wird zu -1) und 1.0 (wird zu 0) abdecken.

    Das reichte mir leider immer noch nicht, da ich gerne eine Abdeckung von 0.0 (wird zu -unendlich) bis 1.0 wollte.

    Dann kam mir die Idee, einfach den Exponenten mittels der Wurzelfunktion zu approximieren.

    Es gibt im x86-Befehlssatz die Funktion FSQRT und diese berechnet die Quadratwurzel von ST(0) (oberster Wert auf dem FPU-Stack).

    Die Potenzgesetze erlauben es einem den Exponent zu addieren, wenn man Potenzen gleicher Basen multiplziert.

    Beispiel: 2^(1/2) * 2^(1/4) = 2^(1/2 + 1/4) = 2^(3/4)

    Jetzt kann man sich immer näher an den gewünschten Wert mittels Quadratwurzeln rantasten:

    Beispiel: 1/6 = 0.166666666

    Approximationsanfang = 0

    1/2^0 = 1/1 = 1 1 + Approximation <= 1/6 ? Nein!

    1/2^1 = 1/2 = 0.5 0.5 + Approximation <= 1/6 ? Nein!

    1/2^2 = 1/4 = 0.25 0.25 + Approximation <= 1/6 ? Nein!

    1/2^3 = 1/8 = 0.125 0.125 + Approximation <= 1/6 ? Ja! 0.125 + 0 = 0.0125 => Neue Approximation = 0.125

    1/2^4 = 1/16 = 0.0625 0.0625 + Approximation <= 1/6 ? Nein!

    1/2^5 = 1/32 = 0.03125 0.03125 + Approximation <= 1/6 ? Ja! 0.03125 + 0.125 = 0.15625 => Neue Approximation = 0.15625

    1/2^6 = 1/64 = 0.015625 0.015625 + Approximation <= 1/6 ? Nein!

    1/2^7 = 1/128 = 0.0078125 0.0078125 + Approximation <= 1/6 ? Ja! 0.15625 + 0.0078125 = 0.1640625 => Neue Approximation = 0.1640625

    Wenn wir nun 0.1640625 als Approximation nutzen wollen müssen wir lediglich folgende Rechnung durchführen:

    x^(1/8) * x^(1/32) * x^(1/128)

    Als ASM-Code ungefähr so zu implementieren:

    x -> ST0 | ST0 = x

    SQRT(ST0) -> ST0 | ST0 = x^(1/2)

    SQRT(ST0) -> ST0 | ST0 = (x^(1/2))^(1/2) = x^((1/2) * (1/2)) = x^(1/4)

    SQRT(ST0) -> ST0 | ST0 = x^(1/8)

    Wir haben nun einen Wert den wir für die Approximation brauchen also kopieren wir ST0 nach ST1

    FST(ST1) | ST0 = ST0, ST1 = ST0

    SQRT(ST0) -> ST0 | ST0 = x^(1/16)

    SQRT(ST0) -> ST0 | ST0 = x^(1/32)

    Neuer benötigter Wert!

    FST(ST2) | ST0 = x^(1/32), ST1 = x^(1/8)

    SQRT(ST0) -> ST0 | ST0 = x^(1/64)

    SQRT(ST0) -> ST0 | ST0 = x^(1/128)

    Neuer benötigter Wert steht in ST0.

    Wir haben die Approximatino nun ausgerechnet und müssen nur noch die Ergebnisse multiplizieren:

    FMULP | ST0 = x^(1/128) * x^(1/32), ST1 = x^(1/8) FMULP popt nämlich das Register nach der Multiplikation

    FMULP | ST0 = x^(1/128) * x^(1/32) * x^(1/8)

    Fertig gerechnet, das Ergebnis steht in ST0!

    Man kann das ganze natürlich in ASM so programmieren, dann man beliebig nah an die Approximation kommt (wie das AutoIt-Äquivalent unten)

    aber in meinem Code haben vier Faktoren als Genauigkeit ausgereicht also habe ich es hardgecodet und nicht dymanisch.

    Momentan übergebe ich fast alle Daten mittels DllStructs an den ASM-Code und lese diese auch so wieder aus (DllStructGetData).

    Gibt es da eine schnellere Möglichkeit als jeden Index einzeln aus dem Struct zu lesen?

    Die "Zyklen pro Sekunde"-Messung schwankt sehr stark je nach Bildschirmauflösung und Visualizergröße.

    Das Script liest aus dem fftBuffer aus der aktuellen .mp3-Datei und berechnet einen Visualizer welcher mittels GDI+ angezeigt wird.

    Das Script zieht bei mir rund 15-20% CPU-Last (Intel Core i7 4790) und berechnet 260 Zyklen die Sekunde.

    Am Ende soll ein Sleep in die Schleife eingebaut werden, damit nur noch 40-50 Zyklen pro Sekunde berechnet werden (sieht immer noch flüssig aus) und die CPU-Last dann auf <1% sinkt.

    Das klappt auch bei mir schon fast mit einem Sleep(15) (42-44 Zyklen): CPU-Last: ~2% (mit anderen Prozessen im Hintergrund die nichts berechnen).

    Dann wäre noch die Frage:

    Ist es möglich die _GDIPlus_PathAddLine2 aus dem ASM-Code aufzurufen? Dann könnte ich nämlich die gesamte Berechnung der Punkte auf den ASM-Code auslagern und hätte dann nochmal einen Geschwindigkeitsboost! Wäre nett, wenn mir jemand zeigen könnte (wenn es möglich ist) wie es geht, die Implementierung würde ich gerne selber ausprobieren!!

    Angehangen ist AssembleIt2_64, Approximation.au3, BASS.au3, BASSconstants.au3, bass.dll, visualizer.au3

    Tobt euch aus!

  • Hi,

    für den Kollegen minx hatte ich vor einigen Jahren eine Optimierung in Assembler geschrieben, da war u.a. auch eine pow(a,b)-Funktion dabei, kannst du ja mal ausprobieren!

    Dazu muss man sagen, dass AutoIt intern die Bibliotheken von Microsoft verwendet bzw. von dessen Compiler.

    Dieser rechnet NICHT mit den von der FPU bereitgestellten 80 Bit Genauigkeit, sondern "nur" mit einer 53 Bit Mantisse, also 64 Bit Genauigkeit!!!

    Daher, um "gleiche" Ergebnisse von AutoIt und dem ASM-Code zu erhalten, sollte man dieses Verhalten anpassen.

    Geht wie hier beschrieben:

    Beispiel für POW incl. setzen auf 53Bit Mantisse

    Jetzt hab ich mir mal dein Script angeschaut, dein ASM-Code läuft bei mir 0,02Millisekunden, da ist nicht mehr viel zu optimieren, jedenfalls nicht merkbar^^

    Der darauffolgende loop allerdings hat es in sich, der läuft 5 Millisekunden, und liest doch eigentlich nur eine Struct mit Werten aus.

    Schau dir mal Func _GDIPlus_PathAddLine2($hPath, $aPoints) an!

    Du musst dort in den dllcall doch nur den pointer auf die vom ASM-Code gefüllte Struct übergeben, dann hast du das ganze doppelt berechnete und hin- und herstructausgelese nicht mehr!

    Das wären dann ca. 300-400 Zyklen

    Ist es möglich die _GDIPlus_PathAddLine2 aus dem ASM-Code aufzurufen?

    Na klar^^

    Du musst dir nur den Pointer auf die Funktion holen per

    Code
    #include <WinAPI.au3>
    
    
    $hKERNEL32 = DllOpen("kernel32.dll")
    $hGDIPLUS = dllopen("gdiplus.dll")
     $handle= _WinAPI_GetModuleHandle("gdiplus.dll")
     ConsoleWrite('@@ Debug(' & @ScriptLineNumber & ') : $handle = ' & $handle & @CRLF & '>Error code: ' & @error & @CRLF) ;### Debug Console
    $pointer= DllCall($hKERNEL32, "ptr", "GetProcAddress", "ptr", $handle, "str", "GdipAddPathLine2")
    _arraydisplay($pointer)
    ConsoleWrite('@@ Debug(' & @ScriptLineNumber & ') : $pointer[0] = ' & $pointer[0] & @CRLF & '>Error code: ' & @error & @CRLF) ;### Debug Console


    diesen Pointer an den ASM-code übergeben und dort CALLen.

    Die Parameter werden auf den Stack gepushed

    Zurück bekommst du in EAX den Pointer bzw. das Ergebnis

    //EDIT4 Beispiel

    Der CALL bereinigt selbstständig den Stack!

    ciao
    Andy


    "Schlechtes Benehmen halten die Leute doch nur deswegen für eine Art Vorrecht, weil keiner ihnen aufs Maul haut." Klaus Kinski
    "Hint: Write comments after each line. So you can (better) see what your program does and what it not does. And we can see what you're thinking what your program does and we can point to the missunderstandings." A-Jay

    Wie man Fragen richtig stellt... Tutorial: Wie man Script-Fehler findet und beseitigt...X-Y-Problem

    7 Mal editiert, zuletzt von Andy (4. Februar 2018 um 20:13)

  • Ich hab mir mal aus der GDIPLus.au3 angeguckt in welchem Format das ganze sein muss und habe gesehen, dass dort einfach nur ein DllStruct mit floats erstellt wird.
    Das wollte ich jetzt auch machen und habe das ganze mehr oder weniger übersetzt und versuche es in ASM zu callen.

    Den Pointer zur Funktion, das Handle des Pfads, den Pointer zur Struct und die Anzahl der Punkte übergebe ich aber es crasht leider:

    Die Funktion wird auf jeden Fall ausgeführt, da der Stack aufgeräumt wird aber das Skript crasht anschließend.

    EAX wird auch auf 0 gesetzt. Das ist der GdiStatus 0 = OK.

    Wenn ich das ganze so wie du erstmal in die Register schiebe und dann neu auf den Stack pushe dann klappt das ganze (2. Script) aber wenn ich alles schon vorher richtig auf dem Stack platziere und dann nur den Call ausführe klappt das komischerweise nicht. Wieso?

  • die Idee ist soweit nicht schlecht....ABER:

    Der Stack ist nur nach oben, und IMMER nur nach oben konsistent! Beim Aufrufen einer jedwegen Funktion steht die Rücksprungadresse immer auf [esp+0]. Wenn du die jetzt nach "unten" schiebst, passiert folgendes:

    Wenn der Call bzw. die aufgerufene Funktion sich über SUB esp,1024 Platz (1024 Bytes) auf dem Stack für bspv. Variablen reserviert, dann ist dieser Platz (will sagen der Inhalt dort) nach dem Funktionsaufruf natürlich auch noch dort!

    Die Funktion "schiebt" lediglich am Ende den Stackpointer wieder in die richtige Position per ADD esp,1024.

    Capice?

    Du "schiebst" die Rücksprungaddresse mit deinem POP vom Stack "nach unten" ins Datengrab, denn irgendwann wird IMMER irgendetwas auf dem Stack reserviert^^

    Also musst du die Rücksprungaddresse sichern, könntest du irgendwie machen, sicherer ist immer die Methode SICHER!

    //EDIT

    Denn du weißt auch nicht, welche Inhalte auf dem Stack "über" deinen Funktionsparametern noch gebraucht werden, denn da stehen natürlich auch die Parameter, die die aufrufenden Funktionen dort abgelegt haben...

    ciao
    Andy


    "Schlechtes Benehmen halten die Leute doch nur deswegen für eine Art Vorrecht, weil keiner ihnen aufs Maul haut." Klaus Kinski
    "Hint: Write comments after each line. So you can (better) see what your program does and what it not does. And we can see what you're thinking what your program does and we can point to the missunderstandings." A-Jay

    Wie man Fragen richtig stellt... Tutorial: Wie man Script-Fehler findet und beseitigt...X-Y-Problem

    Einmal editiert, zuletzt von Andy (4. Februar 2018 um 21:23)

  • Beim Aufrufen einer jedwegen Funktion steht die Rücksprungadresse immer auf [esp+0]

    Ich dachte es mir fast schon! Danke für die Bestätigung! :)

    Wenn ich nach unten hin schiebe werden diese dann auch anschließend überschrieben, kein Wunder, dass die Rücksprungsadresse dann nicht mehr stimmt.

    Ich werde mich mal jetzt dranmachen die Berechnung der Punkte und das Hinzufügen mittels _GDIPlus_PathAddLine2 in ASM zu packen und berichte dann wieder.

  • Coole Sache das!

    Hau rein!

  • Das wären dann ca. 300-400 Zyklen

    300-400 Zyklen? Nix da! Ich kriege 800 Zyklen pro Sekunde jetzt raus!! :D ASSEMBLER ROCKT!!!

    Der ASM-Code berechnet die Amplituden und die Koordinaten der Frequenzen und fügt diese mit GdipAddPathLine2 hinzu.

    In der AutoIt-Hauptschleife wird nur noch der Gdi+Buffer gecleared, der fftBuffer gelesen, der ASM-Code gecalled und anschließend der Pfad und der Double-Buffer gezeichnet.

    Kein unnötiges Hin und Her mit DllStructs sondern alles zeitkritische in ASM!

    Das tolle obendrein ist ja, dass ich 100 Zwischenfrequenzen (_SubdivideFrequencies) statt 10 nehmen kann

    und die Performance bricht bei weitem nicht so heftig zusammen wie vorher.

    Man kann sicherlich noch bisschen was rauskitzeln aber das ist schon mal ne Wucht! Kein Vergleich zu reinem AutoIt!

  • Man kann sicherlich noch bisschen was rauskitzeln aber das ist schon mal ne Wucht!

    Wenn ich den DllCall zur Berechnung komplett auskommentiere, dann ändern sich die Zyklen pro Sekunde NICHT! Logisch, da die Berechnung nur in einer Handvoll Prozessortakten abläuft.

    Per SSE könntest du trotzdem 4 Punkte gleichzeitig berechnen. Macht hier aufgrund der sowieso kaum eingesparten Zeit wenig Sinn, lohnt sich aber bei aufwendigen Berechnungen immens!

    Btw. habe ich auf meinem 27'' Monitor (2560x1440) die Auflösung in Windows auf 125% gestellt, und somit ca. 440 Zyklen/s. Und das mit 273 Bytes Code....:thumbup:

  • Ich sollte noch dazu sagen, dass ich ursprünglich x0.7 der Höhe genommen hatte (weil ich noch ID3-Tags anzeigen wollte) und das Testen ohne x0.7 gemacht habe.

    Wenn wir das noch in Betracht ziehen sind wir bei knapp 1000 Zyklen pro Sekunde!

    Falls hier jemand das gerne mal ausprobieren möchte (AutoIt-Berechnung mit ASM-Code zu substituieren) soll er das auf jeden Fall machen.

    Wenn man ständig auf seinen x86-Befehlssatzspicker schaut, ist das gar nicht mal so schwierig. Zumindest habe ich kaum mehr Fehler gehabt als normal mit AutoIt zu scripten.

    Und mit ein paar Bytes Code kann man seinem Script einen unfassbaren Boost verpassen!

  • Daher, um "gleiche" Ergebnisse von AutoIt und dem ASM-Code zu erhalten, sollte man dieses Verhalten anpassen.

    Geht wie hier beschrieben

    Falls sich jemand für das gesamte Werk hinter Andy 's Link interessieren sollte, dann kann sie/er es sich hier herunterladen (wahlweise in der Variante HTML , PDF oder CHM) :

    Download HTML Edition

    Download PDF Edition

    Download CHM Edition

    (Das Hochladen der Dateien in das Forum ist, lt. Nutzungsbedingungen des Autor's, leider nicht zulässig)

    Der von Andy angesprochene Bereich findet sich z.B. in der PDF-Edition unter RealArithmetic.pdf .

    Gruß Musashi

    86598-musashi-c64-png

    "Am Anfang wurde das Universum erschaffen. Das machte viele Leute sehr wütend und wurde allenthalben als Schritt in die falsche Richtung angesehen."

  • Falls hier jemand das gerne mal ausprobieren möchte (AutoIt-Berechnung mit ASM-Code zu substituieren) soll er das auf jeden Fall machen.

    Ja, definitiv!

    Es ist immer und ewig das gleiche Spiel! Die "langsame" Schleife suchen und diese optimieren bzw. beschleunigen.

    Meistens ist es kein Hexenwerk, und wie hier im Thread gezeigt fehlt meist auch nur ein Fingerzeig oder Tip.

    Niemals würde ich komplette Programme incl. GUI in ASM erstellen, das ist imho auch obsolet und dafür gibt es die "Hochsprachen", aber das ist auch gar nicht die Intension. Mit einer Handvoll Bytes an der richtigen Stelle die (ggf. zeitkritische) Geschwindigkeit des Programms x-fach beschleunigen, DAS ist die Intension!

    Und wie gesagt, man kann sich ja mal anschauen, was bspw. ein (schneller) Compiler aus der Schleife machen würde.

    Langsamer kann´s also schon mal nicht werden:D

    Wenn man ständig auf seinen x86-Befehlssatzspicker schaut, ist das gar nicht mal so schwierig. Zumindest habe ich kaum mehr Fehler gehabt als normal mit AutoIt zu scripten.

    Ich hatte irgendwo mal geschrieben, dass man für 90% der ASM-Programme mit zwei Handvoll Befehlen auskommt, und das ist auch so!

    Ich habe auch nur einen SEHR groben "Überblick", meistens könnte man mit speziellen Befehlen noch den einen oder anderen Takt einsparen, aber entweder weiß ich nicht ob es da gerade jetzt einen anderen bzw. besser geeigneten Befehl gibt, oder mir ist es schlichtweg egal! Bei SSE/SIMD habe ich schon einen ziemlich guten Draht, da lege ich mich gern mit JEDEM Compiler an. Compiler, NICHT Programmierer, denn wenn dieser weiß was er da tut, zwingt er sowieso dem Compiler sein Compilat auf ;)

    ciao
    Andy


    "Schlechtes Benehmen halten die Leute doch nur deswegen für eine Art Vorrecht, weil keiner ihnen aufs Maul haut." Klaus Kinski
    "Hint: Write comments after each line. So you can (better) see what your program does and what it not does. And we can see what you're thinking what your program does and we can point to the missunderstandings." A-Jay

    Wie man Fragen richtig stellt... Tutorial: Wie man Script-Fehler findet und beseitigt...X-Y-Problem

    Einmal editiert, zuletzt von Andy (5. Februar 2018 um 16:57)

  • Oha, jetzt bin ich angefixt...

    Ich habe da auch so einen Fall. Soll ich einen neuen Thread starten? Geht um Koordinatenberechnung (Punktwolken). Vier verschachtelte Loops (X, Y, Z, Skalar) 400 MB TXT (nur die Skalare, Koordinaten werden per Start/Step/End berechnet) in paar MB XYZ/CSV. Habs in AutoIt schick als GUI fertig... Da auf der Arbeit entwickelt, darf ich es nicht komplett offenlegen... Wie könnte das laufen?

  • In der Regel ist der verwendete Algorithmus 100x wichtiger als das Werkzeug mit welchem dieser umgesetzt wird.

    Bevor du also daran gehst und versuchst dein Problem einfach dadurch zu lösen Assembler statt irgendetwas anderes (vermeintlich "langsameres") zu verwenden, würde es dir wohl erst einmal mehr bringen wenn du deine Problemstellung genauer beschreibst (eventuell mit ein paar Testdaten), so dass sich hier Gedanken gemacht werden kann mit welchem Ansatz am besten herangegangen werden kann.

    ... puh das war aber ein langer Satz ...

  • ... puh das war aber ein langer Satz ...

    ...und trifft 100%ig den Punkt!:thumbup:

    @Zec, ja , günstigstenfalls machst du einen eigenen Thread dafür auf. Dann können bzw. werden sich auch diejenigen damit beschäftigen, denen sich hier beim Lesen der 3 Buchstaben "ASM" die Haare zu Berge stellen=O

  • :D Alles klar, danke, ich mache morgen einen Extrakt mit Demodaten fertig. Die Umwandlung ist im Prinzip super simpel. Sind nur leider ~60 Mio. Punkte, die generiert und mit einem Skalar belegt werden. Vielleicht ist das ja auch was für OpenCL, würde ich ich auch gern mal ausprobieren. Gn8 und bis morgen.