Мнение специалиста от 26 октября 2012 года | Конструктивная кибернетика

Вычисления на 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, Большие вычисления.