Вычисления на Wolfram Mathematica – ускоряем и распараллеливаем
Существует достаточно популярное мнение, что MathWorks MATLAB – это численные расчёты, а Wolfram Mathematica – это символьные вычисления. У приверженцев этой точки зрения, заметка, про Wolfram Mathematica и Большие вычисления, вызвала некоторое замешательство, которое можно сформулировать одной фразой: «она же медленная, она же интерпретирует». Но, оказывается, Wolfram Mathematica не такая уж и медленная, и умеет компилировать. И, более того, умеет «делать» параллельные программы. Об этом речь и пойдёт ниже.
Настоящее эссе, конечно ни в коем случае не претендует на полноту раскрытия означенной темы про компилирование кода и параллельные вычисления в Wolfram Mathematica. Это скорее некий пример, с одной стороны, дающий представление о том, как это делается, с другой – несколько защищающий положения уже упомянутой заметки, что Wolfram Mathematica действительно возможно применять при прототипировании систем Больших вычислений.
Дальнейшее изложение, в принципе, рассчитано на читателя уже немного знакомого с системой Wolfram Mathematica 8 и с синтаксисом её языка программирования. Если знаний всё же окажется недостаточно, по ходу чтения можно заглядывать в официальное справочное руководство.
Итак, существенного ускорения вычислений на Wolfram Mathematica можно добиться если использовать компилируемый код. Для создания подобного кода необходимо использовать системную функцию Compile[ ], которая фактически является обёрткой для пользовательских функций. Компиляция возможна в код «C» (если установлен соответствующий компилятор) или в код «WVM» (Wolfram Virtual Machine).
При применении Compile[ ] необходимо помнить (знать), что расплатой за ускорение является жёсткая типизация используемых переменных. Поддерживаемые типы: _Integer – machine-size integer; _Real – machine-precision approximate real number (default); _Complex – machine-precision approximate complex number; True | False – logical variable. Поэтому два похожих кода могут привести к совершенно разному поведению программы.
FqError = Compile[{},
FullSimplify[y (Sin[x]^2 + Cos[x]^2)]
, CompilationTarget -> "C"];
FqNorma = Compile[{},
FullSimplify[y Log[Sin[x]^2 + Cos[x]^2]]
, CompilationTarget -> "C"];
Так, функция FqNorma[ ] отрабатывает нормально и выдаёт в качестве результата «0», в свою очередь, функция FqError[ ] также выдаёт правильный результат «y», но при этом порождает два диагностических сообщения:
CompiledFunction::cfse: Compiled expression y should be a machine-size integer.
CompiledFunction::cfexe: Could not complete external evaluation;
proceeding with uncompiled evaluation.
Из этих сообщений становится ясно, что выражение, которое возвращает FullSimplify[ ] в функции FqError[ ] имеет тип не поддерживающийся в режиме исполнения компилируемого кода, поэтому она исполняется в интерпретируемом режиме.
В интерпретируемом режиме происходит существенное падение скорости расчётов. Это не было заметно в примере данном выше, но наглядно проявляется при больших объёмах вычислений. Рассмотрим две функции:
FIntDo =
Function[
Module[{sum},
sum = 0;
Do[sum++;, {i, 1, 10 000 000}];
sum]
];
FCmpDo =
Compile[{},
Module[{sum},
sum = 0;
Do[sum++;, {i, 1, 10 000 000}];
sum]
, CompilationTarget -> "C"];
Результат запуска:
FIntDo[ ] // AbsoluteTiming
{9.2196162, 10 000 000}
FCmpDo[ ] // AbsoluteTiming
{0.0780001, 10 000 000}
Как видно из приведённых чисел, компилируемый код (в этом конкретном примере) быстрее интерпретируемого примерно в 118 раз. Практически пустой цикл из 10 млн. итераций Wolfram Mathematica на машине с Intel Core i5-2430M в однопоточном режиме проходит за 78 мс. Быстро это или медленно – судите сами.
Теперь о параллелизме. В Wolfram Mathematica его можно реализовать тремя основными способами. Первые два, через MathLink (и/или Wolfram LibraryLink) и Parallelize[ ] (и близкие по смыслу функции) – здесь не рассматриваются, ибо напрямую к компилируемому, внутри Mathematica, коду отношения не имеют. Речь пойдёт о третьем способе, который реализуется через опции Compile[ ]:
RuntimeAttributes -> {Listable} и Parallelization -> True.
Необходимо заметить, что параллелизм, который осуществляется через соответствующие опции функции Compile[ ], во-первых, относится к модели общей памяти (shared memory model), а во-вторых, к модели SIMD (single instruction multiple data). Сама схема многопоточности очень близка к идеологии нитей в OpenMP. Число потоков, которое доступно для параллельного исполнения, определяется системной переменной $ProcessorCount.
Итак, определим функцию, которую будем вызывать параллельно:
Funter = Compile[{{X, _Integer, 1}, {Y, _Integer}},
Module[{sum, QuantityElem},
QuantityElem = Length[X];
sum = 0.;
Do[sum += X[[ind]], {ind, 1, QuantityElem}];
sum/Y]
, RuntimeAttributes -> {Listable}, Parallelization -> True, CompilationTarget -> "C"];
А также данные, над которыми будут происходить вычисления:
XX10 = Range[10];
XX11 = Range[5];
YY11 = Range[4];
XX2 = Table[i + Range[5], {i, 1, 4}];
YY2 = Range[4];
Возможны четыре базовых варианта вызова функции Funter[ ]:
Z0 = Funter[XX10, 10]
5.5
Z1 = Funter[XX11, YY11]
{15., 7.5, 5., 3.75}
Z2 = Funter[XX2, YY2]
{15., 10., 8.33333, 7.5}
Z3 = Funter[XX2, 100]
{0.15, 0.2, 0.25, 0.3}
Варианты вызова различаются следующим:
«Z0» – функция Funter[ ] вызывается один раз, и выполняется в один поток.
«Z1» – функция Funter[ ] вызывается 4 раза, исполняется над одними и теми же элементами массива XX11, но над различными элементами массива YY11. Выполняется в несколько потоков.
«Z2» – функция Funter[ ] вызывается 4 раза, каждый вызов использует свою строку из массива XX2 и элемент из массива YY11. Выполняется в несколько потоков.
«Z3» – функция Funter[ ] вызывается 4 раза, каждый вызов использует свою строку из массива XX2. Выполняется в несколько потоков.
Таким образом, этот несложный пример наглядно демонстрирует модели SIMD и shared memory, реализованные в функции Compile[ ] системы Wolfram Mathematica. Комбинируя приведённые способы вызовов функций, можно строить весьма сложные и достаточно быстрые параллельные программы, выполняющиеся в виде скомпилированного кода, и по своей структуре отвечающие идеологии нитевого параллелизма OpenMP.
26 октября 2012 года.
Андрей Макаренко,
группа «Конструктивная Кибернетика».
Обсуждение: contact@rdcn.ru
Ключевые слова: Wolfram Mathematica, компилированный код, параллельные вычисления, shared memory, SIMD, Большие вычисления.