Разница в производительности между функциями и сопоставлением с образцом в Mathematica - PullRequest
19 голосов
/ 15 ноября 2010

Таким образом, Mathematica отличается от других диалектов lisp тем, что стирает грани между функциями и макросами.В Mathematica, если пользователь хочет написать математическую функцию, он, скорее всего, будет использовать сопоставление с шаблоном, например f[x_]:= x*x вместо f=Function[{x},x*x], хотя оба будут возвращать один и тот же результат при вызове с f[x].Насколько я понимаю, первый подход является чем-то эквивалентным макросу lisp и, по моему опыту, предпочтителен из-за более краткого синтаксиса.

Итак, у меня есть два вопроса, есть ли разница в производительности между выполняемыми функциями и шаблономсоответствие / макро подход?Хотя часть меня не удивилась бы, если бы функции на самом деле были преобразованы в какую-то версию макросов, чтобы можно было реализовать такие функции, как Listable.

Причина, по которой меня волнует этот вопрос, связана с недавним набором вопросов (1) (2) о попытке отловить ошибки Mathematica в больших программах.Если бы большинство вычислений были определены в терминах функций, мне кажется, что отслеживать порядок вычисления и источник ошибки было бы проще, чем пытаться отловить ошибку после того, как входные данные были переписаны последовательным применением макросов./ шаблоны.

Ответы [ 5 ]

15 голосов
/ 16 ноября 2010

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

Из этого следует, что чем меньше вам придется проходитьсписок правил, тем быстрее оценка.Глядя на то, что происходит при использовании Trace (используя функции * и 100 * gdelfino )

In[1]:= Trace@(#*#)&@x
Out[1]= {x x,x^2}
In[2]:= Trace@g@x
Out[2]= {g[x],x x,x^2}
In[3]:= Trace@h@x
Out[3]= {{h,Function[{x},x x]},Function[{x},x x][x],x x,x^2}

, становится понятно, почему анонимные функции выполняются быстрее и почему использование Function приводит к дополнительным издержкамнад простым SetDelayed.Я рекомендую взглянуть на введение превосходной книги Леонида Шифрина, где эти концепции объяснены в некоторых деталях.

Иногда я составлял Dispatch таблицувсе функции, которые мне нужны, и вручную применили его к моему начальному выражению.Это обеспечивает значительное увеличение скорости по сравнению с обычной оценкой, поскольку ни одна из встроенных функций Mathematica не должна сопоставляться с моим выражением.

12 голосов
/ 26 ноября 2011

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

Не совсем.Mathematica является переписчиком терминов, как и макросы Lisp.

Итак, у меня есть два вопроса, есть ли разница в производительности между выполнением функций и подходом сопоставления с шаблоном / макроса?

Да.Обратите внимание, что вы никогда не выполняете «функции» в Mathematica.Вы просто применяете правила перезаписи, чтобы изменить одно выражение на другое.

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

In[1] := N@Range[100000];

In[2] := Sqrt[xs]; // AbsoluteTiming

Out[2] = {0.0060000, Null}

Мы можем определить глобальное переписываниеправило, которое имеет условия вида sqrt[x], переписанные в Sqrt[x] так, что будет вычислен квадратный корень:

In[3] := Clear[sqrt];
         sqrt[x_] := Sqrt[x];
         Map[sqrt, xs]; // AbsoluteTiming

Out[3] = {0.4800007, Null}

Обратите внимание, что это примерно в 100 раз медленнее, чем в предыдущем решении.

В качестве альтернативы, мы можем определить глобальное правило перезаписи, которое заменяет символ sqrt лямбда-функцией, которая вызывает Sqrt:

In[4] := Clear[sqrt];
         sqrt = Function[{x}, Sqrt[x]];
         Map[sqrt, xs]; // AbsoluteTiming

Out[4] = {0.0500000, Null}

Обратите внимание, что это примерно в 10 раз быстрее, чем в предыдущем решении.

Почему?Поскольку медленное второе решение ищет правило перезаписи sqrt[x_] :> Sqrt[x] во внутреннем цикле (для каждого элемента массива), тогда как быстрое третье решение ищет значение Function[...] символа sqrt один раз, а затем применяет эту лямбдуфункционировать повторно.Напротив, самым быстрым первым решением является цикл, вызывающий sqrt, написанный на C. Таким образом, поиск по глобальным правилам перезаписи является чрезвычайно дорогостоящим, а переписывание терминов - дорогим.

Если так, почему Sqrt всегда быстрый?Можно ожидать замедления в 2 раза вместо 10 ×, потому что мы заменили один поиск для Sqrt на два поиска для sqrt и Sqrt во внутреннем цикле, но это не так, потому что Sqrt имеет особый статусбыть встроенной функцией, которая будет сопоставляться с ядром самого термина перезаписи Mathematica, а не через глобальную таблицу перезаписи общего назначения.

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

Вы можете многое узнать о производительности Mathematica, написав мини-реализации Mathematica.В этом случае вышеприведенные решения могут быть скомпилированы для (например) F #.Массив может быть создан следующим образом:

> let xs = [|1.0..100000.0|];;
...

Встроенная функция sqrt может быть преобразована в замыкание и передана функции map следующим образом:

> Array.map sqrt xs;;
Real: 00:00:00.006, CPU: 00:00:00.015, GC gen0: 0, gen1: 0, gen2: 0
...

Это занимает 6 мс, как и Sqrt[xs] в Mathematica.Но этого следует ожидать, потому что этот код был скомпилирован в JIT для машинного кода .NET для быстрой оценки.

Поиск правил перезаписи в глобальной таблице перезаписи Mathematica аналогичен поиску замыкания в словаре с ключом.на его имя функции.Такой словарь может быть сконструирован следующим образом в F #:

> open System.Collections.Generic;;
> let fns = Dictionary<string, (obj -> obj)>(dict["sqrt", unbox >> sqrt >> box]);;

Это похоже на структуру данных DownValues в Mathematica, за исключением того, что мы не ищем несколько результирующих правил для первого совпадения нааргументы функции.

Затем программа становится:

> Array.map (fun x -> fns.["sqrt"] (box x)) xs;;
Real: 00:00:00.044, CPU: 00:00:00.031, GC gen0: 0, gen1: 0, gen2: 0
...

Обратите внимание, что мы получаем аналогичное снижение производительности в 10 раз из-за поиска в хэш-таблице во внутреннем цикле.

Альтернативой может быть сохранение DownValues, связанного с символом, в самом символе, чтобы избежать поиска в хеш-таблице.

Мы можем даже написать полный перезаписывающий термин всего за несколько строк кода.Термины могут быть выражены в виде значений следующего типа:

> type expr =
    | Float of float
    | Symbol of string
    | Packed of float []
    | Apply of expr * expr [];;

Обратите внимание, что Packed реализует упакованные списки Mathematica, то есть распакованные массивы.

Следующая функция init создает List с n элементами, используя функцию f, возвращая Packed если каждое возвращаемое значение было Float или более общим Apply(Symbol "List", ...) в противном случае:

> let init n f =
    let rec packed ys i =
      if i=n then Packed ys else
        match f i with
        | Float y ->
            ys.[i] <- y
            packed ys (i+1)
        | y ->
            Apply(Symbol "List", Array.init n (fun j ->
              if j<i then Float ys.[i]
              elif j=i then y
              else f j))
    packed (Array.zeroCreate n) 0;;
val init : int -> (int -> expr) -> expr

Следующая функция rule использует сопоставление с образцом для идентификации выражений, которые она может понять, и заменяет их другими выражениями:

> let rec rule = function
    | Apply(Symbol "Sqrt", [|Float x|]) ->
        Float(sqrt x)
    | Apply(Symbol "Map", [|f; Packed xs|]) ->
        init xs.Length (fun i -> rule(Apply(f, [|Float xs.[i]|])))
    | f -> f;;
val rule : expr -> expr

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

Теперь наша программа может бытьопределяется и выполняется нашим пользовательским редактором терминов:

> rule (Apply(Symbol "Map", [|Symbol "Sqrt"; Packed xs|]));;
Real: 00:00:00.049, CPU: 00:00:00.046, GC gen0: 24, gen1: 0, gen2: 0

Мы восстановили производительность Map[Sqrt, xs] в Mathematica!

Мы можем даже восстановить производительность Sqrt[xs], добавивсоответствующее правило:

| Apply(Symbol "Sqrt", [|Packed xs|]) ->
    Packed(Array.map sqrt xs)

Я написал статью о переписывании термина в F # .

6 голосов
/ 16 ноября 2010

Некоторые измерения

Основываясь на ответе @gdelfino и комментариях @rcollyer, я создал небольшую программу:

j = # # + # # &;
g[x_] := x x + x x ;
h = Function[{x}, x x + x x ];


anon = Table[Timing[Do[ # # + # # &[i], {i, k}]][[1]], {k, 10^5, 10^6, 10^5}];
jj   = Table[Timing[Do[ j[i],           {i, k}]][[1]], {k, 10^5, 10^6, 10^5}];
gg   = Table[Timing[Do[ g[i],           {i, k}]][[1]], {k, 10^5, 10^6, 10^5}];
hh   = Table[Timing[Do[ h[i],           {i, k}]][[1]], {k, 10^5, 10^6, 10^5}];

ListLinePlot[ {anon,   jj,    gg,   hh}, 
 PlotStyle -> {Black, Red, Green, Blue},
 PlotRange -> All]

Результаты, по крайней мере для меня, очень удивительны: alt text

Есть объяснения? Пожалуйста, не стесняйтесь редактировать этот ответ (комментарии - беспорядок для длинного текста)

Редактировать

Проверено с функцией тождества f [x] = x, чтобы изолировать анализ от фактической оценки. Результаты (одинаковые цвета):

alt text

Примечание: результаты очень похожи на этот график для постоянных функций (f [x]: = 1);

4 голосов
/ 15 ноября 2010

Сопоставление с образцом кажется более быстрым:

In[1]:= g[x_] := x*x
In[2]:= h = Function[{x}, x*x];

In[3]:= Do[h[RandomInteger[100]], {1000000}] // Timing
Out[3]= {1.53927, Null}

In[4]:= Do[g[RandomInteger[100]], {1000000}] // Timing
Out[4]= {1.15919, Null}

Сопоставление с образцом также более гибкое, поскольку позволяет перегрузить определение:

In[5]:= g[x_] := x * x
In[6]:= g[x_,y_] := x * y

Для простых функций вы можете скомпилировать, чтобы получитьлучшая производительность:

In[7]:= k[x_] = Compile[{x}, x*x]
In[8]:= Do[k[RandomInteger[100]], {100000}] // Timing
Out[8]= {0.083517, Null}
3 голосов
/ 15 ноября 2010

Вы можете использовать функцию recordSteps в предыдущем ответе , чтобы увидеть, что Mathematica на самом деле делает с функциями.Это относится к нему так же, как к любой другой голове.То есть, предположим, что у вас есть следующее

f = Function[{x}, x + 2];
f[2]

Сначала оно преобразует f [2] в

Function[{x}, x + 2][2]

. На следующем шаге x+2 преобразуется в 2+2.По сути, оценка «функции» ведет себя как применение правил сопоставления с образцом, поэтому неудивительно, что она не быстрее.

Вы можете думать обо всем в Mathematica как о выражении, где оценка - это процесспереписывая части выражения в предопределенной последовательности , это относится к функции, как и к любой другой голове

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