2015-04-20 7 views
5

Я хотел бы сравнить два малоразмерных 256-битных значения с инструкциями A64 Neon (asm) эффективно.A64 Neon SIMD - 256-битное сравнение


равенства (=)

Для равенства, я уже получил решение:

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

Во-первых, загрузить два значения в регистры SIMD.

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

Сравните каждую 64-разрядную конечность значений друг с другом. Это приводит к -1 (все биты установлены) для тех конечностей, которые равны, и 0 (все разряды очищаются), если бит отличается.

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

Снизить результат от 2-х векторов 1 вектора, оставив только тот, который содержит «0 (все биты ясно)», если есть.

  "uminp.16b v0, v0, v1  \n\t" 

Уменьшить результат от 1 до 1 байт, сохраняя только байт с нулями, если таковой имеется.

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

Переместить в регистр ARM, а затем сравнить с 0xFF. Это результат.

  "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; 
} 

Вопросы

  • Является ли это более эффективны, чем делать 4 сравнение с равнинными старыми регистрами ARM?

    • например. есть ли источник, который цитирует тайминги для разных операций? Я делаю это на iPhone 5s.
  • Есть ли способ оптимизировать это еще дальше? Я думаю, что я трачу много циклов, чтобы уменьшить весь вектор до одного скалярного булева.


Меньше сравнение (<)

Давайте представлять два целые, как кортежи 64-битные конечности (прямой порядок байт):

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

Затем LHS < ок, если это имеет значение верно:

(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 теперь может быть использована для оценки несколько операндов одновременно.Предполагая (l1, l2), (l3, l4), (r1, r2), (r3, r4) - это способ сохранения двух 256-битных чисел, мы можем легко получить все необходимые значения (полезные значения в выделены жирным шрифтом):

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

Вопросы

  • С этими значениями в четырех регистров SIMD, я теперь интересно Какая лучшая стратегия заключается в применении & и | операторов, а затем сводя его к одному булевому.

Update

Я просто ударил вместе рабочую реализацию "меньше чем".

В принципе, я заменил 1s выше на дублирующее условие, потому что A & A == A & 1.

Затем я выложу три квадрата 2x2 в моей матрице и побитовые И их. Теперь я уменьшаю с помощью побитовых ORs - сначала от двух векторов до одного вектора, затем до одного байта, затем копируем в регистр ARM и проверяем на 0xFF. Та же картина, что и для равенства выше.

Вопрос по-прежнему действителен. Я не уверен, оптимален ли код, и задаюсь вопросом, не пропустил ли я какой-то общий шаблон SIMD, чтобы сделать такой материал более эффективно. Также: NEON стоит в таких случаях, когда входные операнды поступают из памяти?

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

Как 'UInt256' используется в других местах, то есть те ценности, более вероятно, чтобы быть в SIMD регистры общего назначения регистров или памяти заранее? Я бы предположил, что 'cmp' и 3' ccmp 'могут иметь меньше накладных расходов, чем куча жонглирования регистра SIMD, но для того, чтобы пролить кучу регистров GP и загрузить значения, можно отклонить баланс другим способом. Я подозреваю, что на вопрос об общей эффективности лучше всего ответить на бенчмаркинг, так как это то, на что будет влиять остальная часть вашего кода (давление в регистре, использование кеша и т. Д.). – Notlikethat

+0

Они находятся в памяти раньше и загружаются с помощью " LD1" . – Etan

ответ

3

Бенчмарк с мерилом XCTestMetrics с тест-драйвом Swift. Два 256-битных Ints выделены. Затем операция повторяется 100 миллионов раз по тем же двум целям, измерение останавливается, и каждой конечности двух ints присваивается новое случайное значение с помощью arc4random. Второй запуск выполняется с приложенными инструментами, а распределение времени процессора указано для каждой команды как комментарий рядом с ним.


Равенство (==)

Для равенства, SIMD, кажется, теряет, когда результат передается от SIMD-регистры обратно в регистр ARM. SIMD, вероятно, стоит того, только когда результат используется в дальнейших вычислениях SIMD, или если используются более длинные интерс, чем 256-битные (ld1, кажется, быстрее, чем 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") 
    

    измеренные [Время, секунды] средний: 11,558, относительное стандартное отклонение: 0,065%, значения: [11,572626, 11,560558, 11,549322, 11,568718, 11,558530, 11,550490, 11,557086 , 11.551803, 11.557529, 11.549782]

  • Стандартный

    Победитель здесь. Инструкция ccmp действительно сияет здесь :-) Ясно, что проблема связана с памятью.

    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; 
    

    измеренные [Время, секунды] средний: 11,146, относительное стандартное отклонение: 0,034%, значения: [11.149754, 11.142854, 11.146840, 11.149392, 11.141254, 11.148708, 11.142293, 11.150491, 11.139593, 11,145873]

  • C

    LLVM не удается обнаружить, что «CCMP» хорошая команда, чтобы использовать здесь, и медленнее, чем версия ассемблерном выше.

    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]; 
    

    Составитель к

    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% 
    

    измеренного [Время, секунды] средний: 11,531, относительное стандартное отклонение: 0,040%, значения: [11,525511, 11,529820, 11,541940, 11,531776, 11,533287, 11,526628, 11.531392, 11.526037, 11.531784, 11.533786]


Less Than (<)

(необходимо определить - будет обновляться позже)

+0

Хорошая работа! В варианте 'ccmp' вы потенциально могли бы сэкономить некоторую дорогостоящую полосу пропускания памяти, сначала загрузив и проверив наиболее значимые пары, и полностью пропустив второй набор нагрузок, если это сравнение не удастся. – Notlikethat

+2

Я хочу постоянное время работы и доступ к памяти, зависящей от данных. – Etan

 Смежные вопросы

  • Нет связанных вопросов^_^