Каков наиболее эффективный способ построения матриц больших блоков в Mathematica? - PullRequest
11 голосов
/ 29 июля 2011

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

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

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

Ответы [ 2 ]

8 голосов
/ 29 июля 2011

Код, показанный ниже, доступен здесь: 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, это может быть быстрее или медленнее. Это всегда быстрее, чем версия [...] Table.

Вы могли бы даже написать это, используя SparseArray:

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

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

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

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

4 голосов
/ 30 июля 2011

Глядя на то, что 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 и т. Д.): Вы можете сохранитьдобавление констант и производной времени от количества констант является постоянным.Это также подтверждается отсутствием такой опции в 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 s дают одинаковый вывод, а именно:

        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 (который, однако,, переназначается в самой внутренней части цикла на R2 = R1, возможно, если мы выведем на C, это в конечном итоге будет оптимизировано gcc).

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...