Юлия против Mathematica: численная интеграция - PullRequest
3 голосов
/ 08 февраля 2020

Джулия, абсолютно новичок здесь с вопросом для вас.

Я учу себя немного Джулии, портируя некоторые из моих кодов Mathematica и Python (в основном научные c вычисления в физике и c и посмотреть что к чему. До сих пор все было довольно гладко. И быстро. До сих пор.

Теперь я моделирую элементарный блокировочный усилитель, который, по сути, принимает - возможно, очень сложный - зависящий от времени сигнал Uin(t) и выдает выходной сигнал * 1006. *, фазовая автоподстройка в некоторой опорной частоте fref (то есть, она подчеркивает компонент Uin(t), который имеет определенное фазовое соотношение с синусоидальной волной эталонной). Мало что имеет значение в описании, имеет значение , что это в основном делает это путем вычисления интеграла (я на самом деле опускаю фазу здесь для ясности):

enter image description here

Итак, я изложил и протестировал это в Mathematica и Julia: я определяю макет Uin(t), передаю некоторые значения параметров, а затем создаю массив Uout(t) в момент t = 0, для диапазона fref.

  • Юлия: Я использовал пакет QuadGK для числовой интеграции.

    T = 0.1
    f = 100.
    Uin(t) = sin(2pi * f * t) + sin(2pi * 2f *t)
    Uout(t, fref)::Float64 = quadgk(s -> Uin(s) * sin(2pi * fref * s), t-T, t, rtol=1E-3)[1]/T
    frng = 80.:1.:120.
    print(@time broadcast(Uout, 0., frng))
    
  • Mathematica

    T = 0.1;
    f = 100.;
    Uin[t_] := Sin[2 π f t] + Sin[2 π 2 f t]
    Uout[t_, fref_] := NIntegrate[Sin[2 π fref s] Uin[s], {s, t - T, t}]/T
    frng = Table[i, {i, 80, 120, 1}];
    Timing[Table[Uout[0, fr], {fr, frng}]]
    

Результаты:

Джулия рассчитывала эту операцию где-то между 45 и 55 секундами на ноутбуке i7-5xxx от батареи, что составляет много , тогда как Mathematica делала это за ~ 2 секунды. Разница ужасна и, честно говоря, в это трудно поверить. Я знаю, что Mathematica имеет несколько довольно приятных и изящных алгоритмов в своем ядре, но Джулия - это Джулия. Итак, вопрос: что дает?

PS: установка f и T как const уменьшает время Джулии до ~ 8-10 секунд, но f и T не может быть const с в реальной программе. Кроме этого, есть ли что-то очевидное, что я упускаю?

РЕДАКТИРОВАТЬ 2 февраля 2020:

Замедление, по-видимому, связано с алгоритму, пытающемуся выследить точность, когда значение близко к нулю, например, см. ниже: для fref = 95 вычисление занимает 1 целую секунду (!), в то время как для смежных значений частоты оно вычисляется мгновенно (возвращаемый результат является кортежем ( res, ошибка)). Кажется, что функция quadgk останавливается при очень малых значениях):

  0.000124 seconds (320 allocations: 7.047 KiB)
fref = 94.0 (-0.08637214864144352, 9.21712218998258e-6)

  1.016830 seconds (6.67 M allocations: 139.071 MiB, 14.35% gc time)
fref = 95.0 (-6.088184966010742e-16, 1.046186419361636e-16)

  0.000124 seconds (280 allocations: 6.297 KiB)
fref = 96.0 (0.1254003757465191, 0.00010132083518769636)

Примечания: это независимо от того, какой допуск я хочу получить. Кроме того, Mathematica, как правило, задает допуски точности станка по умолчанию, в то же время несколько замедляя работу при приближении к нулям, и numpy / scipy просто пролетают через все это, но дают менее точные результаты, чем Mathematica (при настройках по умолчанию; в этом не особо разбирались ).

Ответы [ 3 ]

6 голосов
/ 10 февраля 2020

Ваша проблема связана с выбором допустимого отклонения. Относительная ошибка 1e-3 звучит не так уж плохо, но на самом деле это когда интеграл близок к нулю. В частности, это происходит, когда fref = 80.0 (и 85, 90, 95, , а не 100, 105 и т. Д. c.):

julia> Uout(0.0, 80.0, f, T)
1.2104987553880609e-16

Цитировать из строки документации quadgk:

(Обратите внимание, что полезно указывать положительный атол в тех случаях, когда норма (I) может быть равна нулю.)

Давайте попробуем установить абсолютный допуск, например 1е-6, и сравнить. Сначала код (используя код из @ARamirez):

Uin(t, f) = sin(2π * f * t) + sin(4π * f * t)

function Uout(t, fref, f , T)
    quadgk(s -> Uin(s, f) * sin(2π * fref * s), t-T, t, rtol=1e-3)[1]/T
end
function Uout_new(t, fref, f , T) # with atol
    quadgk(s -> Uin(s, f) * sin(2π * fref * s), t-T, t, rtol=1e-3, atol=1e-6)[1]/T
end

Затем бенчмаркинг (используйте для этого BenchmarkTools)

using BenchmarkTools
T = 0.1
f = 100.0
freqs = 80.0:1.0:120.0

@btime Uout.(0.0, $freqs, $f, $T);
6.302 s (53344283 allocations: 1.09 GiB)

@btime Uout_new.(0.0, $freqs, $f, $T);
1.270 ms (11725 allocations: 262.08 KiB)

ОК, это в 5000 раз быстрее. Это нормально?

6 голосов
/ 08 февраля 2020

Первая проблема, которую я вижу с вашим кодом, заключается в том, что он нестабилен. Это вызвано , потому что вы используете глобальные переменные (см. Совет по производительности номер один в Советы по производительности Julia ): компилятор не может знать типы f и T, которые вы используются внутри ваших функций, поэтому он не может сделать эффективную компиляцию. Именно поэтому, когда вы помечаете их как const, производительность улучшается: теперь у компилятора есть гарантия, что они не изменят свой тип, поэтому он может эффективно скомпилировать ваши две функции.

Как увидеть, что ваш код нестабильно

Если вы запустите свою первую функцию с макросом @code_warntype, например, так:

@code_warntype Uin(0.1,f)

Вы увидите вывод, подобный следующему:

julia> @code_warntype Uin(0.1)
Variables
  #self#::Core.Compiler.Const(Uin, false)
  t::Float64

Body::Any
1 ─ %1 = (2.0 * Main.pi)::Core.Compiler.Const(6.283185307179586, false)
│   %2 = (%1 * Main.f * t)::Any
│   %3 = Main.sin(%2)::Any
│   %4 = (2.0 * Main.pi)::Core.Compiler.Const(6.283185307179586, false)
│   %5 = (2.0 * Main.f)::Any
│   %6 = (%4 * %5 * t)::Any
│   %7 = Main.sin(%6)::Any
│   %8 = (%3 + %7)::Any
└──      return %8

Все эти Anys говорят вам, что компиляция не знает тип вывода на любом этапе.

Как исправить

Вы можете переопределить ваши функции, чтобы принимать f и T как переменные:

Uin(t,f) = sin(2.0pi * f * t) + sin(2.0pi * 2.0f *t)
Uout(t, fref,f,T)::Float64 = quadgk(s -> Uin(s,f) * sin(2pi * fref * s), t-T, t, rtol=1E-3)[1]/T

С этими переопределениями ваш код работает намного быстрее. Если вы попытаетесь проверить их с помощью @code_warntype, вы увидите, что теперь компилятор правильно определяет тип всего.

Для дальнейшего улучшения производительности вы можете проверить Julia Performance Tips

В частности, общепринятый метод измерения производительности вместо использования @time - это @btime из пакета BenchmarkTools. Это связано с тем, что при запуске @time вы также измеряете время компиляции (другой вариант - запускать @time два раза - вторая мера будет правильной, поскольку все функции могли компилироваться).

4 голосов
/ 08 февраля 2020

Есть несколько вещей, которые вы можете сделать, чтобы ускорить его. Изменение порядка интеграции немного помогло, использование Float32 вместо Float64 дало небольшое улучшение, а использование @fastmath - еще одно небольшое улучшение. Можно также использовать SLEEFPirates.sin_fast

using QuadGK, ChangePrecision

@changeprecision Float32 begin
    T = 0.1
    f = 100.
    @inline @fastmath Uin(t,f) = sin(2pi * f * t) + sin(2pi * 2f *t)
    @fastmath Uout(t, fref,f,T) = first(quadgk(s -> Uin(s,f) * sin(2pi * fref * s), t-T, t, rtol=1e-2, order=10))/T

    frng = 80.:1.:120.
    @time Uout.(0., frng, f, T)
end
...