Testprogramm zur prozessorspezifischen X86-Codeoptimierung

Für Fragen zur Programmiersprache auf welcher Lazarus aufbaut
ruewa
Beiträge: 153
Registriert: Sa 12. Apr 2014, 14:43

Testprogramm zur prozessorspezifischen X86-Codeoptimierung

Beitrag von ruewa »

Hallo!

Wie einige von Euch ja schon wissen, beschäftige ich mich seit einiger Zeit mit der Optimierung kurzer Funktionen, die als "Arbeitspferde" für die Bearbeitung großer (Short-) String-Datensätze dienen. Also vor allem mit den Compare-Methoden, die man zum Sortieren braucht, und den Pos-Funktionen, mit denen man Texte durchsuchen kann. Irgendwann habe ich angefangen, Entwürfe hierfür in Assembler zu schreiben und deren Laufzeitverhalten zu testen.

Dabei bin ich darauf gestoßen, daß es unheimlich schwierig ist, vorherzusagen, welche Lösungen richtig schnell funktionieren und welche nicht. Die Zeiten, als man den Zeitbedarf von Assemblercode anhand von Handbuchtabellen einfach addieren konnte, sind längst vorbei: Moderne Prozessoren versuchen, den weiteren Programmablauf vorherzusagen und im Hintergrund Vorarbeit zu leisten, indem sie z.B. später erst anstehende Anweisungen schon mal vorsorglich durchführen, um fertige Ergebnisse parat zu haben, wenn die dann gebraucht werden. Das Ganze ist ein Spiel mit Wahrscheinlichkeiten, denn solange die Vorhersagen, welchem Entscheidungspfad das Programm demnächst wohl folgen würde, zutreffen, funktioniert das Ganze wunderbar – sobald diese Predictions aber in die falsche Richtung gehen, war der ganze Hintergrundprozeß für die Katz und muß neu aufgebaut werden. Das kostet dann wiederum richtig viel der zuvor mühsam eingesparten Laufzeit.

Um nun optimalen, schnellen Code zu schreiben, muß man nun seinerseits vorhersehen, wie der Prozessor an dieser oder jener Stelle wohl „ticken“ würde. Das hat sich als außerordentlich schwierig herausgestellt, und sehr viel von dem, was man darüber im Internet nachlesen kann, hat sich dabei leider auch als durchaus fragwürdig erwiesen. Manchmal nützt es z.B., sinnlos scheinende Verzögerungen (sogenannte NOPs, No-Operation-Anweisungen) mitten in den Code einzubauen. Im Internet wird seit einiger Zeit das seltsame Phänomen diskutiert, daß sogar wahllos eingestreute NOP's Programme beschleunigen können – bislang konnte m.W. aber noch niemand eine schlüssige Begründung für diesen Effekt vorbringen.

Ich selbst bin in meiner kleinen Welt der Compare- und Pos-Funktionen von einer Überraschung in die nächste gefallen, einen Teil davon konntet Ihr in diesem Thread nachlesen, wo es um das sogenannte Alignment ging, der Anordnung von Prozessoranweisungen und Sprungstellen relativ zu 16-, 32- und 64-Byte-Blöcken innerhalb des Speichers bzw. Prozessor-Caches. Man könnte das alles als Kleinkrämerei abtun, aber die Erfahrung zeigt, daß es hier um Laufzeitunterschiede bis zu einer Größenordnung von 1:3 geht – für eine Compare-Funktion, die beim Sortieren von Datensätzen millionenmal durchlaufen wird, sind das Welten, die der Anwender sehr wohl zu spüren bekommt. Sich mit diesem Thema zu beschäftigen lohnt sich also durchaus!

Und es gab noch mehr Überraschungen – die genauen technischen Details werde ich, um Eure Geduld nicht überzustrapazieren, in einem getrennten Posting darlegen. Hier nur soviel: Ich habe daher beschlossen, sozusagen eine weitere Vergrößerungslinse an mein Mikroskop zu schrauben und eine denkbar einfache Funktion (die einen ShortString nach der Position eines Chars durchsucht - also das, was System.Pos in der Variante (Char; ShortString) macht) in einer Reihe von Varianten durchzutesten. Dabei ist das hier präsentierte Testprogramm entstanden.

Es hat sich dann gezeigt, daß keineswegs alles, was auf meinem AMD-64-Desktop gut funktioniert, auch auf meinem 32-bit Laptop (mit einem Intel Celeron) richtig rennt. Manches dann aber schon. Damit bin ich nun an einem Punkt angelangt, an dem ich Euch um Eure (möglichst zahlreiche!) Mithilfe bitten möchte!

Die Frage ist nämlich, ob es genügend Gemeinsamkeiten im Mikro-Laufzeitverhalten unterschiedlichster x86-Prozessoren gibt, aus denen sich Erkenntnisse in Bezug auf Code-Optimierung gewinnen lassen, oder ob die Unterschiede überwiegen. Was es also als nächstes braucht, ist ein Vergleich der Ergebnisse des hier vorgestellten Testprogramms mit möglichst vielen verschiedenen Prozessoren. Da aber einzelne Prozessorfamilien und -typen ebenfalls ständig weiterentwickelt werden (in verschiedene „Modelle“ unterteilt und unterhalb der Modellebene in das sogenannte „Stepping“), interessieren hier also nicht nur CPU-Typen, sondern auch deren Baureihen und Revisionen.

Nun zum Testprogramm AlignTest:

AlignTest_rev02.zip
(589.24 KiB) 89-mal heruntergeladen
(Edit, 19. 3. 2015: Revidierte Fassung mit MaxStrings-Begrenzung)

AlignTest_2015_03_17.jpeg


AlignTest vermißt und protokolliert die jeweilige Laufzeit einer sehr einfachen Pos(Char; Shortstring)-Funktion, deren Kern aus lediglich 6 Assembler-Zeilen besteht, in rund hundert winzigen Variationen. Als Datenbasis verwendet das Programm dabei das Lazarus-Verzeichnis (dieses muß ggfls. noch eingegeben werden) und lädt dann von dort eine gute Million Quelltextzeilen aus den Source-Dateien. Diese Strings werden dann jeweils nach 20 verschiedenen Buchstaben durchsucht. Wichtig ist dabei: Das Programm mißt den gesamten Durchlauf dieser >20.000.000 Aufrufe pro Funktion, es nimmt also keine (unsinnigen) Mikromessungen vor, die Streuung der Egebnisse ist dementsprechend gering (< ± 1% bei gleichmäßiger Belastung des Computers).

Der gesamte Test dauert ca. 12 Minuten (natürlich abhängig vom jeweiligen Rechner, wobei meine 12 Minuten auf eher betagten Kisten ermittelt wurden). Wem das zu lange dauert, der kann die Anzahl der Runs natürlich auch heruntersetzen – die habe ich auf 4 voreingestellt, um auch durchschnittliche Abweichungen, also gewissermaßen das „Betriebssystem-Rauschen“, sowie Ausreißer erfassen zu können. Gut wäre übrigens, dann eine Kaffeepause einzulegen und den Computer während des Tests unbelastet von anderen rechenintensiven Aktivitäten laufen zu lassen (wobei auch das Gegenteil dessen mal ganz interessant zu sehen ist, bloß daß das eben nicht Gegenstand des Tests ist).

Anschließend generiert das Programm ein Logfile namens "AlignTestResult(Prozessorname).txt", das im selben Verzeichnis wie die Quelltext-Dateien abgelegt wird (zwei meiner Ergebnisfiles sind im Zipfile enthalten).

Worum ich Euch nun bitten möchte, ist Folgendes:
  1. Die Programmdateien herunterzuladen und in einen Ordner zu entzippen.
  2. Das Programm (Datei AlignTest.lpr) in Lazarus zu laden, es (unverändert) zu kompilieren und zu starten. Voreingestellt ist das kompilieren ohne Debugger mit Optimization Level 2. Es spielt übrigens für die Messungen keine Rolle, ob man das Programm innerhalb der Lazarus-IDE laufen läßt oder außerhalb als Executable aufruft.
  3. Ggfls. muß jetzt noch das Lazarus-Verzeichnis eingegeben werden, falls die Vorgabe (/usr/share/lazarus/ für Linux bzw. c:\lazarus für Windows) bei Euch nicht zutrifft.
  4. Den Test durchlaufen zu lassen (das dauert wie gesagt etwa 12 Minuten).
  5. Und schließlich die Ergebnisdatei ("AlignTestResult(Prozessorname).txt") entweder hier zu posten oder mir per EMail zuzusenden (meine EMail-Adresse erscheint auf dem unteren Memofeld)..
Was könnte bei dieser Erhebung herauskommen? Naja, wenn man das vorher immer schon wüßte, bräuchte man es nicht zu machen. Der Worst Case wäre: Die Ergebnisse sind derart „gewürfelt“, daß sich daraus keinerlei Gemeinsamkeiten ableiten lassen. Der Versuch, Codeoptimierung auf unterster Assemblerebene zu betreiben, wäre dann eine Sackgasse, verlorene Liebesmüh. Aber auch das wäre dann ja eine Erkenntnis!

Im besten Fall dagegen könnten wir aber schon einiges darüber lernen, wie man zeitkritische Hotspot-Routinen (für die ist das hier vorrangig von Interesse) erheblich verbessern kann. Das könnte – im allerbesten Fall – vielleicht sogar für die FPC-Kernentwickler interessant sein.

Jedenfalls bin ich bei meinen Internetrecherchen zu diesem Thema nirgendwo auf einen solchen Versuch gestoßen, das Mikro-Laufzeitverhalten prozessorübergreifend empirisch zu untersuchen. Klar: Hier wird nur ein winziger Ausschnitt des Prozessorverhaltens betrachtet. Aber allein dieser Ausschnitt kann Laufzeitunterschiede von 1:3, z.B. beim Sortieren, bewirken. Und man wird sehen, ob es sich lohnt, ggfls. später einen umfangreicheren Test zu enwickeln (dann aber besser im Team).

Ob ich hier Neuland betrete oder bloß im nebligen Morast herumstapfe – ganz ehrlich: Ich weiß es nicht. Aber ich denke, einen Versuch ist es wert. Selbstverständlich werde ich Euch über die Ergebnisse auf dem Laufenden halten.

Jedenfalls: Je mehr Testergebnisse ich auswerten kann, desto wertvoller und damit umso eher von allgemeinem Interesse könnten etwaige Erkenntnisse daraus sein.

Gruß Rüdiger


P.S.: AlignTest wurde unter 32- und 64-bit Linux entwickelt und ausgiebig getestet, ich konnte es aber nicht unter Windows testen. Ich hoffe, es läuft auch dort anstandslos (möglicherweise mit einigen optischen Abstrichen). Wenn es doch unter Windows ernste Schwierigkeiten gibt, sagt bitte Bescheid, dann müßte man nochmal einen Zwischenstop machen.
Zuletzt geändert von ruewa am Do 19. Mär 2015, 17:36, insgesamt 2-mal geändert.

ruewa
Beiträge: 153
Registriert: Sa 12. Apr 2014, 14:43

Re: Testprogramm zur prozessorspezifischen X86-Codeoptimieru

Beitrag von ruewa »

Hier nun zu den technischen Details.

Erste Vorbemerkung
: Es geht hier nicht um einen Performance-Vergleich von Prozessoren untereinander. Sondern um die Frage, welche Codevariante auf dem jeweiligen Prozessor relativ schnell läuft und welche die jeweilige CPU hingegen ausbremst. Oder anders gesagt: Was ist der beste Code für diesen und jenen Prozessor?

Zweite Vorbemerkung
: Ich werde zunächst Codevarianten anhand der Ergebnisse des AMD64 Athlon und des Intel Celeron darstellen, dann zum Thema „Alignment“ übergehen. Um die Ergebnisse besser nachvollziehen zu können, habe ich die Ergebnislisten hier in eine übersichtlichere Form gebracht und farblich markiert:

Uebersicht AlignTest (Intel_Celeron).pdf
(40.07 KiB) 100-mal heruntergeladen

Uebersicht AlignTest (AMD64_Athlon).pdf
(40.46 KiB) 88-mal heruntergeladen



1) Codierung

Betrachtet wird folgende Funktion:

Code: Alles auswählen

function CharPos_OwnPascal_***Align(c : Char; const s : ShortString): SmallInt;
var
  LenS,
  CntS : Byte;
  PC   : PChar;
begin
  result := 0;
  LenS   := Length(S);
  CntS   := LenS;
  if CntS = 0 then exit;
  PC := @S[1];
  while PC^ <> c do
  begin
    dec(CntS);
    if CntS = 0 then exit;
    inc(PC);
  end;
  result := LenS - CntS + 1;
end;


Dies ist eine Abwandlung der originalen System.Pos(Char; ShortString)-Funktion, die ein wenig assemblerfreundlicher konstruiert ist (sie braucht, je nach den Umständen, auch nur zwischen 96 und 55 % der Laufzeit des RTL-Originals). Für die weitere Betrachtung ist eigentlich nur die innere (while-) Schleife interessant, alles davor wird nur einmal pro Aufruf durchlaufen, die Result-Zuweisung am Ende statistisch (im Test) sogar nur 0,3-mal pro Aufruf.

Warum ist dieses Schleifenkonstrukt „assemblerfreundlicher“ als das der originalen System.Pos? Nun, deren Schleife sieht so aus:

Code: Alles auswählen

  for i:=1 to length(s) do
  begin
    if pc^=c then
    begin
      result:=i;
      exit;
    end;
    inc(pc);
  end;
 

Der Unterschied ist, daß im ersten Fall der Schleifenzähler auf Null herunter dekrementiert wird, während im zweiten Fall ein Vergleich zwischen Ist- und Grenzgröße stattfinden muß. Die RTL-Funktion spart sich also die zusätzliche lokale Variable CntS und deren Wertzuweisung. Da der Prozessor aber beim Subtrahieren bzw. Dekrementieren immer auch das Zero-Flag setzt, kann der verlaufsentscheidende Zustand, ob Count gleich oder ungleich Null ist, anschließend sofort ausgewertet werden, während im zweiten Fall erst noch ein zusätzlicher Registervergleich stattfinden muß. (Möglicherweise wandelt der Compiler auf einer höheren Optimierungsstufe diese Logik entsprechend um, das habe ich bisher aber noch nicht überprüft).

Heruntergebrochen auf Assembler-Ebene sieht die Schleife (das Davor und Danach lasse ich weg) dann so aus: Variante 1: CharPos_Asm1_LoopStart_* (für i386):

Code: Alles auswählen

  .LLoop:   cmpb   (%esi), %dl      // 2 Byte Code 3a16                  --- Vergleiche (@S[n] in ESI)^ mit c in DL
            je     .LResult         // 2 Byte code 74xx                  --- Wenn gleich, springe zur Result-Zuweisung
            subw   $1, %ax          // 4 Byte code 662d0100              --- Decrementiere Schleifenzähler in AX
            jz     .LExit           // 2 Byte code 74xx                  --- Wenn der Null ist, dann Exit mit Result := 0
            incl   %esi             // 1 Byte code 46                    --- Incrementiere Stringzeiger auf @S[n+1]
            jmp    .LLoop           // 2 Byte code ebxx                  --- Und springe zurück zum Loopanfang

 
Die drei beteiligten CPU-Register sind in allen weiteren Beispielen gleich belegt: ESI (bzw. RSI in der 64-bit-Version) enthält den Zeiger auf die aktuelle Position im String S, DL enthält das Char c und in AX liegt der gegen Null laufende Schleifenzähler. (In AX deshalb, weil dort auch das Funktionsergebnis an die aufrufende Routine zurückgegeben wird, im Fall eines Mismatches (d.h. Abbruch bei Count = 0, was in 70 % aller Aufrufe passiert) braucht es also keine weitere Ergebniszuweisung mehr.)

Was zum Teufel gibt es da noch zu optimieren? Daß sich dort nichts mehr einsparen läßt, ist ja offenkundig! Und doch läuft das verdammte Ding kaum schneller (bzw. auf dem Celeron sogar anderthalb mal langsamer!) als die doch schon erwiesenermaßen verbesserungsfähige RTL-Funktion! Woran liegt das?

In der zweiten Variante (und allen folgenden) habe ich die Reihenfolge der Anweisungen umgestellt: Anstatt als Erstes den eigentlichen Char-Vergleich durchzuführen, wird hier erst der Schleifenzähler dekrementiert sowie der String-Zeiger erhöht und dann erst der Vergleich durchgeführt (Voraussetzung ist, daß der Pointer in RSI/ESI anfangs auf S[0] statt auf S[1] zeigt und der Zähler um eins erhöht ist):

Code: Alles auswählen

  .LLoop:   subw   $1, %ax
            jz     .LExit
            incl   %esi
            cmpb   (%esi), %dl
            je     .LResult
            jmp    .LLoop 


Das Ergebnis ist im Wesentlichen dasselbe wie bei Variante 1.

Bis hierher folgt die Codierung brav dem etablierten Lehrbuchwissen bzw. dem, was in unzähligen Variationen im Internet darüber zu lesen ist, z.B. hier: http://www.agner.org/optimize/optimizing_assembly.pdf

  1. „Make conditional jumps most often not taken: The efficiency and throughput for not-taken branches is better than for taken branches on most processors. Therefore, it is good to place the most frequent branch first.“
  2. Konditionale Jumps (Jcc) sind aufwendiger und unter Vorhersage-Gesichtspunkten unberechenbarer als unbedingte Sprunganweisungen (JMP).
  3. „The INC and DEC instructions do not modify the carry flag but they do modify the other arithmetic flags. Writing to only part of the flags register costs an extra μop on P4 and P4E. (…) Use ADD and SUB when optimizing for speed. Use INC and DEC when optimizing for size or when no penalty is expected“.
Aber wenn alles nicht so tut, wie die Experten empfehlen, muß man auch mal das Gegenteil ausprobieren. Die dritte Variante schießt daher die Lehrmeinung in den Wind, zieht den Rücksprung zum Loopanfang um eine Zeile vor und überläßt diesen (weitaus häufigeren) Fall einer bedingten - und zumeist befolgten - Sprunganweisung (Variante 3 a):

Code: Alles auswählen

  .LLoop:   subw   $1, %ax
            jz     .LExit
            incl   %esi
            cmpb   (%esi), %dl
            jne    .LLoop
            jmp    .LResult


Jetzt kommt die Überraschung: Diese Variante läuft auf dem AMD64 plötzlich ziemlich genau anderhalb mal so schnell wie die Variante 2! Beim Celeron hingegen ändert sich erstmal noch nichts.

Der wiederum kommt bei der nächsten kleinen Änderung auf Hochtouren: In Variante 3b habe ich die Subtraktion durch einen DEC-Befehl ersetzt:

Code: Alles auswählen

  .LLoop:   decw   %ax
            jz     .LExit
            incl   %esi
            cmpb   (%esi), %dl
            jne    .LLoop
            jmp    .LResult


Während diese Änderung den AMD64 überhaupt nicht beeindruckt, reagiert der Celeron darauf mit einer exorbitanten Beschleunigung um den Faktor 1,8! Wobei anzumerken ist, daß der Assembler diese Anweisung „decw %ax“ bei 64-bit-Systemen anders übersetzt (als 3-Byte-Code 66ffc8) als im 32-Bit-Modus (dort wird dieselbe Anweisung als 2-Byte 6648 codiert). Diese Variante 3b ist damit diejenige, die bei beiden Prozessoren die besten Ergebnisse liefert.


2) Alignment

Der Begriff „Alignment“ bezeichnet in unserem Zusammenhang die Anordnung der Prozessorbefehle innerhalb von 16-, 32- oder 64-Byte-Speicherblöcken. Die Frage ist dabei, ob ein Maschinenbefehl in hexadezimaler Schreibweise z.B. auf einer Null, einer 7 oder etwa auf C liegt. Das ist deshalb von Bedeutung, weil der Prozessor seine Befehlsfolgen in ebensolchen Blöcken aus dem Speicher in seinen Cache kopiert. Im Prozessor-internen Speicher wiederum haben diese Blöcke eine überragende Bedeutung, weil sich innerhalb dieser Blöcke z.B. die Ablaufvorhersage organisiert. Um das anhand der vorigen Variante 3b zu verdeutlichen:

Code: Alles auswählen

    .balign 16                        
  .LLoop:   decw   %ax                // 2 Byte code 6648    --- beginnt auf Adresse xyz0 h
            jz     .LExit             // 2 Byte code 74xx    --- liegt auf Adresse xyz2 h
            incl   %esi               // 1 Byte code 46      --- liegt auf Adresse xyz4 h
            cmpb   (%esi), %dl        // 2 Byte code 3a16    --- liegt auf Adresse xyz5 h
            jne    .LLoop             // 2 Byte code 75xx    --- liegt auf Adresse xyz7 h
            jmp    .LResult           // 2 Byte code ebxx    --- liegt auf Adresse xyz9 h
  .LResult: subw   %ax, %cx           //                     --- beginnt auf Adresse xyzB h 


Die Anweisung „.balign 16“ sorgt dafür, daß der Assembler genausoviele NOPs (No-Operation-Anweisungen, die nichts tun, außer Platz und minimal Rechnerzeit zu verbrauchen) einfügt, so daß die nachfolgende Anweisung auf einer Null, d.h. auf einem 16-Byte-Blockanfang zu liegen kommt. Mit dieser Anweisung ist also sichergestellt, daß der Loopstart immer an einer 16-Byte-Blockgrenze ausgerichtet ist. Dasselbe läßt sich auch im Pascal-Quellcode erzwingen: Der Free-Pascal-Compiler stellt hierfür die Compileranweisung „{$CODEALIGN LOOP=xx}“ zur Verfügung.

Nun paßt dieser gesamte Loop bequem in einen einzelnen 16-Byte-Block – das sollte doch für hohe Geschwindigkeiten optimal sein – oder etwa nicht? Könnte also das Alignment so eine Art „Zauberstab“ sein? Nächste Überraschung: Nein, das glatte Gegenteil scheint – bei manchen Prozessoren jedenfalls - der Fall zu sein!

Um die Auswirkungen des Alignments zu untersuchen, habe ich nun jede der oben beschriebenen Code-Varianten ver-18-facht: Vor dem Loopanfang wird zunächst per .balign 16 sichergestellt, daß dieser auf einer Null beginnt. Zwischen diesem .balign und dem tatsächlichen Loopstart habe ich dann weitere NOPs (nochmals Danke, Florian!) eingefügt, von 1 – 15 Byte Länge, also z.B. so:

Code: Alles auswählen

    .balign 16                        // 0 + 12 byte NOP (never reached)
    .byte   0x66, 0x66, 0x0F, 0x1F, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00          // 10 x NOP
    .byte   0x66, 0x90                                                          //  2 x NOP
  .LLoop:   decw   %ax 


Da der Loopbeginn von oben her angesprungen (statt angelaufen) wird, werden die NOPs, und zwar sowohl die von .balign automatisch eingefügten wie meine zusätzlichen, niemals selbst durchlaufen. Sie nehmen einfach nur Platz weg, kosten aber keine Rechnerzeit. Auf diese Weise habe ich nun jeweils 16 Funktionen, die sich nur darin unterscheiden, daß der Loopanfang auf allen denkbaren Positionen innerhalb des 16-Byte-Blocks zu liegen kommt. Zusätzlich habe ich noch zwei weitere Subvarianten mit .balign 32 und .balign 64 hinzugefügt, insgesamt also pro Code-Variante je 18 Alignment-Subvarianten.

Das Ergebnis ist hochinteressant – und verwirrend: Die beiden untersuchten Prozessoren reagieren weitgehend genau gegenläufig auf dieses Alignment. Der Celeron verhält sich so, wie man es erwarten würde: Er mag es nicht, wenn der Loop auf zwei Blöcke aufgeteilt wird, Punkt. Aber allzu groß ist der Unterschied dann doch nicht (5-6 %). Frappierend ist hingegen das Verhalten des AMD64: Der reagiert mit einer deutlichen Leistungssteigerung (um 15 %), sobald der unkonditionale Sprung der untersten Zeile in den nächsten Block abgeschoben wird. Und das, obwohl diese Sprunganweisung in weniger als 3 % aller Loopdurchläufe überhaupt erreicht wird! Was bedeutet, daß nicht die eigentliche Ausführung dieses Sprungs Zeit kostet, sondern daß vielmehr die Vorhersage-Struktur des CPU-Cache-Blocks durch die Anwesenheit dieser Sprungmöglichkeit empfindlich gestört wird!

Die weiteren Varianten sind schnell erklärt: In Variante 3c liegen die NOPs statt am Loopstart innerhalb des Loops selbst, werden also tatsächlich durchlaufen. Auch hier reagiert der AMD64 empfindlicher als der Celeron (und unterm Strich sogar etwas besser), aber die Effekte sind, wie nicht anders erwartet, gering.

Was die Variante 4a angeht: Die habe ich mit hineingenommen, nachdem ich über eine weitere Merkwürdigkeit im Verhalten des AMD64 gestolpert bin. Diese Variante enthält keinerlei Operationen und auch keinen Loop - sie macht nichts anderes als einen sofortigen „Rücksturz zur Erde“:

Code: Alles auswählen

asm
            jmp    .LExit
    .balign 16                     
    .byte   0x0F, 0x1F, 0x44, 0x00, 0x00                        //  5 x NOP
  .LExit:
end;


Der Compiler setzt an die Stelle des „end;“ dann ein „RET“. Nun zeigt sich, daß die Speicherposition dieser Return-Anweisung erstaunliche Oszillationen im Verhältnis von knapp 1:3 bewirkt:

Code: Alles auswählen

Function CharPos_Asm4a_NoLoop_ExitStart_0     112.9 ms   =     5 ns / Call
Function CharPos_Asm4a_NoLoop_ExitStart_1     112.9 ms   =     5 ns / Call
Function CharPos_Asm4a_NoLoop_ExitStart_2     301.3 ms   =    14 ns / Call
Function CharPos_Asm4a_NoLoop_ExitStart_3     112.9 ms   =     5 ns / Call
Function CharPos_Asm4a_NoLoop_ExitStart_4     301.5 ms   =    14 ns / Call
Function CharPos_Asm4a_NoLoop_ExitStart_5     113.3 ms   =     5 ns / Call
Function CharPos_Asm4a_NoLoop_ExitStart_6     301.4 ms   =    14 ns / Call
Function CharPos_Asm4a_NoLoop_ExitStart_7     112.9 ms   =     5 ns / Call
Function CharPos_Asm4a_NoLoop_ExitStart_8     301.4 ms   =    14 ns / Call
Function CharPos_Asm4a_NoLoop_ExitStart_9     112.9 ms   =     5 ns / Call
Function CharPos_Asm4a_NoLoop_ExitStart_A     301.4 ms   =    14 ns / Call
Function CharPos_Asm4a_NoLoop_ExitStart_B     112.9 ms   =     5 ns / Call
Function CharPos_Asm4a_NoLoop_ExitStart_C     301.5 ms   =    14 ns / Call
Function CharPos_Asm4a_NoLoop_ExitStart_D     112.9 ms   =     5 ns / Call
Function CharPos_Asm4a_NoLoop_ExitStart_E     327.3 ms   =    15 ns / Call
Function CharPos_Asm4a_NoLoop_ExitStart_F     112.9 ms   =     5 ns / Call
 
Function CharPos_Asm4a_NoLoop_ExitStart_32    112.9 ms   =     5 ns / Call
 
Function CharPos_Asm4a_NoLoop_ExitStart_64    360.8 ms   =    16 ns / Call


Zusätzlich erstaunt, daß der AMD64 hier offenbar ungerade Speicherstellen (sowie die Null) bevorzugt. Der Celeron wiederum zeigt bei diesem Test keinerlei Auffälligkeit. Und sobald man eine weitere Anweisung hinzufügt (Variante 4b):

Code: Alles auswählen

asm
            pushq  %rsi
            jmp    .LExit
    .balign 16                     
    .byte   0x0F, 0x1F, 0x44, 0x00, 0x00                                        //  5 x NOP
  .LExit:   popq   %rsi
end;


… ist der Spuk auch beim AMD64 vorüber.


3) Fazit

Ich bin offen gestanden weit davon entfernt, diese Ergebnisse interpretieren oder gar verstehen zu können. Nur zeigen die Messungen, daß die Effekte in einer Größenordnung liegen, die alles andere als marginal ist. Es könnte sich also zumindest lohnen, der Sache tiefer auf den Grund zu gehen.

Gibt es genügend Gemeinsamkeiten hinsichtlich des Micro-Laufzeitverhaltens heute gebräuchlicher Prozessoren oder sind deren Unterschiede zu groß? Ist mein spezielles AMD64-Modell vielleicht ein besonders untypischer Exot, bin ich womöglich einer Fata Morgana hinterhergerannt? Und ist das für die Cracks alles längst nur noch kalter Kaffee? Oder lassen sich doch ein paar verallgemeinerbare Erkenntnisse aus solchen Querschnitt-Tests gewinnen? Würde es womöglich Sinn machen, die Option Hersteller- oder gar CPU-Familien-spezifischer Optimierungsalgorithmen ins Auge zu fassen?

Mir ist klar, daß mein Testprogramm bestenfalls an der Kruste des Problems zu kratzen vermag. Aber vielleicht legen die weiteren Ergebnisse nahe, daß es durchaus Sinn machen würde, das Thema noch gründlicher und systematischer anzugehen – und dann vielleicht auch mit mehr Sachverstand, als ich ihn (alleine) aufzubringen vermag.

Danke für Eure Geduld!

Gruß Rüdiger
Zuletzt geändert von ruewa am Do 12. Mär 2015, 08:21, insgesamt 1-mal geändert.

Socke
Lazarusforum e. V.
Beiträge: 3158
Registriert: Di 22. Jul 2008, 19:27
OS, Lazarus, FPC: Lazarus: SVN; FPC: svn; Win 10/Linux/Raspbian/openSUSE
CPU-Target: 32bit x86 armhf
Wohnort: Köln
Kontaktdaten:

Re: Testprogramm zur prozessorspezifischen X86-Codeoptimieru

Beitrag von Socke »

Ich lade meine Ergebnisse einfach hier hoch - für den Fall, dass es hilft.

Wenn ich das richtig beobachtet habe, hat das Programm bei mir nur einen Kern verwendet. Daher wünsche ich dir schon einmal im Voraus viel Erfolg bei der Aufklärung des Einflusses von Multithreaded-Anwendungen auf die Messungen sowie des Power-Managements des Prozessors. In der Theorie könnte ein moderner Mehrkernprozessor nach der Hälfte der Tests nicht genutzte Kerne stilllegen und den aktiven Kern mit zusätzlicher Leistung ausstatten.

ruewa hat geschrieben:Erste Vorbemerkung[/i]: Es geht hier nicht um einen Performance-Vergleich von Prozessoren untereinander. Sondern um die Frage, welche Codevariante auf dem jeweiligen Prozessor relativ schnell läuft und welche die jeweilige CPU hingegen ausbremst. Oder anders gesagt: Was ist der beste Code für diesen und jenen Prozessor?

Anmerkung: Mein Prozessor läuft z.Z. mit 3,3 GHz und ist damit eher mit einem AMD Phenom II X6 1100T vergleichbar.
Dateianhänge
AlignTestResult_(AMD_Phenom(tm)_II_X6_1055T_Processor).txt
(89.56 KiB) 83-mal heruntergeladen
MfG Socke
Ein Gedicht braucht keinen Reim//Ich pack’ hier trotzdem einen rein

Michl
Beiträge: 2505
Registriert: Di 19. Jun 2012, 12:54

Re: Testprogramm zur prozessorspezifischen X86-Codeoptimieru

Beitrag von Michl »

Wenn es was hilft?! Hab zur Zeit nur 2 Notebooks bei mir. Tests anbei.
Dateianhänge
AlignTestResult_(AMD_E-450_APU_with_Radeon(tm)_HD_Graphics).txt
(89.29 KiB) 84-mal heruntergeladen
AlignTestResult_(Intel(R)_Core(TM)_i5_CPU_______M_430___227GHz).txt
(89.28 KiB) 91-mal heruntergeladen

Code: Alles auswählen

type
  TLiveSelection = (lsMoney, lsChilds, lsTime);
  TLive = Array[0..1] of TLiveSelection; 

mschnell
Beiträge: 3444
Registriert: Mo 11. Sep 2006, 10:24
OS, Lazarus, FPC: svn (Window32, Linux x64, Linux ARM (QNAP) (cross+nativ)
CPU-Target: X32 / X64 / ARMv5
Wohnort: Krefeld

Re: Testprogramm zur prozessorspezifischen X86-Codeoptimieru

Beitrag von mschnell »

Socke hat geschrieben:Wenn ich das richtig beobachtet habe, hat das Programm bei mir nur einen Kern verwendet. Daher wünsche ich dir schon einmal im Voraus viel Erfolg bei der Aufklärung des Einflusses von Multithreaded-Anwendungen auf die Messungen sowie des Power-Managements des Prozessors.


Es ist sicher sinnvoll, zunächst einmal das Verhalten eines Prozessors zu untersuchen.

Bei mehreren Prozessoren wird die Sache natürlich noch viel schwerer zu durchschauen. Die Prozessoren können ja oft nicht wirklich parallel-laufen, weil sich nicht alle Daten im jeweils eigenen Cache befinden und die Synchronisation der Zugriffe auf den 2nd Level Cache dann ein Nadelöhr werden kann.

Bei mehrerer Hardware-Threads eines Prozessors wird es noch komplizierter und Chip_Abhängiger, weil sich beide Threads ja in die diversen Ausführungs-Einheiten des Prozessors und den 1st Level Cache teilen müssen.

-Michael
Zuletzt geändert von mschnell am Do 12. Mär 2015, 09:50, insgesamt 1-mal geändert.

mschnell
Beiträge: 3444
Registriert: Mo 11. Sep 2006, 10:24
OS, Lazarus, FPC: svn (Window32, Linux x64, Linux ARM (QNAP) (cross+nativ)
CPU-Target: X32 / X64 / ARMv5
Wohnort: Krefeld

Re: Testprogramm zur prozessorspezifischen X86-Codeoptimieru

Beitrag von mschnell »

ruewa hat geschrieben:Erste Vorbemerkung: Es geht hier nicht um einen Performance-Vergleich von Prozessoren untereinander. Sondern um die Frage, welche Codevariante auf dem jeweiligen Prozessor relativ schnell läuft und welche die jeweilige CPU hingegen ausbremst. Oder anders gesagt: Was ist der beste Code für diesen und jenen Prozessor?

Dann sollte die Ausgabe des Programms nur Angaben über die Verhältnisse zwischen Loops, die dasselbe tun enthalten und kein absoluten Performance-Werte.

Ich verstehe auch nicht, warum das Test-Programm Dateien läd und ähnliche komplexe Sache tut, wo es doch erstmal nur um die Unterschiede geht, die zwischen den Arbeitsweisen einzelnen Chips bestehen. Da wäre meiner Ansicht nach ein sehr viel simpleres Programm, das nur eine Reihe von normierten Unterprogrammen mit jeweils x-facher Ausführung einer zu untertersochenden Loop aufruft, mit sehr viel simplerem Output sinnvoll.

Wenn es darum geht, die Eigenschaften des Prozessors zu testen, verstehe ich auch nicht, warum Du nicht einfach eine EXE und/oder ELF (am besten Kommando-Zeilen-Programm ohne GUI) verbreitest.

-Michael

Benutzeravatar
af0815
Lazarusforum e. V.
Beiträge: 6219
Registriert: So 7. Jan 2007, 10:20
OS, Lazarus, FPC: FPC fixes Lazarus fixes per fpcupdeluxe (win,linux,raspi)
CPU-Target: 32Bit (64Bit)
Wohnort: Burgenland
Kontaktdaten:

Re: Testprogramm zur prozessorspezifischen X86-Codeoptimieru

Beitrag von af0815 »

Ergebnis von mir. Windows 7 / 32 auf einem älteren Shuttle XPC Glamor
Dateianhänge
AlignTestResult_(Intel(R)_Core(TM)2_Quad_CPU____Q6600___240GHz).txt
Aligntest
(89.28 KiB) 103-mal heruntergeladen
Blöd kann man ruhig sein, nur zu Helfen muss man sich wissen (oder nachsehen in LazInfos/LazSnippets).

Horst_h
Beiträge: 72
Registriert: Mi 20. Mär 2013, 08:57

Re: Testprogramm zur prozessorspezifischen X86-Codeoptimieru

Beitrag von Horst_h »

Hallo,

ich verstehe die Vorgehensweise der Generierung der Testdaten auch nicht.Ich habe mal einen String erzeugt und einen gemixten String mit ein paar nicht vorkommenden Zeichen.Alle Daten liegen im Level I-Cache, ich will ja nicht die Speicherzugriffszeiten darin haben.
Ich habe mal CharPos_OwnPascal_NoAlign geändert.
Man muss es der CPU schon einfacher machen, deshalb NativeInt und die verwendung von pChar mit der man dann leicht indizieren kann, ohne den Umweg einer Umrechnung smallint in NativeInt.
CodeAlign proc= 2 oder 16 war das schnellste , proc = 8 das langsamste.
FPC 2.6.5 linux 32 Haswell i3-4330 3.5 Ghz
Statt maximal 30% Gewinn gegenüber Pos nun fast 40%.

Code: Alles auswählen

{$IFDEF FPC}
  {$MODE DELPHI}
  {$OPTIMIZATION ON,REGVAR,PEEPHOLE,CSE,ASMCSE}
  {$CODEALIGN proc=2}
{$ENDIF}
uses
  sysutils;
const
  RUNDEN = 285714
type
  tmyF = function (c : Char; const s : ShortString):smallint;
var
  s,sMix : string[255];
procedure Test(myF:tmyF);
var
  i,j,p: NativeInt;
Begin 
 For j := 1 to RUNDEN do
   For i := 1 to Length(sMix) do
     p := myF(sMix[i],s);
end
 
procedure OrgPos;
var
  i,j: NativeInt;
Begin 
 For j := 1 to RUNDEN do
   For i := 1 to Length(sMix) do
    Pos(sMix[i],s);
end;
 
procedure Mix;
var
  i,j: NativeInt;
  c : char; 
begin
  For i := length(sMix) downto 2 do
  Begin
    j := random(i)+1;
    c := sMix[j];sMix[j]:= sMix[i];sMix[i]:= c;
  end;
end;   
 
function CharPos_OwnPascal_ProcLoopAlign(c : Char; const s : ShortString): SmallInt;
var              // Exact copy of CharPos_OwnPascal_NoAlign, but with aligned PROC & LOOP
  LenS,
  CntS : Byte;
  PS   : PChar;
begin
  result := 0;
  LenS   := Length(S);
  CntS   := LenS;
  if CntS = 0 then exit;
  PS := @S[1];
  while PS^ <> c do
  begin
    dec(CntS);
    if CntS = 0 then exit;
    inc(PS);
  end;
  result := LenS - CntS + 1;
end;
 
function CharPos_OwnPascal_NoAlign(c : Char; const s : ShortString):smallint;
var 
  cntS,     
  LenS : NativeInt;
  PS   : PChar;
begin
  result := 0;
  LenS   := Length(S);
  if LenS = 0 then
    exit;
  CntS := 1;
  PS := @S[0];
  while PS[Cnts] <> c do
  begin
    Inc(CntS);
    if CntS > LenS then
      exit;
  end;
  result := cntS;
end;
 
var
  T0,T1: TDateTime;
  i: nativeInt;
begin
  randomize;
  s := '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
  sMix := s+'#!"§$%&/';
  For i := 0 to 9 do
  Begin
    t0:= time;
    OrgPos;
    t1:= time; 
    write(FormatDateTime('NN:SS.ZZZ   ',T1-T0));
    t0:= time;
    Test(@CharPos_OwnPascal_NoAlign);
    t1:= time; 
    write(FormatDateTime('NN:SS.ZZZ   ',T1-T0));
    t0:= time;
    Test(@CharPos_OwnPascal_ProcLoopAlign);
    t1:= time; 
    writeln(FormatDateTime('NN:SS.ZZZ',T1-T0));
    Mix;   
  end
end.
{Output:
 
00:00.744   00:00.461   00:00.571
00:00.754   00:00.464   00:00.580
00:00.755   00:00.460   00:00.586
00:00.754   00:00.451   00:00.572
00:00.770   00:00.459   00:00.580
00:00.742   00:00.440   00:00.590
00:00.753   00:00.461   00:00.581
00:00.757   00:00.457   00:00.572
00:00.757   00:00.458   00:00.592
00:00.746   00:00.448   00:00.569
 
real   0m17.884s}
 


Gruß Horst

Scotty
Beiträge: 768
Registriert: Mo 4. Mai 2009, 13:24
OS, Lazarus, FPC: Arch Linux, Lazarus 1.3 r44426M FPC 2.6.4
CPU-Target: x86_64-linux-qt/gtk2
Kontaktdaten:

Re: Testprogramm zur prozessorspezifischen X86-Codeoptimieru

Beitrag von Scotty »

Lazarus 1.5 r47911M FPC 2.6.4 x86_64-linux-qt
Dateianhänge
AlignTestResult_(Intel(R)_Core(TM)_i7_CPU_________920___267GHz).txt
(83.16 KiB) 90-mal heruntergeladen

martin_frb
Beiträge: 573
Registriert: Mi 25. Mär 2009, 21:12
OS, Lazarus, FPC: Laz trunk / fpc latest release / Win and other
CPU-Target: mostly 32 bit

Re: Testprogramm zur prozessorspezifischen X86-Codeoptimieru

Beitrag von martin_frb »

Hab mich mal beteiligt.

Allerdings werden die Zeiten und die damit auch die prozentualen unterscheide von etlichen anderen Faktoren mit beeinflusst.

z.b. Memory size. Wenn das OS die app schwappt dann sind die Zeiten ruiniert. Ebenso wenn das background tasks startet (wenn man nix an Tastatur und Mause tut, dann nimmt sich Windows Freiheiten.

Auch im Aufbau des Programm. Afaik geht es um das durchsuchen sehr großer Daten. Diese passen nicht in den Cache, und RAM Zugriffs Zeiten fließen ein. So kann die selbe CPU, mit unterschiedlichem RAM zu unterschiedlichen Ergebnissen kommen.
Interessant wären also zusätzlich Zeiten fuer mittlere und kleine Datensaetze.

Und selbst dann sind die Ergebnisse bestenfalls Indikativ. Wer weiß was noch alles mit eine Rolle spielt.

Code: Alles auswählen

Operating System : Win32
Architecture     : i386
CPUID Support    : True
CPU Vendor       : GenuineIntel
CPU Name         : Intel(R) Core(TM)2 CPU          6600  @ 2.40GHz
CPUID            : 6F6 h
CPU Family       : 6
CPU Model        : 15
CPU Stepping     : 6
Compiled with Optimization Level 2 and Debug-Info OFF
 


Code: Alles auswählen

Directory:                                   c:\lazarus
Filename Mask:                               *.pas;*.pp;*.inc;*.lpr;*.dpr
Files loaded:                                      9,535
Strings loaded:                                2,119,277
Number of search chars:                               20      (A;a;B;b;C;c;D;d;E;e;_;\;7;$;%;^;|; ;Q;T)
Number of calls per function:                 42,385,540
Number of matches per function:               12,778,570   =  30.15 %
Number of mismatches per function:            29,606,950   =  69.85 %
Number of Char comparisons per function:   1,621,056,931
Cumulated result per function:               223,162,230      (used for verification)
Average char comparisons per call:                 38.25
Average result per matched call:                   17.46
 


fpc 2.6.4
Dateianhänge
res.txt
Intel(R) Core(TM)2 CPU 6600 @ 2.40GHz
(66.87 KiB) 90-mal heruntergeladen

martin_frb
Beiträge: 573
Registriert: Mi 25. Mär 2009, 21:12
OS, Lazarus, FPC: Laz trunk / fpc latest release / Win and other
CPU-Target: mostly 32 bit

Re: Testprogramm zur prozessorspezifischen X86-Codeoptimieru

Beitrag von martin_frb »

Noch was was ich gefunden habe.

IntVal div 2
CardinalVal div 2

Cardinal produziert weniger Code, und ist schneller. Weiß man also das der wert positiv ist, lohnt sich der typecast.
Grund ist das fur negative Werte nach dem bitwise shift noch eine Korrektur (Rundung) nötig ist.

ruewa
Beiträge: 153
Registriert: Sa 12. Apr 2014, 14:43

Re: Testprogramm zur prozessorspezifischen X86-Codeoptimieru

Beitrag von ruewa »

Hallo!

Erstmal vielen Dank für die zahlreichen Ergebnisse, die Ihr hier eingestellt bzw. mir per Email zugesandt habt! Da ich seither unterwegs war, konnte ich sie bisher nur flüchtig durchsehen, aber mein erster Eindruck ist, daß die Sache sehr spannend wird! Das Spektrum scheint breiter zu sein, als ich erwartet hatte.

Es wird mit der Auswertung noch etwas dauern, da ich zum einen natürlich noch auf viele weitere Ergebnislisten hoffe, und ich zum anderen für die Auswertung ein separates Programm schreibe, das noch einige Arbeit erfordert. Das heißt andererseits aber auch, daß es kein Kapazitätsproblem bei der Auswertung geben wird, also schüttet mich bitte mit Ergebnis-Files zu!

Womit ich nicht gerechnet hatte, ist, daß bei manchen der eingelesene Datensatz sehr viel größer ist: Teilweise > 10.000 Dateien, wo es bei mir nur knapp 4.000 sind. Ich sehe, ich habe schon viel zu lange der Windows-Welt Adieu gesagt: Liegen unter Windows eigene Dateien und die Bibliothek in einem gemeinsamen Lazarus-Verzeichnis? Unter diesen Umständen ist meine 12-Minuten-Angabe natürlich allzu optimistisch. Daher die Information für die Windows-User: Unter Linux zeigt der voreingestellte Pfad ausschließlich auf die Lazarus-Bibliothek. In dem Fall könnt Ihr also ruhig tiefer in den Verzeichnisbaum hineingehen, ich denke, grob eine Million Strings ist völlig ausreichend für valide Ergebnisse (das zeigen auch die Streuungen). Darauf bezieht sich auch die Pi*Daumen-Angabe von 12 Minuten. Die Anzahl der eingelesenen Strings könnt Ihr übrigens im mittleren Memofeld ablesen, bevor Ihr den Test startet.

Nun zu den Einwänden:

@Michael: Klar! Ich verstehe auch nicht, warum mein Nachbar Tomaten anpflanzt, wo doch ein Apfelbäumchen viel sinnvoller wäre! Jedenfalls hätte ich das so gemacht. Hab ich aber nicht... Nein, im Ernst: Ich wollte für den Test lebensnahe Ausgangsdaten haben. Und da die Routine zum Einlesen der Strings aus dem Lazarus-Verzeichnis (das ja bei jedem, der hier mitliest, mit Sicherheit vorhanden ist) in eine Hashlist (und dort in ein Zeiger-Array mit konstantem Zugriffsverhalten) seit meinen Tests zur Entwicklung von TSortableHashList fertig herumliegt, war es auch der einfachste Weg, das kurzerhand zu recyclen. Natürlich kann man alles Mögliche anders machen, aber kommt es darauf an? Wo sollte denn da der Nachteil sein?

Ich habe hier ganz bewußt keine Executables eingestellt, weil ich selbst äußerst zögerlich bin, solche auf meinem Rechner unbesehen laufen zu lassen. Jeder hier hat ohne Weiteres die Möglichkeit, AlignTest über die Lazarus-IDE aufzurufen und der ganze Zusatzaufwand, das Programm statt als Executable als Quelltextprojekt in die IDE zu laden und dort zu starten, besteht aus genau zwei Mausklicks. Lohnt das irgendeine Prinzipdiskussion?

@Martin: Mit Deinem Ergebnisfile kann ich leider nichts anfangen, weil Du den Teil, der für das maschinelle Einlesen (u.a. der nicht-stellengerundeten Zahlen) vorgesehen ist, kurzerhand weggeschnitten hast und ich keine Extraroutine für die Reparatur zerschnipselter Files schreiben werde. Ich kann das auch nicht wirklich nachvollziehen, denn daß da keine hinterhältigen Geheiminformationen ausgelesen werden, ist eigentlich schon bei einem flüchtigen Blick auf den Quelltext (WriteLogFile und FuncTest) ersichtlich. Deswegen legt man's ja offen.

Was die RAM-Zugriffe anbelangt – nun, die gehören zum Leben dazu! Wichtig ist doch, daß für jeden einzelnen Funktionsaufruf auf diesem einen Computer die gleichen Bedingungen herrschen. Soweit man das halt überhaupt beeinflussen kann, natürlich! Theoretisch denkbar ist viel, aber bisher kann ich bei den vorliegenden Daten keine Artefakte erkennen. Funktionsaufrufe, RAM-Zugriffe und Ergebnisverarbeitung betrachte ich daher näherungsweise als konstante Größe (die ja auch mit CharPos_AsmDummy_NoLoop erfaßt wird), und solange die Streuungen keine Auffälligkeiten zeigen, also gewissermaßen der Signal-Rauschabstand hinreichend groß ist, läßt sich das, meine ich, auch als gegeben annehmen.

Dein zweites Posting verstehe ich leider nicht. „div 2“, „bitwise shift“ - wo siehst Du das in den Testroutinen?

@Horst: Es hat ein wenig gedauert, bis bei mir der Groschen fiel: Deine CharPos_OwnPascal_NoAlign ist nicht meine CharPos_OwnPascal_NoAlign, und Deine CharPos_OwnPascal_ProcLoopAlign ist zwar eine Quelltextkopie meiner gleichnamigen Funktion, hat aber mit „ProcLoopAlign“ nichts mehr zu tun... Vielleicht wäre eine eigenständige Namensgebung in solchen Fällen übersichtlicher. Egal: Ich habe Deine CharPos_OwnPascal_NoAlign mal in CharPos_HorstH_Pascal_*** umbenannt und in dreifacher Ausfertigung (als ***_NoAlign, ***_ProcAlign und ***_ProcLoopAlign) in AlignTest eingebaut. Folgendes kommt auf meinem AMD64 dabei heraus:

Code: Alles auswählen

Function SysPosByCall                        Verified   2.462 s  (100.0 %)  =   111 ns / Call
 
Function SysPosClone_NoAlign                 Verified   1.651 s  ( 67.1 %)  =    75 ns / Call
Function SysPosClone_ProcAlign               Verified   1.652 s  ( 67.1 %)  =    75 ns / Call
Function SysPosClone_ProcLoopAlign           Verified   1.651 s  ( 67.1 %)  =    75 ns / Call
 
Function OwnPascal_NoAlign                   Verified   2.066 s  ( 83.9 %)  =    93 ns / Call
Function OwnPascal_ProcAlign                 Verified   1.429 s  ( 58.0 %)  =    64 ns / Call
Function OwnPascal_ProcLoopAlign             Verified   1.429 s  ( 58.0 %)  =    64 ns / Call
 
Function HorstH_Pascal_NoAlign               Verified   1.395 s  ( 56.7 %)  =    63 ns / Call
Function HorstH_Pascal_ProcAlign             Verified   1.714 s  ( 69.6 %)  =    77 ns / Call
Function HorstH_Pascal_ProcLoopAlign         Verified   1.401 s  ( 56.9 %)  =    63 ns / Call


Von 30 vs. 40% ist bei mir also nichts zu erkennen. Wobei das durchaus sein kann, daß es sich auf Deinem Rechner so verhält. Aber da liegt auch ein Mißverständnis vor: Ich hatte meine „OwnPascal“-Funktion als Entwurf für die Assembler-Routinen geschrieben und gar nicht weiter versucht, die optimale Pascal-Variante zu finden. Daß genau die von Rechner zu Rechner völlig unterschiedlich ausfällt, war ja gerade die Erfahrung, aus der heraus die Assembler-Versuche und letztlich dieser Test überhaupt entstanden sind. Man kommt auf Pascal -Ebene nicht an den Kern des Problems heran. Das zeigt schon die Tatsache, daß die Bibliotheksroutine System.Pos (also by call) auf manchen Rechnern erheblich langsamer läuft als deren (frisch kompilierte) Kopien, auf anderen dagegen deutlich schneller. Solange man nicht tiefer hinabsteigt, ist das Ganze wie verhext. Was Du nach mühevoller Kleinarbeit auf einem Rechner glaubst, herausgefunden zu haben, überlebt auf dem zweiten Computer nur selten auch nur den ersten Versuch... Selbst auf allerunterster Assembler-Ebene ist es noch keineswegs ausgemacht, ob es gelingt, übergreifend gültige Regeln zu erkennen – genau darum geht es ja hier!

Ich bin inzwischen auch sehr vorsichtig geworden mit flotten Deutungen. Bei der Festlegung auf SmallInt als Rückgabeformat habe ich keine großen theoretischen Überlegungen angestellt. Das ist einfach aus empirischen Beobachtungen heraus entstanden: Über hunderte von Vergleichstests gemittelt scheint das einen Tick schneller zu sein als mit anderen Formaten. Innerhalb der Funktion verwendet der Compiler in dem Fall 16-bit-Registerbefehle anstelle von 32-bit breiten. Auf meinen Rechnern ist da kein Unterschied bei den Laufzeiten beobachtbar (was nicht heißt, daß es sie nicht doch woanders geben kann). Lediglich beim Rücksprung muß dann eventuell eine Typkonversion stattfinden. Das hat aber mit „Umrechnen“ nichts zu tun: Die Zuweisung von Registerwerten unterschiedlicher Breite (von klein auf groß) gehört zum Standardrepertoire des Prozessors, ggfls. kommt statt eines MOV-Befehls eben ein MOVZX- oder MOVSX-Befehl zum Einsatz, der den Rest des Zielregisters mit Nullen bzw. Vorzeichen auffüllt. Selbst wenn das zusätzliche Zeit kösten würde (wovon ich nicht überzeugt bin), wäre davon lediglich ein Promille-Anteil des gesamten Arbeitsvolumens der Funktion betroffen, das ja weitgehend innerhalb des Loops liegt. Da führen solche Vermutungen sehr schnell in die Irre.


Um das nochmal klar zu sagen: Keine Versuchsanordnung wird perfekt sein. Dieser Test wird keine unverrückbaren Wahrheiten ergeben, er ist vielmehr dazu da, in einem Fachgebiet, das bei Licht besehen keiner von uns wirklich durchdringt, Indizien zu sammeln, und Hinweise darauf, wo man etwas verbessern könnte. Daß ich hier an den Grenzen meines technischen Verständnisses operiere, ist mir völlig bewußt. Damit bin ich ganz sicher auch nicht alleine. Das ist aber auch der Grund, warum ich einen empirischen Ansatz gewählt habe und mit Deutungen sehr zurückhaltend bin.

Selbstverständlich ist es legitim, fachliche Kritik am Versuchaufbau und den zugrundeliegenden Annahmen zu äußern. Daß Einwände sich als berechtigt erweisen werden, davon gehe ich sogar aus. Dieser Kritik stelle ich mich auch gerne, das ist nicht das Problem. Mir fällt aber schon auf, daß die einen ganz pragmatisch Testergebnisse beisteuern, die anderen Bedenken – und daß es dort keine Schnittmenge zu geben scheint, ist schon irgendwie seltsam.

Insofern würde ich mir wünschen, daß die Sache ausnahmsweise mal nicht in reflexhaften Wisenheimer-Debatten zerschreddert wird – sondern als Chance aufgenommen würde, gemeinsam etwas dazuzulernen.

In der Hoffnung auf weiterhin rege Beteiligung

Gruß Rüdiger
Zuletzt geändert von ruewa am Fr 13. Mär 2015, 23:58, insgesamt 1-mal geändert.

martin_frb
Beiträge: 573
Registriert: Mi 25. Mär 2009, 21:12
OS, Lazarus, FPC: Laz trunk / fpc latest release / Win and other
CPU-Target: mostly 32 bit

Re: Testprogramm zur prozessorspezifischen X86-Codeoptimieru

Beitrag von martin_frb »

@Martin: Mit Deinem Ergebnisfile kann ich leider nichts anfangen, weil Du den Teil, der für das maschinelle Einlesen (u.a. der nicht-stellengerundeten Zahlen) vorgesehen ist, kurzerhand weggeschnitten hast und ich keine Extraroutine für die Reparatur zerschnipselter Files schreiben werde. Ich kann das auch nicht wirklich nachvollziehen, denn daß da keine hinterhältigen Geheiminformationen ausgelesen werden, ist eigentlich schon bei einem flüchtigen Blick auf den Quelltext (WriteLogFile und FuncTest) ersichtlich. Deswegen legt man's ja offen.


Ich hatte den text aus dem memo kopiert und gespeichert.

Ich hatte nach dem Testlauf (40 Minuten) keine log Datei gesehen? Wie erzeuge ich die korrekte Datei? Ich sehe auch keinen save button.
Gerade nochmal geschaut, da sind 3 (DREI) Dateien AlignTestResult_*, und eine hat meine CPU Bezeichnung. Ich kann aber nicht sagen, ob die vorher schon existierte oder nicht. Immerhin sind da auch 2, die nix mit meiner CPU zu tun haben.

Was die RAM-Zugriffe anbelangt – nun, die gehören zum Leben dazu! Wichtig ist doch,


Wenn ich jetzt meinen PC oeffne, und das RAM tausche (schnelleres oder langsameres), dann ändern sich alle Zeiten. Und möglicherweise auch die prozentualen Unterschiede.
Die ermittelten Werte sind also verfälscht, und sagen nix ueber die CPU alleine aus.

Daher mein Vorschlag die test 2 mal (oder oefter) laufen zu lassen:
- Kleine Daten (Komplett im cache // ggf Cache Level 1 versus 2): Ergebnis nur fuer die CPU
- Grosse Daten: mit Ram Verfälschung

Die Großen Daten sind mehr real live, und zeigen was Du eigentlich sehen willst. Sie haben aber eine Größere Varianz. Die kleinen Daten erlauben den besseren Vergleich wenn es nur um den Code geht.

Außerdem sind auch in real live Daten nicht immer groß.

Dein zweites Posting verstehe ich leider nicht. „div 2“, „bitwise shift“ - wo siehst Du das in den Testroutinen?

Hat nix mit diesem Programm zu tun. War im größerem Kontext "Optimierungen"
"div 2" oder "div 4/8/16" kommen regelmäßig zum Einsatz, und werden von fpc in ein bitwise shift übersetzt. Sind also deutlich schneller als echte Division.

ruewa
Beiträge: 153
Registriert: Sa 12. Apr 2014, 14:43

Re: Testprogramm zur prozessorspezifischen X86-Codeoptimieru

Beitrag von ruewa »

martin_frb hat geschrieben:Ich hatte nach dem Testlauf (40 Minuten) keine log Datei gesehen? Wie erzeuge ich die korrekte Datei? Ich sehe auch keinen save button.
Gerade nochmal geschaut, da sind 3 (DREI) Dateien AlignTestResult_*, und eine hat meine CPU Bezeichnung. Ich kann aber nicht sagen, ob die vorher schon existierte oder nicht. Immerhin sind da auch 2, die nix mit meiner CPU zu tun haben.


Ah, okay! Ja, das Logfile wird automatisch im selben Verzeichnis erstellt, sobald der Test beendet (und nicht abgebrochen) wurde. Eine entsprechende Meldung mit Pfad und Dateiname wird dann oben in der Statuszeile angezeigt. Die beiden anderen Files sind "AMD_Athlon(tm)_64_X2_Dual_Core_Processor_5000+" und "Intel(R)_Celeron(R)_CPU__________900___220GHz", das sind meine Ergebnislisten, die ich zur Illustration beigefügt habe. Stimmt, ich hätte die besser in ein separates Unterverzeichnis packen sollen.

Wenn ich jetzt meinen PC oeffne, und das RAM tausche (schnelleres oder langsameres), dann ändern sich alle Zeiten. Und möglicherweise auch die prozentualen Unterschiede. Die ermittelten Werte sind also verfälscht, und sagen nix ueber die CPU alleine aus.


Naja, das Argument hängt an dem Wort "möglicherweise". Ich kann keinen Grund erkennen, warum sich die relativen Verhältnisse dadurch ändern sollten. Der Testlauf sieht folgendermaßen aus: Während eines einzelnen Testlaufs (was dann als Zeile im unteren Memo erscheint) wird auf das PShortString-Array in der Hashliste, auf den String mit den 20 Chars und auf die Shortstrings selbst zugegriffen. Das alles ist im RAM, nichts davon muß von der Festplatte gelesen werden. Immer in derselben Reihenfolge. Jeder Run findet dieselben Bedingungen vor, und jede Funktion ebenfalls. Würde sich währenddessen der Cache gravierend umorganisieren, müßte man das an den Streuungen der Runs sehen, die sitzen nämlich in der äußersten Loopschale der jeweiligen Funktionsmessung. Diese Streuungen sind aber erfahrungsgemäß viel kleiner als die gemessenen Zeitunterschiede zwischen den einzelnen Funktionen.

Ich sehe dabei eher das Problem, daß stark schwankende Nebenprozesse das Rauschen bis zur Wertlosigkeit der Messungen verstärken können. Also wenn man gleichzeitig noch Videos codiert, macht es keinen Sinn, den Test durchlaufen zu lassen. Aber auch das müßte man dann an den Streuungen erkennen können.

Gruß Rüdiger

ruewa
Beiträge: 153
Registriert: Sa 12. Apr 2014, 14:43

Re: Testprogramm zur prozessorspezifischen X86-Codeoptimieru

Beitrag von ruewa »

Mal noch die Frage an die Windows-Nutzer: Wo liegen denn in Eurem Verzeichnisbaum üblicherweise die Lazarus-Bibliotheksdateien? Also C:\Lazarus\...? So, daß man auf 3.800-4.000 Files mit 0,9-1,1 Mio. Strings kommt?

Ich würde den Default-Pfad dann ändern, es muß ja nicht sein, daß die Kisten gleich eine Dreiviertelstunde lang auf die Reise geschickt werden.

Gruß Rüdiger

Antworten