die meisten Kompiler unterstützen ja performantere Versionen von z.B.
Mathemathik-Routinen wenn eine aktuellere CPU-Verbaut ist
jetzt frage ich mich nur die diese Code-Ersetzung zur Laufzeit
funktioniert
1
sin(doublex)
2
{
3
if(sse2)
4
sin_sse2
5
if(sse3)
6
sin_sse3
7
...
8
}
wird es wohl kaum sein weil das zusätzliche Branching ja auch einiges
kostet
und sin(und anderen Funktionen) als Funktionszeiger zu definieren der
beim Programmstart dann auf SSE2, SSE3 Varianten verbogen wird (durch
die Runtime vor main) ist doch auch immer wieder eine Pointer mehr zu
dereferenzieren
oder ist der Verlust dadurch einfach zu gering?
man könnte auch Runtime-Patchen - also die Sprung-Adresse zur Laufzeit
auf die richtigen Routinen mappen aber ich glaube nicht das so was
gemacht wird
Erleuchtet mich
Die verschiedenen Varianten werden wohl nicht nur verschiedene einzelne
Sinusse aufrufen. Die Operanden sind ja unterschiedlich breit, was sich
auf den Code drumrum auswirkt. Also wird man Funktionen auf einen ganzen
Satz Daten in jeder Variante implementieren. Dann spielt die Laufzeit
indirekter Aufrufe keine Rolle mehr. Wobei man das auch ganz gut in C++
verstecken kann.
If-Stapel sind schon der Quellcode-Organisation wegen ungünstig.
Sinnvollerweise packt man die Implementierung mit SSE2 in ein Modul, die
mit SSE3 in ein anderes, ... und nicht alles wild durcheinander in ein
einziges.
Prozessoren geben sich viel Mühe, das Ziel von Sprungbefehlen gut
vorherzusagen, ohne den Befehlsfluss aufzuhalten. Das ist hier auch
nicht schwierig, weil das Ziel sich nicht ändert. Der Einfluss auf die
Laufzeit ist dementsprechend gering.
(prx) A. K. schrieb:> Die verschiedenen Varianten werden wohl nicht nur verschiedene einzelne> Sinusse aufrufen. Die Operanden sind ja unterschiedlich breit, was sich> auf den Code drumrum auswirkt. Also wird man Funktionen auf einen ganzen> Satz Daten in jeder Variante implementieren. Dann spielt die Laufzeit> indirekter Aufrufe keine Rolle mehr.
redest du von den speziellen Vektor-Varianten der sin/cos/math etc.
Funktionen die mal 4 oder 8 floats/doubles parallel behandeln?
es ging mir um den zur-Laufzeit Austausch von Funktionen mit immer
gleicher Signatur - vom Kompiler forciert - nicht durch mich
implementiert
> Wobei man das auch ganz gut in C++> verstecken kann.
keine Ahnung was du damit sagen willst
> If-Stapel sind schon der Quellcode-Organisation wegen ungünstig.> Sinnvollerweise packt man die Implementierung mit SSE2 in ein Modul, die> mit SSE3 in ein anderes, ... und nicht alles wild durcheinander in ein> einziges.
wie der Kompiler/Math.Lib-Hersteller in diesem Zusammenhang seine
Implementation aufteilt ist für mich doch gar nicht relevant, oder?
> Prozessoren geben sich viel Mühe, das Ziel von Sprungbefehlen gut> vorherzusagen, ohne den Befehlsfluss aufzuhalten. Das ist hier auch> nicht schwierig, weil das Ziel sich nicht ändert. Der Einfluss auf die> Laufzeit ist dementsprechend gering.
bei ffmpeg sind das 5-10 Extensions(SSE2,AVX,...) die Supported werden
d.h. das wären 5-10 Laufzeit-Ifs - da der Code wohl zu doch groß ist für
den Cache würde ich schon denke das es ordentlich spürbar ist
cppbert3 schrieb:> redest du von den speziellen Vektor-Varianten der sin/cos/math etc.> Funktionen die mal 4 oder 8 floats/doubles parallel behandeln?
Naja, die Unterscheidung SSE2/SSE3 macht man nicht pro Sinus, sondern
eher pro Algorithmus. Also "decodiere einen Frame", "mache eine FFT auf
diesem Datenblock" oder so. Und da tut ein Funktionspointer nicht mehr
weh.
>> Wobei man das auch ganz gut in C++>> verstecken kann.>> keine Ahnung was du damit sagen willst
1
classMyFancyAlgo{...};/* abstract */
2
classMyFancyAlgoSse2:publicMyFancyAlgo{...};
3
classMyFancyAlgoSse3:publicMyFancyAlgo{...};
4
5
// muss von der implementation nix wissen
6
voidprocessData(MyFancyAlgo*algo,void*data);
7
8
intmain(){
9
// nimm beste implementation
10
MyFancyAlgo*algo;
11
if(cpuKnowsSse3())algo=newMyFancyAlgoSse3();
12
elseif(cpuKnowsSse2())algo=newMyFancyAlgoSse2();
13
elseexit(1);
14
15
// und lass sie arbeiten
16
void*data=getNewData();
17
processData(algo,data);
18
}
Das geht so auch in C, aber wenn man will, kann man die einzelnen
Implementationen auch innerhalb der jeweiligen Klassen implementieren
und im Konstruktur der Klasse auswählen lassen. Das ist besonders dann
sinnvoll, wenn die Implementationen sich viel Code teilen können (wenn
z.B. nur manche Schritte in SSE3 implementiert sind, der Rest aber in
SSE2).
>> Prozessoren geben sich viel Mühe, das Ziel von Sprungbefehlen gut>> vorherzusagen, ohne den Befehlsfluss aufzuhalten. Das ist hier auch>> nicht schwierig, weil das Ziel sich nicht ändert. Der Einfluss auf die>> Laufzeit ist dementsprechend gering.>> bei ffmpeg sind das 5-10 Extensions(SSE2,AVX,...) die Supported werden> d.h. das wären 5-10 Laufzeit-Ifs - da der Code wohl zu doch groß ist für> den Cache würde ich schon denke das es ordentlich spürbar ist
Wenn bei der Erstellung des Objekts die Funktionszeiger einmalig mit 20
ifs gesetzt werden, dann sind sie ab diesem Zeitpunkt konstant. Und das
schafft eine gute Sprungvorhersage im Prozessor (aka speculative
execution).
S. R. schrieb:>>> Wobei man das auch ganz gut in C++>>> verstecken kann.>>>> keine Ahnung was du damit sagen willst> class MyFancyAlgo { ... }; /* abstract */> class MyFancyAlgoSse2 : public MyFancyAlgo { ... };> class MyFancyAlgoSse3 : public MyFancyAlgo { ... };>> // muss von der implementation nix wissen
kurz "du kannst auch die C Funktions-Pointer durch Objekte mit
virtuellen ersetzen"
das ist klar
Für wirklich schnelle Routinen schreibt man optimierte Routinen für
mehrere Prozessoren. Das Programm stellt dann einmal fest, auf was für
einem Prozessor es läuft oder welche Fähigkeiten der hat und verwendet
dann die entsprechende Routine.
Ansonsten werden die SSE-Operationen durch normale CPU-Opcodes
ausgeführt. Ein Versuch, so einen Opcode auf einem Prozessor ohne die
entsprechende Erweiterung auszuführen, löst einen Fehler-Interrupt aus
(illegal opcode).
Ben B. schrieb:> Für wirklich schnelle Routinen schreibt man optimierte Routinen für> mehrere Prozessoren. Das Programm stellt dann einmal fest, auf was für> einem Prozessor es läuft oder welche Fähigkeiten der hat und verwendet> dann die entsprechende Routine.
das ist mir klar - ich wollte nur wissen ob das Ersetzen vielleicht
direkt gepatcht wird und nicht über Funktion-Pointer oder abstrakte
Klassen läuft
aber es scheinen nur diese beiden normalen Strategien zu sein - kein
On-the-fly-Patching von Code oder sowas - die Derefenzierung wird eben
als kleine Einbuße akzeptiert und ist bei großen Algorithmen eh
belanglos
Oder: Gucken welche CPU man hat und dann die richtige Datei aus einer
Sammlung jeweils optimierter .dll/.so nachladen... oder gleich mehrere
Binaries des Gesamtprogramms vorhalten und bei der Installation schon
das richtige nehmen...
cppbert3 schrieb:> ich wollte nur wissen ob das Ersetzen vielleicht> direkt gepatcht wird
Selbstmodifizierender Code war in der Frühzeit üblich, würde mich nicht
wundern, wenn da nicht auch zur Laufzeit der Algorithmenaufruf gepatcht
würde. In jedem Fall kenne ich das aus der 3./4. Generation (386/486).
Sowas beißt sich natürlich mit aktuellem Sicherheitsdenken.
Der Linux-Kernel mappt VDSO an eine fixe Adresse im Adressraum jeden
Prozesses. Welches VDSO genommen wird, hängt wiederum vom Prozessor ab
(z.B. wegen SYSENTER vs. SYSCALL bei den CPU-Herstellern).
Ansonsten, wie bereits gesagt, kann man auch einfach die passende DLL
(oder .so) laden, dann wird das Patchen vom dynamic loader erledigt. Und
zumindest für mplayer sind mir Win32-Varianten mit getrennten Binaries
bekannt.
Also ja, grundsätzlich wurde alles, was möglich ist, auch mal gemacht.
Aber der "übliche" Weg besteht tatsächlich aus einer billigen
Indirektion, weil die einfach besser zu entwickeln ist.
Selbstmodifizierender Code war sicherheitstechnisch schon immer ungerne
gesehen. Alle guten Virenscanner mit heuristischer Analyse zu 486er
Zeiten haben solche Programme sofort als "suspicious" eingestuft.
Der 486 hat diesbezüglich auch einen interessaten Bug - wenn man Code
modifiziert, der bereits in die Pipeline geladen wurde, übernimmt der
Prozessor die Änderung nicht und führt den unmodifizierten Code aus.
Andy D. schrieb:> Oder: Gucken welche CPU man hat und dann die richtige Datei aus einer> Sammlung jeweils optimierter .dll/.so nachladen
Aber nicht nach dem Typcode der CPU gehen, sondern nur nach den von ihr
publizierten Eigenschaften. Die passen nämlich nicht immer zusammen.
In Virtualisierungsumgebungen werden für Lastausgleich und ggf auch für
höhere Verfügbarkeit mehrere Hosts zu einer Gruppe zusammengefasst. Da
die VMs darin jederzeit den Host wechseln können, ohne das zu merken,
wird zwar der momentane CPU-Typ direkt durchgereicht, aber die
Feature-Bits werden durch den Hypervisor ggf auf einen gemeinsamen
Subset reduziert.
NB: In solchen Umgebungen kann es auch schwierig sein, die echte
Taktfrequenz spitz zu kriegen. Da wird vom Betriebssystem der VM
permanent die Nominalfrequenz angezeigt, während der reale Core voll im
Turbo läuft.