Быстрое умножение k x k булевых матриц, где 8 <= k <= 16

Я хочу найти как можно быстрее способ умножения двух небольших булевых матриц, где малые значения, 8x8, 9x9 ... 16x16. Эта процедура будет использоваться очень много, поэтому она должна быть очень эффективной, поэтому, пожалуйста, не предполагайте, что простое решение должно быть достаточно быстрым.

Для особых случаев 8x8 и 16x16 у меня уже есть довольно эффективные реализации, основанные на найденном решении здесь , где мы обрабатываем всю матрицу как uint64_t или uint64_t [4] соответственно. На моей машине это примерно в 70-80 раз быстрее, чем простая реализация.

However, in the case of 8 < k < 16, I don't really know how I can leverage any reasonable representation in order to enable such clever tricks as above.

Поэтому в основном я открыт для любых предложений, использующих любое представление (матриц) и сигнатуру функции. Вы можете предположить, что это нацелено на 32-битную или 64-битную архитектуру (выберите, что лучше всего подходит для вашего предложения)

8
nl ja de
@hakoja, например, в правильной ситуации, если у вас есть два значения unsigned int, которые представляют строку A и столбец B, а проблемная область предусматривает, что значения каждого элемента a_row и b_col равны только 0 или 1 , а точка (a_row, b_col) всегда равна 0 или 1 (это означало бы матрицы с только некоторыми допустимыми значениями), то для умножения 32-элементного вектора было бы просто (0 <(a_row & b_col)). Очень быстро, без операции умножения.
добавлено автор Josh Petitt, источник
Матрицы разрежены?
добавлено автор Josh Petitt, источник
укажите тип ваших массивов.
добавлено автор Josh Petitt, источник
можете ли вы дать образец умножения и результата? После элементарного умножения вектора столбца на вектор строки вы выполняете сумму (то есть продукт)? Или вы ожидаете, что результат также будет булевой матрицей (т. Е. Элементы равны 1 или 0)?
добавлено автор Josh Petitt, источник
@hakoja, по моему опыту, реализация «быстрого» умножения матрицы не обязательно означает понимание математики так же, как понимание машины и проблемной области. Вот почему я задаю вопросы.
добавлено автор Josh Petitt, источник
@hakoja, AFAIK, «полностью нормальное» матричное умножение «булевой» матрицы не приводит к булевой матрице. Рассмотрим матричное умножение матрицы 2x2 A = [[1, 1], [1, 1]] и B = [[1, 1], [1, 1]], результат A * B = [[2, 2], [2, 2]], а не «булева» матрица? Я не совсем понимаю требование GF (2). Я googled это, но не понимаю, как это относится к вашей проблеме. Можете ли вы показать две матрицы выборки и ожидаемый результат?
добавлено автор Josh Petitt, источник
Можете ли вы уточнить, что вы подразумеваете под умножением булевых матриц? Вы говорите об арифметике modulo-2?
добавлено автор Oliver Charlesworth, источник
Почему мы можем выбрать 32-битный или 64-битный?
добавлено автор John Dvorak, источник
@Josh Спасибо за проявленный интерес к моей проблеме :) и извините, если я не понял. GF (2) - это еще один способ сказать «делать каждую операцию по модулю 2», т.е. add = XOR и mult. = AND, поэтому в вашем примере A * B фактически будет [[0,0], [0,0]]. ссылка wikipedia объясняет это хорошо. И - да: вы правы, что мы не будем использовать «обычное» умножение, но будем использовать логический И вместо (поскольку они эквивалентны). Однако мой вопрос несколько выходит за рамки этого. См. Ссылку в OP для примера того, как мы можем использовать, что мы работаем с булевыми матрицами.
добавлено автор hakoja, источник
@Josh Нет, матрицы не являются (обязательно) разреженными, но вы можете предположить, что они всегда обратимы (если это какая-то помощь). Ожидаемый результат - новая k x k булева матрица yes. То есть Я хочу полностью нормальное матричное умножение двух булевых матриц (т. Е. Элементы находятся в GF (2)). Часть вопроса заключается в нахождении хорошего представления этих матриц, что позволяет эффективно вычислять, поэтому я не ставил критерии для выбранных вами типов. Меня тоже не волнует, если вы выбрали порядок столбцов или строк. Делайте то, что проще для вас, я всегда могу извлечь существенные идеи :)
добавлено автор hakoja, источник
@Oli Да - матрицы состоят только из двоичных значений, поэтому для всех операций вы можете просто использовать битовые операции.
добавлено автор hakoja, источник
@Dvorak Это было просто не ограничить никаких ответов. Если у вас есть очень умный способ сделать это, но для этого требуется 64-бит, просто используйте это :)
добавлено автор hakoja, источник

3 ответы

Учитывая две матрицы 4x4 a = 0010,0100,1111,0001, b = 1100,0001,0100,0100, сначала можно вычислить транспонирование b '= 1000,1011,0000,0100.

Then the resulting matrix M(i,j)=a x b mod 2 == popcount(a[i]&b[j]) & 1;//or parity

Из этого можно заметить, что сложность растет только в n ^ 2, если битвектор соответствует компьютерному слову.

Это может быть ускорено для матриц 8x8, по крайней мере, при условии, что доступны некоторые специальные операции перестановки и выбора бит. Можно итератировать ровно N раз с NxN битами в векторе. (так что 16x16 - это в значительной степени предел).

Each step consists of accumulating i.e. Result(n+1) = Result(n) XOR A(n) .& B(n), where Result(0) = 0, A(n) is A <<< n, and '<<<' == columnwise rotation of elements and where B(n) copies diagonal elements from the matrix B:

    a b c          a e i          d h c          g b f
B=  d e f  B(0) =  a e i  B(1) =  d h c   B(2) = g b f
    g h i          a e i          d h c          g b f

И, немного подумав об этом, лучший вариант - это ^^^ (строка wise rotate) матрица B и выберите A (n) == скопированные диагоналями столбцы из A:

    a b c         a a a           b b b           c c c 
A=  d e f  A(0) = e e e , A(1) =  f f f,  A(2) =  d d d 
    g h i         i i i           g g g           h h h 

EDIT To benefit later readers, I'd propose the full solution for W<=16 bit matrix multiplications in portable C.

#include 
void matrix_mul_gf2(uint16_t *a, uint16_t *b, uint16_t *c)
{
   //these arrays can be read in two successive xmm registers or in a single ymm
    uint16_t D[16];     //Temporary
    uint16_t C[16]={0}; //result
    uint16_t B[16];  
    uint16_t A[16];
    int i,j;
    uint16_t top_row;
   //Preprocess B (while reading from input) 
   //-- "un-tilt" the diagonal to bit position 0x8000
    for (i=0;i>(W-i));
    for (i=0;i>15; //copy sign bit to rows
        for (i=0;i
7
добавлено
Нет проблем с этим - хотя в настоящий момент я бы рассмотрел полностью параллельную версию, которая использовала бы инструкции ~ W * 16 xmm, превосходящие подход W ^ 2 * K, предложенный мной в первой редакции.
добавлено автор Aki Suihkonen, источник
@Hakoja Не стесняйтесь переместить принятый ответ на этот ответ. Он охватывает больше земли, чем мой, и имеет пример кода, так что это намного более полно.
добавлено автор Sjoerd, источник
@AkiSuihkonen Последнее должно быть «c [i] = W [i]" -> "c [i] = C [i]"? Кроме того, знаете оптимизацию Intel для W == 4?
добавлено автор Chad Brewbaker, источник
Спасибо за ваш ответ, однако, поскольку Сьёрдд первым предложил использовать транспонированные матрицы, я приму его ответ.
добавлено автор hakoja, источник

Как насчет заполнения его до следующего «умного» (например, 8 или 16) размера со всеми «1» по диагонали?

5
добавлено
@JanDvorak, если вы не используете тот факт, что логическое «умножение» - это просто AND. Кроме того, процессор будет работать с размером слова. Таким образом, любая «маленькая» квадратная матрица должна использовать размер родного слова машины и, вероятно, использовать MSB, чтобы избежать необходимости переходить на получение окончательного ответа.
добавлено автор Josh Petitt, источник
Тогда вы потратите много времени на умножение нулей, если вы набрали от 9x9 до 16x16.
добавлено автор John Dvorak, источник
Мы умножаем битовые векторы, машинное слово за раз. Дополнительные умножения будут намного перевешиваться условным ветвлением, которое вы потратите на обработку разных размеров.
добавлено автор sheu, источник
@sheu Как вы думаете, вы могли бы немного расширить свой ответ? Как точно должно выглядеть представление 16x16 матрицы 9x9 (скажем)? И какова должна быть подпись этой функции? Должна ли заполняемость быть ответственной за вызывающую или собственно функцию?
добавлено автор hakoja, источник

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

4
добавлено
Я считаю, что вы забываете суммировать по некоторому размеру и по существу вычислять только a. * B ';
добавлено автор Aki Suihkonen, источник
Абсолютно, как и в моем ответе ниже.
добавлено автор Aki Suihkonen, источник
@AkiSuihkonen Хороший улов, спасибо! Это лишает меня большей части моего ответа, хотя я думаю, что все еще есть преимущество, сохраняя транспонирование.
добавлено автор Sjoerd, источник