Intel SSE: Warum gibt '_mm_extract_ps' 'int' anstelle von 'float' zurück?

7

Warum gibt _mm_extract_ps eine int anstatt einer float zurück?

Was ist der richtige Weg, um ein einzelnes float von einem XMM-Register in C zu lesen?

Oder anders gefragt: Was ist das Gegenteil von _mm_set_ps ?

    
Mehrdad 02.04.2011, 23:52
quelle

4 Antworten

1

Von der MSDN-Dokumentation glaube ich, dass Sie das Ergebnis in einen Float-Effekt umsetzen können.

Beachten Sie, dass der Wert 0xc0a40000 äquivalent zu -5.125 (a.m128_f32 [1]) ist.

Update: Ich empfehle dringend die Antworten von @ doug65536 und @PeterCordes (unten) anstelle von mir, die scheinbar schlecht performenden Code auf vielen Compilern erzeugt.

    
holtavolt 03.04.2011, 04:08
quelle
17

Keine der Antworten scheint die Frage tatsächlich zu beantworten, warum gibt sie int zurück.

Der Grund dafür ist, dass der Befehl extractps tatsächlich eine Komponente des Vektors in ein allgemeines Register kopiert. Es scheint ziemlich albern zu sein, dass es einen int zurückgibt, aber genau das passiert tatsächlich - der rohe Fließkommawert endet in einem allgemeinen Register (das Ganzzahlen enthält).

Wenn Ihr Compiler zum Generieren von SSE für alle Gleitkommaoperationen konfiguriert ist, dann wäre es am ehesten, wenn Sie einen Wert in ein Register "extrahieren", den Wert in die niedrige Komponente des Vektors mischen und dann in einen Skalar umwandeln schweben. Dies sollte dazu führen, dass diese Komponente des Vektors in einem SSE-Register verbleibt:

%Vor%

Das _mm_cvtss_f32 intrinsic ist frei, es generiert keine Anweisungen, es bewirkt nur, dass der Compiler das xmm-Register als float neuinterpretiert, so dass es als solches zurückgegeben werden kann.

Der _mm_shuffle_ps bekommt den gewünschten Wert in die unterste Komponente. Das Makro _MM_SHUFFLE generiert einen unmittelbaren Operanden für die resultierende Anweisung shufps .

Das 2 im Beispiel ruft den Float von Bit 95:64 des 127: 0-Registers ab (die dritte 32-Bit-Komponente von Anfang an, in Speicherreihenfolge) und speichert es in der 31: 0-Komponente des Registers (der Anfang, in der Speicherreihenfolge).

Der resultierende generierte Code gibt höchstwahrscheinlich den Wert in einem Register zurück, wie bei jedem anderen Gleitkommawert, ohne ineffizient in den Speicher zu schreiben und ihn zurückzulesen.

Wenn Sie Code generieren, der die x87-FPU für Fließkommawerte verwendet (für normalen C-Code, der nicht SSE-optimiert ist), würde dies wahrscheinlich dazu führen, dass ineffizienter Code generiert wird - der Compiler würde wahrscheinlich die Komponente des SSE-Vektor verwendet dann fld , um es zurück in den x87-Registerstapel zu lesen. Im Allgemeinen verwenden 64-Bit-Plattformen kein x87 (sie verwenden SSE für alle Fließkomma-Anweisungen, meist skalare Anweisungen, es sei denn, der Compiler vektorisiert).

Ich sollte hinzufügen, dass ich immer C ++ verwende, also bin ich mir nicht sicher, ob es effizienter ist, __m128 nach Wert oder Zeiger in C zu übergeben. In C ++ würde ich const __m128 & verwenden und diese Art von Code wäre in einem Header, so kann der Compiler inline.

    
doug65536 23.06.2013 07:03
quelle
4

Verwirrenderweise ist int _mm_extract_ps() nicht dafür da, ein skalares float -Element von einem Vektor zu bekommen. Das intrinsische stellt die Speicherzielform der Anweisung nicht zur Verfügung (was dafür nützlich sein kann) Zweck). Dies ist nicht der einzige Fall, in dem die intrinsischen Elemente nicht direkt alles ausdrücken können, für das eine Anweisung nützlich ist. : (

gcc und clang wissen, wie die asm-Anweisung funktioniert und verwenden sie so für Sie, wenn Sie andere Shuffles erstellen. type-punning das _mm_extract_ps Ergebnis zu float führt in der Regel zu schrecklichen Asm aus gcc ( extractps eax, xmm0, 2 / mov [mem], eax ).

Der Name macht Sinn, wenn Sie an _mm_extract_ps denken, wenn Sie ein IEEE 754 binary32 Fließkomma-Bitmuster extrahieren der FP-Domäne der CPU in die Ganzzahldomäne (als C-Skalar int ), anstatt FP-Bitmuster mit ganzzahligen Vektoroperationen zu manipulieren. Laut meinen Tests mit gcc, clang und icc (siehe unten) ist dies der einzige "portable" Anwendungsfall, bei dem _mm_extract_ps in allen Compilern in gute Asm kompiliert wird . Alles andere ist nur ein Compiler-spezifischer Hack, um die gewünschte ASM zu erhalten.

Die entsprechende Anweisung asm ist EXTRACTPS r/m32, xmm, imm8 . Beachten Sie, dass das Ziel ein Speicher oder ein Integer -Register sein kann, aber kein anderes XMM-Register. Es ist das FP-Äquivalent von PEXTRD r/m32, xmm, imm8 (auch in SSE4.1), wobei die Ganzzahl Register-Zielformular ist offensichtlich nützlich. EXTRACTPS ist nicht das Gegenteil von INSERTPS xmm1, xmm2/m32, imm8 .

Vielleicht macht diese Ähnlichkeit mit PEXTRD die interne Implementierung einfacher, ohne den Anwendungsfall (für asm, nicht intrinsics) zu beeinträchtigen, oder vielleicht hielten die SSE4.1-Entwickler bei Intel es auf diese Weise nützlicher als als eine nicht-destruktive FP-domain copy-and-shuffle (die x86 ernsthaft ohne AVX fehlt). Es gibt FP-Vektor-Befehle, die eine XMM-Quelle und ein Speicher-oder-Xmm-Ziel haben, wie MOVSS xmm2/m32, xmm Also wäre diese Art von Unterricht nicht neu. Interessante Tatsache: Die Opcodes für PEXTRD und EXTRACTPS unterscheiden sich nur im letzten Bit.

In der Assemblierung ist ein Skalar float nur das untere Element eines XMM-Registers (oder 4 Bytes im Speicher). Die oberen Elemente des XMM müssen nicht einmal auf Nullen gesetzt werden, damit Anweisungen wie ADDSS funktionieren, ohne dass irgendwelche zusätzlichen Kosten anfallen FP-Ausnahmen In Aufrufkonventionen, die FP-Argumente in XMM-Registern übergeben / zurückgeben (z. B. alle üblichen x86-64-ABIs), muss float foo(float a) davon ausgehen, dass die oberen Elemente von XMM0 bei der Eingabe "marbage" enthalten, aber in den hohen Elementen von XMM0 inaktiv bleiben Rückkehr. ( Weitere Informationen ).

As @doug weist darauf hin , andere Shuffle-Anweisungen können verwendet werden, um ein float-Element eines Vektors in das untere Ende eines xmm-Registers zu bringen. Dies war bereits ein meist gelöstes Problem in SSE1 / SSE2 , und es scheint, dass EXTRACTPS und INSERTPS nicht versuchten, es für Registeroperanden zu lösen.

SSE4.1 INSERTPS xmm1, xmm2/m32, imm8 ist eine der besten Möglichkeiten für Compiler, _mm_set_ss(function_arg) zu implementieren, wenn der skalare Gleitkommazahl ist bereits in einem Register und sie können nicht optimiert werden, um die oberen Elemente auf Null zu setzen. ( Was am meisten ist der Zeit für Compiler anders als Clang ). Diese verknüpfte Frage erörtert auch den Ausfall von intrinsischen Systemen, um die Last zu exponieren oder Versionen von Anweisungen wie EXTRACTPS, INSERTPS und PMOVZX zu speichern, die einen Speicheroperanden schmaler als 128b haben (und daher auch ohne AVX keine Ausrichtung erfordern). Es kann unmöglich sein, sicheren Code zu schreiben, der so effizient kompiliert wie das, was Sie in asm tun können.

Ohne AVX-3-Operanden SHUFPS bietet x86 keinen vollständig effizienten und universellen Weg zum Kopieren und Mischen eines FP-Vektors wie Ganzzahl PSHUFD kann. SHUFPS ist ein anderes Biest, wenn es nicht direkt mit src = dst verwendet wird. Um das Original zu erhalten, ist ein MOVAPS erforderlich, das vor IvyBridge CPU-Belastung und Latenzzeit kostet und immer eine Code-Größe kostet. Die Verwendung von PSHUFD zwischen FP-Befehlen kostet Latenz (Bypass-Verzögerungen). (Siehe diese horizontale Summe Antwort für einige Tricks, wie SSE3 MOVSHDUP verwenden.

SSE4.1 INSERTPS kann ein Element in ein separates Register extrahieren, aber AFAIK hat immer noch eine Abhängigkeit vom vorherigen Wert des Ziels, selbst wenn alle ursprünglichen Werte ersetzt wurden. Falsche Abhängigkeiten wie diese sind schlecht für die Out-of-Order-Ausführung. xor-zeroing ein Register als Ziel für INSERTPS wäre immer noch 2 Ups, und haben eine geringere Latenz als MOVAPS + SHUFPS auf SSE4 .1 CPUs ohne Mov-Elimination für MOVAPS ohne Latenzzeit (nur Penryn, Nehalem, Sandybridge. Auch Silvermont, wenn Sie Low-Power-CPUs mit einbeziehen). Die Code-Größe ist jedoch etwas schlechter.

Verwenden Sie _mm_extract_ps und geben Sie dann das Ergebnis zurück in den Float (wie vorgeschlagen in der derzeit akzeptierten Antwort und seinen Kommentaren) ist eine schlechte Idee. Es ist einfach für Ihren Code zu etwas schrecklichen kompilieren (wie EXTRACTPS in den Speicher und dann wieder in ein XMM-Register laden) entweder gcc oder ICC. Clang scheint immun zu sein vor dem Gehirn-Tod-Verhalten und macht seine üblichen Shuffle-Compiling mit seiner eigenen Wahl von Shuffle-Anweisungen (einschließlich der entsprechenden Verwendung von EXTRACTPS).

Ich habe versucht, diese Beispiele mit gcc5.4 -O3 -msse4.1 -mtune=haswell , clang3.8.1 und icc17, auf der Compiler-Explorer Godbolt. Ich habe C-Modus, nicht C ++, aber Union-basierte Art Punning ist in GNU C ++ als eine Erweiterung zu ISO C ++ erlaubt. Pointer-Casting für Typ-Punning verletzt striktes Aliasing in C99 und C ++, sogar mit GNU-Erweiterungen.

%Vor%

Wenn Sie das Endergebnis in einem xmm-Register haben wollen, liegt es am Compiler, Ihre Extrakte zu optimieren und etwas völlig anderes zu tun. Gcc und clang sind beide erfolgreich, aber ICC nicht.

%Vor%

Beachten Sie, dass icc auch für extr_pun schlecht war, daher mag es keine Union-basierte Typ-Punning-Funktion.

Der klare Gewinner hier ist das Shuffle "manuell" mit _mm_shuffle_ps(v,v, 2) und mit _mm_cvtss_f32 . Wir haben optimalen Code von jedem Compiler für Register- und Speicherziele, außer für ICC welche konnte EXTRACTPS für den Speicher-dest-Fall nicht verwenden. Mit AVX, SHUFPS + separater Speicher ist immer noch nur 2 Ups auf Intel-CPUs, nur größere Code-Größe und benötigt ein tmp-Register. Ohne AVX würde es jedoch ein MOVAPS kosten, den ursprünglichen Vektor nicht zu zerstören: /

Nach Agner Fogs Instruktionstabellen , alle Intel CPUs außer Nehalem implementieren die Register-Zielversionen von sowohl PEXTRD als auch EXTRACTPS mit mehreren UPs: Normalerweise nur einen Shuffle-UP und einen MOVD-UPOP, um Daten aus dem Vektor zu bewegen domain to gp-integer Nehalem register-destination EXTRACTPS ist 1 uop für Port 5 mit 1 + 2 Zykluslatenz (1 + Bypass-Verzögerung).

Ich habe keine Ahnung, warum sie es geschafft haben, EXTRACTPS als einen einzigen UOP, aber nicht als PEXTRD zu implementieren (was 2 Ups und eine Latenz von 2 + 1 Zyklen bedeutet). Nehalem MOVD ist 1 UOP (und läuft auf jedem ALU-Port), mit 1 + 1 Zykluslatenz. (Die +1 ist für die Umgehungsverzögerung zwischen vec-int und allgemeinen Ganzzahlregs, glaube ich).

Nehalem kümmert sich sehr um Vektor-FPs gegen Integer-Domains; SnB-Familien-CPUs haben kleinere (manchmal Null) Umgehungsverzögerungslatenzen zwischen Domänen.

Die Speicher-Ziel-Versionen von PEXTRD und EXTRACTPS sind beide 2 Ups auf Nehalem.

Auf Broadwell und später, Speicher-Ziel EXTRACTPS und PEXTRD sind 2 Ups, aber auf Sandybridge durch Haswell, Speicher-Ziel EXTRACTPS ist 3 Ups. Speicher-Ziel-PEXTRD ist 2 ups auf alles außer Sandybridge, wo es 3 ist. Das scheint seltsam, und Agner Fogs Tabellen haben manchmal Fehler, aber es ist möglich. Micro-Fusion funktioniert nicht mit einigen Anweisungen auf einigen Mikroarchitekturen.

Wenn sich einer der Befehle als äußerst nützlich für etwas Wichtiges herausgestellt hat (z. B. in inneren Schleifen), würden die CPU-Entwickler Ausführungseinheiten erstellen, die das Ganze als ein UOP (oder vielleicht 2 für das Speicherziel) ausführen könnten. Aber das erfordert möglicherweise mehr Bits im internen uop-Format (das Sandybridge vereinfacht).

Fun fact: _mm_extract_epi32(vec, 0) kompiliert (bei den meisten Compilern) zu movd eax, xmm0 , was kürzer und schneller ist als pextrd eax, xmm0, 0 .

Interessanterweise schneiden sie auf Nehalem anders ab (was eine Menge über Vektor-FPs gegenüber Integer-Domains ausmacht, und kamen bald danach heraus SSE4.1 wurde in Penryn (45nm Core2) eingeführt. EXTRACTPS mit einem Registerziel ist 1 uop mit 1 + 2 Zykluslatenz (die +2 von einer Umgehungsverzögerung zwischen FP und Integer-Domäne). PEXTRD ist 2 Ups und läuft in 2 + 1 Zykluslatenz.

    
Peter Cordes 16.12.2016 20:26
quelle
1

Probieren Sie _mm_storeu_ps oder eine der Variationen von SSE-Speicheroperationen .

    
Steve-o 03.04.2011 03:58
quelle

Tags und Links