2015-04-20 9 views
5

Ich möchte zwei Little-Endian 256-Bit-Werte mit A64 Neon Anweisungen (Asm) effizient vergleichen.A64 Neon SIMD - 256-Bit-Vergleich


Gleichheit (=)

Für Gleichheit, ich habe bereits eine Lösung:

bool eq256(const UInt256 *lhs, const UInt256 *rhs) { 
    bool result; 

Zunächst werden die beiden Werte in SIMD-Register laden.

__asm__("ld1.2d { v0, v1 }, %1 \n\t" 
      "ld1.2d { v2, v3 }, %2 \n\t" 

Vergleichen Sie jedes 64-Bit-Glied der Werte miteinander. Dies führt zu -1 (alle gesetzten Bits) für diese Glieder, die gleich sind, und zu 0 (alle Bits löschen), wenn sich ein Bit unterscheidet.

  "cmeq.2d v0, v0, v2  \n\t" 
      "cmeq.2d v1, v1, v3  \n\t" 

reduzieren das Ergebnis von 2 Vektoren zu 1-Vektor, nur den einen zu halten, die „0 (alle Bits clear)“ enthält, wenn es irgendeine.

Reduzieren Sie das Ergebnis von 1 Vektor auf 1 Byte und behalten Sie nur ein Byte mit Nullen bei, falls vorhanden.

  "uminv.16b b0, v0   \n\t" 

In ARM-Register wechseln, dann mit 0xFF vergleichen. Dies ist das Ergebnis.

  "umov  %w0, v0.b[0]  \n\t" 
      "cmp  %w0, 0xFF   \n\t" 
      "cset  %w0, eq    " 
      : "=r" (result) 
      : "m" (*lhs->value), "m" (*rhs->value) 
      : "v0", "v1", "v2", "v3", "cc"); 
    return result; 
} 

Fragen

  • Ist dies effizienter als die vier Vergleiche mit einfachen alten ARM-Register zu tun?

    • z.B. Gibt es eine Quelle, die Timings für die verschiedenen Operationen angibt? Ich mache das auf iPhone 5s.
  • Gibt es eine Möglichkeit, dies noch weiter zu optimieren? Ich denke, dass ich viele Zyklen verschwende, nur um den gesamten Vektor auf einen einzigen skalaren booleschen Wert zu reduzieren.


Weniger als Vergleich (<)

Lasst uns die beiden Ints als Tupel von 64-Bit-Glieder darstellen (Little-Endian):

  • lhs = (l0 , l1, l2, l3)
  • rhs = (r0, r1, r2, r3)

Dann lhs < rhs wenn diese der Wert true:

(l3 < r3) &  1  &  1  &  1  | 
(l3 = r3) & (l2 < r2) &  1  &  1  | 
(l3 = r3) & (l2 = r2) & (l1 < r1) &  1  | 
(l3 = r3) & (l2 = r2) & (l1 = r1) & (l0 < r0) 

SIMD-Befehle nun verwendet werden, können mehrere Operanden zu einem Zeitpunkt, zu bewerten.Angenommen, (l1, l2), (l3, l4), (r1, r2), (r3, r4) ist die Art, wie die zwei 256-Bit-Zahlen gespeichert werden, können wir leicht alle erforderlichen Werte (nützliche Werte in fett):

  • cmlo.2d =>(l1 < r1), (l2 < r2)
  • cmlo.2d =>(l3 < r3), (l4 < r4)
  • cmeq.2d => (l1 = r1), (l2 = r2)
  • cmeq.2d =>(l3 = r3), (l4 = r4)

Fragen

  • Mit diesen Werten in vier SIMD-Register, frage ich mich jetzt Was ist die beste Strategie, um die & und | Operatoren, und dann reduziert es auf einen einzigen Boolean.

aktualisieren

ich zusammen nur gestanzt, um eine funktionierende Implementierung für "kleiner als".

Grundsätzlich ersetzte ich die 1s oben mit einer doppelten Bedingung, weil A & A == A & 1.

Dann lege ich die drei 2x2 Quadrate in meiner Matrix, und bitweise UND ihnen. Jetzt reduziere ich mit bitweisen ORs - zuerst von zwei Vektoren zu einem Vektor, dann zu einem Byte, dann zum ARM-Register kopieren und auf 0xFF testen. Gleiches Muster wie oben für Gleichheit.

Die obige Frage ist immer noch gültig. Ich bin mir nicht sicher, ob der Code noch optimal ist und frage mich, ob ich ein allgemeines SIMD-Muster verpasst habe, um solche Sachen effizienter zu machen. Auch: Lohnt es sich NEON für solche Fälle, wenn die Eingangsoperanden aus dem Speicher kommen?

bool lt256(const UInt256 *lhs, const UInt256 *rhs) { 
    bool result; 
    __asm__(// (l3 < r3) & (l3 < r3) | 
      // (l3 = r3) & (l2 < r2) | 
      // (l3 = r3) & (l2 = r2) & (l1 < r1) & (l1 < r1) | 
      // (l3 = r3) & (l2 = r2) & (l1 = r1) & (l0 < r0) 

      "ld1.2d { v0, v1 }, %1 \n\t" 
      "ld1.2d { v2, v3 }, %2 \n\t" 

      // v0: [ l3 = r3 ] [ l2 = r2 ] 
      // v1: [ l0 < r0 ] [ l1 < r1 ] 
      // v2: [ l0 = r0 ] [ l1 = r1 ] 
      // v3: [ l2 < r2 ] [ l3 < r3 ] 
      // v4: [ l2 = r2 ] [ l3 = r3 ] 
      "cmeq.2d v4, v1, v3  \n\t" 
      "cmlo.2d v3, v1, v3  \n\t" 
      "cmlo.2d v1, v0, v2  \n\t" 
      "cmeq.2d v2, v0, v2  \n\t" 
      "ext.16b v0, v4, v4, 8  \n\t" 

      // v2: [ l1 < r1 ] [ l1 = r1 ] 
      // v1: [ l1 < r1 ] [ l0 < r0 ] 
      "trn2.2d v2, v1, v2  \n\t" 
      "ext.16b v1, v1, v1, 8  \n\t" 

      // v1: [ l1 < r1 & l1 < r1 ] [ l1 = r1 & l0 < r0 ] 
      "and.16b v1, v2, v1  \n\t" 

      // v2: [ l3 < r3 ] [ l3 = r3 ] 
      // v3: [ l3 < r3 ] [ l2 < r2 ] 
      "ext.16b v2, v3, v0, 8  \n\t" 
      "ext.16b v3, v3, v3, 8  \n\t" 

      // v3: [ l3 < r3 & l3 < r3 ] [ l3 = r3 & l2 < r2 ] 
      "and.16b v3, v2, v3  \n\t" 

      // v2: [ l3 = r3 ] [ l3 = r3 ] 
      // v4: [ l2 = r2 ] [ l2 = r2 ] 
      "ext.16b v2, v4, v0, 8  \n\t" 
      "ext.16b v4, v0, v4, 8  \n\t" 

      // v2: [ l3 = r3 & l2 = r2 ] [ l3 = r3 & l2 = r2 ] 
      "and.16b v2, v2, v4  \n\t" 

      // v1: [ l3 = r3 & l2 = r2 & l1 < r1 & l1 < r1 ] 
      //  [ lr = r3 & l2 = r2 & l1 = r1 & l0 < r0 ] 
      "and.16b v1, v2, v1  \n\t" 

      // v1: [ l3 < r3 & l3 < r3 | 
      //  l3 = r3 & l2 = r2 & l1 < r1 & l1 < r1 ] 
      //  [ l3 = r3 & l2 < r2 | 
      //  lr = r3 & l2 = r2 & l1 = r1 & l0 < r0 ] 
      "orr.16b v1, v3, v1  \n\t" 

      // b1: [ l3 < r3 & l3 < r3 | 
      //  l3 = r3 & l2 = r2 & l1 < r1 & l1 < r1 | 
      //  l3 = r3 & l2 < r2 | 
      //  lr = r3 & l2 = r2 & l1 = r1 & l0 < r0 ] 
      "umaxv.16b b1, v1   \n\t" 
      "umov  %w0, v1.b[0]  \n\t" 
      "cmp  %w0, 0xFF   \n\t" 
      "cset  %w0, eq" 
      : "=r" (result) 
      : "m" (*lhs->value), "m" (*rhs->value) 
      : "v0", "v1", "v2", "v3", "v4", "cc"); 
    return result; 
} 
+1

Wie ist 'UInt256' an anderer Stelle verwendet werden, dh die Werte eher in SIMD sein Register, Allzweck- Register oder Speicher vorher? Ich würde mir vorstellen, dass 'cmp' und 3' ccmp's weniger Overhead haben als ein Haufen SIMD-Register-Jonglieren, aber ein paar GP-Register zu verschütten und die Werte zu laden, kann das Gleichgewicht auf die andere Seite bringen. Ich vermute, dass die Frage der Gesamteffizienz am besten durch das Benchmarking beantwortet wird, da es sich dabei um den Rest des Codes handelt (Registerdruck, Cache-Nutzung usw.). – Notlikethat

+0

Sie befinden sich bereits im Speicher und sind geladen mit " ld1 ". – Etan

Antwort

3

Benchmark mit XCTest measureMetrics mit einem Swift-basierten Test Runner. Zwei 256-Bit-Ints werden zugewiesen. Dann wird eine Operation 100 Millionen Mal auf den gleichen zwei Ints wiederholt, die Messung wird gestoppt und jedem Glied der zwei Eingänge wird ein neuer zufälliger Wert mit arc4random zugewiesen. Ein zweiter Lauf wird mit angeschlossenen Geräten durchgeführt, und die CPU-Zeitverteilung wird für jede Anweisung als Kommentar direkt daneben notiert.


Gleichheit (==)

Für Gleichheit scheint SIMD zu verlieren, wenn das Ergebnis übertragen wird, von der SIMD auf die ARM-Register Register zurück. SIMD lohnt sich wahrscheinlich nur, wenn das Ergebnis in weiteren SIMD-Berechnungen verwendet wird, oder wenn längere als 256 Bit verwendet werden (ld1 scheint schneller zu sein als ldp).

  • SIMD

    bool result; 
    __asm__("ld1.2d { v0, v1 }, %1 \n\t" // 5.1% 
         "ld1.2d { v2, v3 }, %2 \n\t" // 26.4% 
         "cmeq.2d v0, v0, v2  \n\t" 
         "cmeq.2d v1, v1, v3  \n\t" 
         "uminp.16b v0, v0, v1  \n\t" // 4.0% 
         "uminv.16b b0, v0   \n\t" // 26.7% 
         "umov  %w0, v0.b[0]  \n\t" // 32.9% 
         "cmp  %w0, 0xFF   \n\t" // 0.0% 
         "cset  %w0, eq    " 
         : "=r" (result) 
         : "m" (*lhs->value), "m" (*rhs->value) 
         : "v0", "v1", "v2", "v3", "cc"); 
    return result;        // 4.9% ("ret") 
    

    gemessen [Time, Sekunden] Durchschnitt: 11,558, relative Standardabweichung: 0,065%, Werte: [11,572626, 11,560558, 11,549322, 11,568718, 11,558530, 11,550490, 11,557086 , 11.551803, 11.557529, 11.549782]

  • Standard-

    Der Gewinner hier. Die ccmp Anweisung scheint wirklich hier :-) Es ist jedoch klar, dass das Problem speichergebunden ist.

    bool result; 
    __asm__("ldp  x8, x9, %1   \n\t" // 33.4% 
         "ldp  x10, x11, %2  \n\t" 
         "cmp  x8, x10    \n\t" 
         "ccmp  x9, x11, 0, eq  \n\t" 
         "ldp  x8, x9, %1, 16  \n\t" // 34.1% 
         "ldp  x10, x11, %2, 16 \n\t" 
         "ccmp  x8, x10, 0, eq  \n\t" // 32.6% 
         "ccmp  x9, x11, 0, eq  \n\t" 
         "cset  %w0, eq    \n\t" 
         : "=r" (result) 
         : "m" (*lhs->value), "m" (*rhs->value) 
         : "x8", "x9", "x10", "x11", "cc"); 
    return result; 
    

    gemessen [Time, Sekunden] Durchschnitt: 11,146, relative Standardabweichung: 0,034%, Werte: [11,149754, 11,142854, 11,146840, 11,149392, 11,141254, 11,148708, 11,142293, 11,150491, 11,139593, 11,145873]

  • C

    LLVM nicht erkennt, dass „ccmp“ eine gute Anleitung ist hier zu verwenden, und ist langsamer als die asm oben Version.

    return 
        lhs->value[0] == rhs->value[0] & 
        lhs->value[1] == rhs->value[1] & 
        lhs->value[2] == rhs->value[2] & 
        lhs->value[3] == rhs->value[3]; 
    

    erstellenden

    ldp x8, x9, [x0]       // 24.1% 
    ldp x10, x11, [x1]       // 0.1% 
    cmp x8, x10        // 0.4% 
    cset w8, eq         // 1.0% 
    cmp x9, x11        // 23.7% 
    cset w9, eq 
    and w8, w8, w9        // 0.1% 
    ldp x9, x10, [x0, #16] 
    ldp x11, x12, [x1, #16]     // 24.8% 
    cmp x9, x11 
    cset w9, eq         // 0.2% 
    and w8, w8, w9 
    cmp x10, x12        // 0.3% 
    cset w9, eq         // 25.2% 
    and w0, w8, w9 
    ret           // 0.1% 
    

    gemessen [Time, Sekunden] Durchschnitt: 11,531, relative Standardabweichung: 0,040%, Werte: [11,525511, 11,529820, 11,541940, 11,531776, 11,533287, 11,526628, 11.531392, 11.526037, 11.531784, 11.533786]


Less Than (<)

(bestimmt werden - später aktualisieren)

+0

Gute Arbeit! In der 'ccmp'-Variante würden Sie möglicherweise wertvolle Speicherbandbreite sparen, indem Sie zuerst die signifikantesten Paare laden und testen und den zweiten Satz von Ladungen ganz überspringen, wenn dieser Vergleich fehlschlägt. – Notlikethat

+2

Ich möchte konstante Laufzeit und keinen datenabhängigen Speicherzugriff. – Etan