Каков наиболее эффективный способ построения больших матричных матриц в Mathematica?

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

Очевидно, что если матрица разрежена, можно использовать конструкторы SparseMatrix, но будут случаи, когда матрица блоков, которую вы строите, не является разреженной.

Какова наилучшая практика для такого рода проблем? Я предполагаю, что элементы матрицы являются числовыми.

Ответ 1

Ниже приведен код, приведенный ниже: http://pastebin.com/4PWWxGhB. Просто скопируйте и вставьте его в блокнот, чтобы проверить его.

Я действительно пытался сделать несколько функциональных способов вычисления матриц, так как я что функциональный способ (который обычно идиоматичен в Mathematica) более эффективен.

В качестве одного примера у меня была эта матрица, состоящая из двух списков:

In: L = 1200;
e = Table[..., {2L}];
f = Table[..., {2L}];

h = Table[0, {2L}, {2L}];
Do[h[[i, i]] = e[[i]], {i, 1, L}];
Do[h[[i, i]] = e[[i-L]], {i, L+1, 2L}];
Do[h[[i, j]] = f[[i]]f[[j-L]], {i, 1, L}, {j, L+1, 2L}];
Do[h[[i, j]] = h[[j, i]], {i, 1, 2 L}, {j, 1, i}];

Моим первым шагом было все время.

In: h = Table[0, {2 L}, {2 L}];
AbsoluteTiming[Do[h[[i, i]] = e[[i]], {i, 1, L}];]
AbsoluteTiming[Do[h[[i, i]] = e[[i - L]], {i, L + 1, 2 L}];]
AbsoluteTiming[
 Do[h[[i, j]] = f[[i]] f[[j - L]], {i, 1, L}, {j, L + 1, 2 L}];]
AbsoluteTiming[Do[h[[i, j]] = h[[j, i]], {i, 1, 2 L}, {j, 1, i}];]

Out: {0.0020001, Null}
{0.0030002, Null}
{5.0012861, Null}
{4.0622324, Null}

DiagonalMatrix[...] был медленнее, чем циклы do, поэтому я решил просто использовать циклы Do на последнем шаге. Как вы можете видеть, использование Outer[Times, f, f] в этом случае было намного быстрее.

Затем я написал эквивалент, используя Outer для блоков в верхнем правом и нижнем левом углу матрицы, и DiagonalMatrix для диагонали:

AbsoluteTiming[h1 = ArrayPad[Outer[Times, f, f], {{0, L}, {L, 0}}];]
AbsoluteTiming[h1 += Transpose[h1];]
AbsoluteTiming[h1 += DiagonalMatrix[Join[e, e]];]


Out: {0.9960570, Null}
{0.3770216, Null}
{0.0160009, Null}

DiagonalMatrix был фактически медленнее. Я мог бы заменить его только контурами Do, но я сохранил его, потому что он выглядел чище.

Текущая цифра составляет 9,06 секунды для наивного цикла Do и 1,389 секунды для моей следующей версии с использованием Outer и DiagonalMatrix. Примерно в 6,5 раз быстрее, не так уж плохо.


Звучит намного быстрее, не так ли? Попробуйте использовать Compile сейчас.

In: cf = Compile[{{L, _Integer}, {e, _Real, 1}, {f, _Real, 1}},
   Module[{h},
    h = Table[0.0, {2 L}, {2 L}];
    Do[h[[i, i]] = e[[i]], {i, 1, L}];
    Do[h[[i, i]] = e[[i - L]], {i, L + 1, 2 L}];
    Do[h[[i, j]] = f[[i]] f[[j - L]], {i, 1, L}, {j, L + 1, 2 L}];
    Do[h[[i, j]] = h[[j, i]], {i, 1, 2 L}, {j, 1, i}];
    h]];

AbsoluteTiming[cf[L, e, f];]

Out: {0.3940225, Null}

Теперь он работает в 3,56 раза быстрее, чем моя последняя версия, и в 23,23 раза быстрее, чем первая. Следующая версия:

In: cf = Compile[{{L, _Integer}, {e, _Real, 1}, {f, _Real, 1}},
   Module[{h},
    h = Table[0.0, {2 L}, {2 L}];
    Do[h[[i, i]] = e[[i]], {i, 1, L}];
    Do[h[[i, i]] = e[[i - L]], {i, L + 1, 2 L}];
    Do[h[[i, j]] = f[[i]] f[[j - L]], {i, 1, L}, {j, L + 1, 2 L}];
    Do[h[[i, j]] = h[[j, i]], {i, 1, 2 L}, {j, 1, i}];
    h], CompilationTarget->"C", RuntimeOptions->"Speed"];

AbsoluteTiming[cf[L, e, f];]

Out: {0.1370079, Null}

Большая часть скорости поступала от CompilationTarget->"C". Здесь я получил еще 2,84 ускорения по сравнению с самой быстрой версией и в 66,13 раза быстрее, чем первая версия. Но все, что я сделал, это просто скомпилировать его!

Теперь это очень простой пример. Но это реальный код, который я использую для решения проблемы физики конденсированных сред. Поэтому не отвергайте его как возможно "игрушечный пример".


Как насчет другого примера метода, который мы можем использовать? У меня есть относительно простая матрица, которую я должен создать. У меня есть матрица, состоящая из ничего, кроме только от начала до некоторой произвольной точки. Наивный способ может выглядеть примерно так:

In: k = L;
AbsoluteTiming[p = Table[If[i == j && j <= k, 1, 0], {i, 2L}, {j, 2L}];]
Out: {5.5393168, Null}

Вместо этого создайте его с помощью ArrayPad и IdentityMatrix:

In: AbsoluteTiming[ArrayPad[IdentityMatrix[k], {{0, 2L-k}, {0, 2L-k}}
Out: {0.0140008, Null}

Это действительно не работает для k = 0, но вы можете использовать специальный случай, если вам это нужно. Кроме того, в зависимости от размера k это может быть быстрее или медленнее. Это всегда быстрее, чем таблица [...], хотя.

Вы даже можете записать это с помощью SparseArray:

In: AbsoluteTiming[SparseArray[{i_, i_} /; i <= k -> 1, {2 L, 2 L}];]
Out: {0.0040002, Null}

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

Я посмотрю, смогу ли я выбрать общие методы, которые я придумал, и просто вставлять их в блокнот для использования.

TL; DR: Кажется, что в этих случаях функциональный путь превосходит процедурный путь. Но при компиляции процедурный код превосходит функциональный код.

Ответ 2

Взгляд на то, что Compile делает для циклов Do, поучительно. Рассмотрим это:

L=1200;
Do[.7, {i, 1, 2 L}, {j, 1, i}] // Timing
Do[.3 + .4, {i, 1, 2 L}, {j, 1, i}] // Timing
Do[.3 + .4 + .5, {i, 1, 2 L}, {j, 1, i}] // Timing
Do[.3 + .4 + .5 + .8, {i, 1, 2 L}, {j, 1, i}] // Timing 
(*
{0.390163, Null}
{1.04115, Null}
{1.95333, Null}
{2.42332, Null}
*)

Во-первых, кажется безопасным предположить, что Do не выполняет автоматическую компиляцию своего аргумента, если он имеет некоторую длину (как Map, Nest и т.д.): вы можете продолжать добавлять константы и производную времени, затраченного против vs число констант постоянное. Это подтверждается отсутствием такой опции в SystemOptions["CompileOptions"].

Далее, так как это происходит вокруг n(n-1)/2 раз с n=2*L, поэтому около 3 * 10 ^ 6 раз для нашего L=1200, время, затраченное на каждое добавление, указывает на то, что происходит гораздо больше, чем необходимо.

Далее попробуем

Compile[{{L,_Integer}},Do[.7,{i,1,2 L},{j,1,i}]]@1200//Timing
Compile[{{L,_Integer}},Do[.7+.7,{i,1,2 L},{j,1,i}]]@1200//Timing
Compile[{{L,_Integer}},Do[.7+.7+.7+.7,{i,1,2 L},{j,1,i}]]@1200//Timing
(*
{0.032081, Null}
{0.032857, Null}
{0.032254, Null}
*)

Итак, здесь все разумнее. Давайте посмотрим:

Needs["CompiledFunctionTools`"]
f1 = Compile[{{L, _Integer}}, 
   Do[.7 + .7 + .7 + .7, {i, 1, 2 L}, {j, 1, i}]];
f2 = Compile[{{L, _Integer}}, Do[2.8, {i, 1, 2 L}, {j, 1, i}]];
CompilePrint[f1]
CompilePrint[f2]

два CompilePrint дают тот же результат, а именно

        1 argument
        9 Integer registers
        Underflow checking off
        Overflow checking off
        Integer overflow checking on
        RuntimeAttributes -> {}

        I0 = A1
        I5 = 0
        I2 = 2
        I1 = 1
        Result = V255

    1   I4 = I2 * I0
    2   I6 = I5
    3   goto 8
    4   I7 = I6
    5   I8 = I5
    6   goto 7
    7   if[ ++ I8 < I7] goto 7
    8   if[ ++ I6 < I4] goto 4
    9   Return

f1==f2 возвращает True.

Теперь сделаем

f5 = Compile[{{L, _Integer}}, Block[{t = 0.},
        Do[t = Sin[i*j], {i, 1, 2 L}, {j, 1, i}]; t]];
f6 = Compile[{{L, _Integer}}, Block[{t = 0.},
        Do[t = Sin[.45], {i, 1, 2 L}, {j, 1, i}]; t]];
CompilePrint[f5]
CompilePrint[f6]

Я не буду показывать полные списки, но в первой строке есть строка R3 = Sin[ R1], а во втором - назначение в регистр R1 = 0.43496553411123023 (который, однако, переназначается в самой внутренней части loop by R2 = R1, возможно, если мы выберем C, это будет оптимизировано gcc в конце концов).

Итак, в этих очень простых случаях uncompiled Do просто слепо выполняет тело без его проверки, а Compile выполняет различные простые оптимизации (помимо вывода байтового кода). Хотя здесь я выбираю примеры, которые преувеличивают, как буквально Do интерпретирует свой аргумент, эта вещь частично объясняет большое ускорение после компиляции.

Что касается огромного ускорения в вопросе Майка Бантегуя, я думаю, что ускорение в таких простых проблемах (просто циклизация и умножение вещей) объясняется тем, что нет причин, по которым автоматически созданный код C не может быть оптимизирован компилятором, чтобы ускорить работу. Созданный код C слишком сложно понять для меня, но байт-код читается, и я не думаю, что есть что-то такое расточительное. Таким образом, это не так шокирует, что он настолько быстр при компиляции C. Использование встроенных функций не должно быть быстрее, чем это, так как не должно быть никакой разницы в алгоритме (если есть, Do петля не должна была быть написана именно так).

Все это нужно проверять в каждом случае, конечно. По моему опыту, циклы Do обычно являются самым быстрым способом для такого рода операций. Однако компиляция имеет свои пределы: если вы создаете большие объекты и пытаетесь передать их между двумя скомпилированными функциями (в качестве аргументов), узким местом может быть такая передача. Одним из решений является просто положить все в одну гигантскую функцию и скомпилировать это; это заканчивается тем сложнее и труднее делать (вы вынуждены писать C в mma, так сказать). Или вы можете попробовать скомпилировать отдельные функции и использовать CompilationOptions -> {"InlineCompiledFunctions" -> True}] в Compile. Однако все может быть сложно.

Но это слишком долго.