Введение в компилятор функций Wolfram

В основе Wolfram Language находится оценщик выражений, который преобразует входные выражения в вычисленные результаты с использованием стандартных правил оценки. Этот оценщик делает язык Wolfram Language (сокращенно WL) невероятно гибким и мощным, но выполнение отдельных выражений с помощью этого оценщика требует затрат производительности. Эта история показывает, как вы можете использовать новый компилятор функций Wolfram, чтобы добиться невероятно высокой производительности. Описанные здесь методы легли в вычислительную основу для создания следующего видео:

Основы

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

f[z_] := z^2

Затем мы можем вызвать эту функцию и повторно запустить ее в цикле Do:

Do[ f[0.0+0.0*I], 1000000 ]

Однако писать такой код WL очень неэффективно. На моем компьютере с Linux (Intel Xeon CPU E3–1245) эта оценка занимает около 700 миллисекунд. Цитируя данные из «Звездного пути»: «Для андроида это почти вечность».

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

В этом случае мы можем использовать эту возможность для небольшого улучшения производительности:

Attributes[f] = {Listable}
c = ConstantArray[0.0 + 0.0*I, 1000000]
f[c]

При этом та же операция выполняется примерно за 500 миллисекунд, поэтому на 200 мс меньше, чем раньше. В простых случаях вы также можете воспользоваться функциями со встроенной оптимизацией производительности. В этом случае возведение числа в квадрат (частный случай функции Power) имеет встроенную оптимизацию, поэтому вы можете просто написать:

c^2

Это значительно сокращает время оценки до 2 миллисекунды.

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

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

f = Function[{Typed[z, "ComplexReal64"]}, Do[z^2, 1000000]]
cf = FunctionCompile[f]
cf[0.0+0.0*I]

Этот код занимает 0,3 миллисекунды, поэтому он быстрее, чем любой из некомпилированных методов. Существует стоимость одноразового этапа компиляции, который может занять от нескольких сотен миллисекунд до нескольких секунд или минут в случае очень сложного кода. Но конечным результатом всегда является очень быстрый и оптимизированный код.

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

cf = FunctionCompile[ f, 
  CompilerOptions -> {"AbortHandling" -> False}
]
cf[0.0+0.0*I]

Этот код оценивается примерно за 0,009 миллисекунды (или 9 микросекунд).

Пример

Теперь давайте применим эти новые знания на практике. Например, мы можем создать визуализацию комплексной функции, вычислив множество точек на комплексной плоскости. Чтобы создать изображение результата, мы берем абсолютное значение после возведения в квадрат каждого комплексного числа. Обратите внимание, что пределы таблицы (x и y) даны так, что они создают изображение с правильной ориентацией:

f = Function[{},
  Table[ Abs[(x+I*y)^2], 
  {y, 1.0, -1.0, -0.01}, 
  {x, -1.0, 1.0, 0.01}
]
cf = FunctionCompile[ f, 
  CompilerOptions -> {"AbortHandling" -> False}
]
Image[cf[]]

В результате получается простое изображение с уровнями серого, указывающими абсолютное значение z², где 0,0 представляет черный цвет, а 1,0 - белый. Абсолютные значения, превышающие 1.0, отсекаются и отображаются здесь белым цветом.

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

f = Function[{}, Table[
  {Abs[(x + I*y)^2], 1.0, 1.0}, 
  {y, 1.0, -1.0, -0.01}, 
  {x, -1.0, 1.0, 0.01}
]]
cf = FunctionCompile[f, 
  CompilerOptions -> {"AbortHandling" -> False}]
Image[cf[], ColorSpace -> "HSB"]

В WL цветовое пространство HSB является циклическим, что означает, что цвета в диапазоне 0–1 повторяются в диапазоне 1–2 и за его пределами. Это означает, что красный цвет представляет 0, или 1, или 2, и так далее. На изображении ниже красный цвет в центре представляет 0, красный круг вокруг него представляет 1, а красный цвет в самых углах представляет 2:

Затем, вместо вычисления абсолютных значений z², мы можем посмотреть на его аргумент (или Arg в WL).

f = Function[{}, Table[
  {0.5*(1+Arg[(x + I*y)^2]/Pi), 1.0, 1.0}, 
  {y, 1.0, -1.0, -0.01}, 
  {x, -1.0, 1.0, 0.01}
]]
cf = FunctionCompile[f, 
  CompilerOptions -> {"AbortHandling" -> False}]
Image[cf[], ColorSpace -> "HSB"]

Мы также можем использовать как абсолютное значение, так и аргумент z², например: Используйте абсолютное значение для оттенка и используйте arg для яркости.

f = Function[{}, Table[
  {Abs[(x + I*y)^2], 1.0, 1.0 + Arg[(x + I*y)^2]/Pi}, 
  {y, 1.0, -1.0, -0.01}, 
  {x, -1.0, 1.0, 0.01}
]]

Поскольку диапазон Arg составляет от -Pi до Pi, нам нужно применить некоторое изменение масштаба, чтобы избежать отрицательных значений яркости (которые полностью черные). Теперь у нас есть визуализация сложной функции z², которая показывает как абсолютное значение (в оттенках), так и аргумент (в яркости, где маленькие значения около нуля черные):

После завершения этого предварительного исследования мы можем перейти к более интересным визуализациям, например, f (z) = z³ + 1. Код ниже немного улучшен для удобства чтения. Сначала создается комплексное число z из его действительной и мнимой частей. Затем вычисляется его значение функции. И, наконец, присваивается значение его цвета:

f = Function[{}, Table[
  Module[{z, fz},
    z = x + I*y;
    fz = z^3 + 1;
    {Abs[fz], 1.0, 1.0 + Arg[fz]/Pi}]
   , {y, 2.0, -2.0, -0.005}, {x, -2.0, 2.0, 0.005}]]

Чтобы лучше отображать нули функции, домен также увеличен с -2 до 2 для действительной и мнимой частей:

На этом этапе становится легко экспериментировать с любой функцией. Ниже приведен пример с более высокими степенями z и тригонометрическими функциями:

f = Function[{}, Table[
   Native`UncheckedBlock@Module[{z = x + I*y, fz},
     fz = Sin[z^5] + Cos[z^3];
     {Log[1 + Abs[fz]], 1.0, 1.0 + Arg[fz]/Pi}]
   , {y, 2.0, -2.0, -0.005}, {x, -2.0, 2.0, 0.005}]]

И получившееся изображение:

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