Forum: PC-Programmierung wie werden SSE,SSE2,usw. Mathe-Routinen zur Laufzeit gewählt?


von cppbert3 (Gast)


Lesenswert?

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(double x)
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

von Μαtthias W. (matthias) Benutzerseite


Lesenswert?

Hi

In ffmpeg läuft das über Funktionspointer
https://github.com/FFmpeg/FFmpeg/blob/master/libavfilter/x86/vf_blend_init.c

Matthias

von (prx) A. K. (prx)


Lesenswert?

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.

: Bearbeitet durch User
von cppbert3 (Gast)


Lesenswert?

(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

von cppbert3 (Gast)


Lesenswert?

Μαtthias W. schrieb:
> Hi
>
> In ffmpeg läuft das über Funktionspointer
> https://github.com/FFmpeg/FFmpeg/blob/master/libavfilter/x86/vf_blend_init.c
>
> Matthias

Danke für die ffmpeg Referenz - die wissen schon wie man was schnelles 
zaubert - hatte aber nicht gedacht das dort dann auch Funktionszeiger 
zum Einsatz kommen

von S. R. (svenska)


Lesenswert?

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
class MyFancyAlgo { ... }; /* abstract */
2
class MyFancyAlgoSse2 : public MyFancyAlgo { ... };
3
class MyFancyAlgoSse3 : public MyFancyAlgo { ... };
4
5
// muss von der implementation nix wissen
6
void processData(MyFancyAlgo* algo, void* data);
7
8
int main() {
9
  // nimm beste implementation
10
  MyFancyAlgo* algo;
11
  if (cpuKnowsSse3()) algo = new MyFancyAlgoSse3();
12
  else if (cpuKnowsSse2()) algo = new MyFancyAlgoSse2();
13
  else exit(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).

von cppbert3 (Gast)


Lesenswert?

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

von cppbert3 (Gast)


Lesenswert?

und wenn du die Implementierung mit final festzurrst ist der Unterschied 
zur freien Funktion nur noch die this-Übergabe

von Ben B. (Firma: Funkenflug Industries) (stromkraft)


Lesenswert?

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

von cppbert3 (Gast)


Lesenswert?

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

von Andreas D. (rackandboneman)


Lesenswert?

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

: Bearbeitet durch User
von S. R. (svenska)


Lesenswert?

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.

: Bearbeitet durch User
von Ben B. (Firma: Funkenflug Industries) (stromkraft)


Lesenswert?

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.

von (prx) A. K. (prx)


Lesenswert?

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.

: Bearbeitet durch User
von cppbert3 (Gast)


Lesenswert?

das mit den "Erkennungsproblemen" in  Virtualisierungsumgebungen wusste 
ich gar nicht

Danke für die vielen Infos

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.