Я хотел бы сравнить два малоразмерных 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;
}
Как 'UInt256' используется в других местах, то есть те ценности, более вероятно, чтобы быть в SIMD регистры общего назначения регистров или памяти заранее? Я бы предположил, что 'cmp' и 3' ccmp 'могут иметь меньше накладных расходов, чем куча жонглирования регистра SIMD, но для того, чтобы пролить кучу регистров GP и загрузить значения, можно отклонить баланс другим способом. Я подозреваю, что на вопрос об общей эффективности лучше всего ответить на бенчмаркинг, так как это то, на что будет влиять остальная часть вашего кода (давление в регистре, использование кеша и т. Д.). – Notlikethat
Они находятся в памяти раньше и загружаются с помощью " LD1" . – Etan