A64 Neon SIMD - 256-битное сравнение

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


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

Для равенства у меня уже есть решение:

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

Вопросы

  • Это более эффективно, чем четыре сравнения с простыми старыми регистрами ARM?

    • e.g. is there a source that quotes timings for the different operations? I'm doing this on iPhone 5s.
  • Есть ли способ еще больше оптимизировать это? Я думаю, что трачу много циклов только на то, чтобы свести весь вектор к одному скалярному логическому значению.


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

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

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

Затем lhs ‹rhs, если это истинно:

(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 ‹r1), (l2‹ r2)
  • cmlo.2d => (l3 ‹r3), (l4‹ r4)
  • cmeq.2d => (l1 = r1), (l2 = r2)
  • cmeq.2d => (l3 = r3), (l4 = r4)

Вопросы

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

Обновить

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

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

Затем я раскладываю три квадрата 2x2 в своей матрице и выполняю побитовое И их. Теперь я уменьшаю с помощью побитового ИЛИ - сначала с двух векторов до одного вектора, затем до одного байта, затем копирую в регистр 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;
}

person Etan    schedule 20.04.2015    source источник
comment
Как UInt256 используется в другом месте, т.е. значения, скорее всего, будут заранее находиться в регистрах SIMD, регистрах общего назначения или в памяти? Я предполагаю, что cmp и 3 ccmps могут иметь меньше накладных расходов, чем куча манипуляций с регистрами SIMD, но необходимость пролить кучу регистров GP и загрузить значения может склонить чашу весов в другую сторону. Я подозреваю, что на вопрос об общей эффективности лучше всего ответить с помощью сравнительного анализа, поскольку на него также повлияет остальная часть вашего кода (давление регистра, использование кеша и т. Д.)   -  person Notlikethat    schedule 20.04.2015
comment
Они раньше находятся в памяти и загружаются с помощью ld1.   -  person Etan    schedule 20.04.2015


Ответы (1)


Выполните сравнительный анализ с помощью XCTest measureMetrics с помощью средства запуска тестов на основе Swift. Выделяются два 256-битных Ints. Затем операция повторяется 100 миллионов раз на тех же двух int, измерение останавливается, и каждой конечности из двух int присваивается новое случайное значение с arc4random. Второй прогон выполняется с присоединенными инструментами, и распределение времени ЦП отмечается для каждой инструкции в виде комментария рядом с ней.


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

Для равенства SIMD, кажется, проигрывает, когда результат переносится из регистров SIMD обратно в регистр ARM. SIMD, вероятно, имеет смысл только тогда, когда результат используется в дальнейших вычислениях SIMD, или если используются более длинные целые числа, чем 256-битные (ld1 кажется быстрее, чем ldp).

  • # P4 #
    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")
    
    # P5 #
  • # P6 # # P7 #
    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;
    
    # P8 #
  • C

    LLVM не может определить, что "ccmp" является хорошей инструкцией для использования здесь, и работает медленнее, чем версия asm выше.

    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,531776, 11,533287, 11,526628, 11,531392, 11,526037, 11,531784, 11,533786]


Меньше (‹)

(будет определено - обновлю позже)

person Etan    schedule 23.04.2015
comment
Хорошо сделано! В варианте ccmp вы потенциально можете сэкономить драгоценную полосу пропускания памяти, загрузив и протестировав сначала наиболее значимые пары, и полностью пропустив второй набор нагрузок, если это сравнение не удастся. - person Notlikethat; 25.04.2015
comment
Мне нужно постоянное время работы и отсутствие доступа к памяти, зависящей от данных. - person Etan; 25.04.2015