Комментарии 19
# does not work
u = 1
for i in 1 : 5
u += 1
end
Ещё один классный «прикол» :)
2^(-1) # works
x = -1
2^x # DomainError
Ну и ещё один gotcha (не ошибка даже, а просто медленнее работает): это обход массивов по строкам (а не столбцам. как сделал бы любой уважающий себя FORTRANист :)
На discourse.julialang.org часто ещё вопросы интересные задают в стиле «у меня это работает медленно, какого ХРЕНА!!!111», и не всегда они тривиальные )
Первый пример понравился, просто никогда такого не замечал, так как работаю в Jupyter, а он работает на уровень ниже Глобальной области видимости.
Второй выдал
DomainError with -1:
Cannot raise an integer x to a negative power -1.
Make x a float by adding a zero decimal (e.g., 2.0^-1 instead of 2^-1), or write 1/x^1, float(x)^-1, or (x//1)^-1
То есть, скорей всего возведение в отрицательную степень инта вынудило бы создавать переменную флоат, которую бы возвращали, а так разработчики нас вынуждают возводить вещественное число, чтоб вычисления можно было проводить непосредственно на нем.
(вспоминаю gotcha на С/С++ когда всё занулялось из-за деления на инт)
Короче, нежданчики будут везде, и хорошо быть предупрежденным.
А что на счет Julia, то я нашел её очень удобной: можно быстро набросать абстрактные вычисления, всё проверить, а потом ускорить, а то и скачать готовый пакет. Да и сам процесс кодинга приятен, но в моем случае это скорее страстное хобби
У меня лично в REPL'e вот это:
2^(-1)
ошибки никакой не вызывает, а вот это:
x = -1
2^x
вызывает DomainError
. Причем я скорее сначала был удивлен не почему 2-е ругается, а как возможно, чтобы 1-е работало.
Поясню: каждая функция в Main-е вылизана насколько это возможно, а тут выходит явная type instability
: на вход подаётся 2 Int
, а на выходе может быть как Int
(2^2), так и Float
(2^(-1)).
Чтобы обеспечить стабильность типов нужно либо явно каждый раз проверять, больше ли x чем 0 в выражении 2^x, а это долго, либо какая-то хитрость. Оказалось, что хитрость: просто 2^(-1)
не напрямую вызывает pow
или что-то подобное, но сначала хитро парсится за счёт этого -. Т.е. фактически, если выражение имеет вид ...^(-...), то оно работает немного по-другому.
Поэтому если "спрятать" x = -1
, то сразу "ломается", поскольку уже не имеет вид "a^(-..)"
В частности,
2^(1 - 2)
тоже кидает ошибку ;)
Как дела с проверками выхода за границы массивов?
При выходе за границы будет ругаться, побочный эффект — замедление работы с массивом, это если хотите безопасности. А потом с помощью специальной приписки-макроса эту проверку можно убрать, и вот у нас опять производительность С
У программистов на императивных языках программирования есть привычка использовать избыточные конструкции. Однако в Julia можно многие из них не использовать. Например индексы массива. Очень часто они вообще не нужны. Либо индексы могут быть вычислены автоматически (без необходимости вспоминать, с нуля они начинаются или с единицы). Например, традиционный вариант цикла:
let res = 0
x = [1:0.5:10...] # массив от 1 до 10 с шагом 0.5
for i = 1 : length(x)
res = res + 1 / x[i]
end
println(res) # 5.195479314287365
end
Однако в коде видно, что индекс мы не используем по прямому назначению. Значит код может быть переписан как:
let res = 0
x = [1:0.5:10...]
for item in x
res = res + 1 / item
end
println(res) # 5.195479314287365
end
Более того, мы же видим, что и цикл, в общем-то с точки зрения того, что требуется получить, является избыточным. Нас интересует результат свёртки массива по операции. Значит можем выразить эти действия лакончиным:
x = [1:0.5:10...]
res = mapreduce(item -> 1/item, +, x)
println(res) # 5.195479314287365
Имя метода здесь — mapreduce
, значит аргументы им соответствют. Функция item -> 1/item
относится к map
. Функция +
— к reduce
.
Для случая простой свёртки без map
, можем просто использовать reduce
.
Если же действительно нужен индекс, то можно заставить Julia взять его самостоятельно.
let res = 0
x = [1:0.5:10...]
for i in eachindex(x)
res = res + i / x[i]
end
println(res) # 32.80452068571264
end
Но в примере видим, что нас интересовал индекс и элемент, а не индекс для того, чтобы взять элемент. Значит можем заменить код:
let res = 0
x = [1:0.5:10...]
for (i, item) in enumerate(x)
res = res + i / item
end
println(res) # 32.80452068571264
end
Но и в этом случае нас не просили писать цикл, а просили вычилить результат по каждому индексу и элементу. Значит, можем заменить на:
x = [1:0.5:10...]
res = mapreduce(((i, item),) -> i/item, +, enumerate(x))
println(res) # 32.80452068571264
В последнем примере конструкция ((i, item),)
указывает, что надо разобрать первый аргумент из tuple
.
Можно вроде бы обойтись и без явного выделения массива, т.е.
x = 1 : 0.5 : 10
будет работать быстрее, чем явное выделение, ну и mapreduce дружит, да и вообще может быть использован везде, где может использоваться x = [1 : 0.5 : 10 ...]
julia> x = 1 : 0.5 : 10
1.0:0.5:10.0
julia> typeof(x)
StepRangeLen{Float64,Base.TwicePrecision{Float64},Base.TwicePrecision{Float64}}
julia> x = [1 : 0.5 : 10...]
19-element Array{Float64,1}:
1.0
1.5
2.0
2.5
Не везде это будет работать, где может быть использован массив.
Да и, честно говоря, я не хотел менять типы, по сравнению с примерами выше. Просто использовал одну из конструкций для генерации массива.
Да я понимаю, что типы сильно разные, но с правильным дизайном это по идее должно работать везде, где требуется просто нечто, по чему можно итерироваться.
К примеру:
y = 1 : 0.5 : 10
x = zeros(length(y))
x .= y .+ y # in-place addition
x .= 2 .* y # in-place multiplication
y[7] # == 4
using BenchmarkTools
y = 1 : 0.5 : 10000000
x = zeros(length(y))
b = @benchmark begin
x .= y .+ y # in-place addition
x .= 2 .* y # in-place multiplication
end
#BenchmarkTools.Trial:
# memory estimate: 192 bytes
# allocs estimate: 3
# --------------
# minimum time: 117.716 ms (0.00% GC)
# median time: 127.008 ms (0.00% GC)
# mean time: 133.613 ms (0.00% GC)
# maximum time: 194.349 ms (0.00% GC)
# --------------
# samples: 38
# evals/sample: 1
y = [1 : 0.5 : 10000000...]
x = zeros(length(y))
b2 = @benchmark begin
x .= y .+ y # in-place addition
x .= 2 .* y # in-place multiplication
end
#BenchmarkTools.Trial:
# memory estimate: 96 bytes
# allocs estimate: 4
# --------------
# minimum time: 59.379 ms (0.00% GC)
# median time: 62.218 ms (0.00% GC)
# mean time: 66.322 ms (0.00% GC)
# maximum time: 132.069 ms (0.00% GC)
# --------------
# samples: 76
# evals/sample: 1
Спасибо за ответ. Некоторые похожее фишки и в С++ даже завезли.
Итерация в for происходит по ссылке или по значению?
Правильно ли я понимаю что если использовать встроенные функции языка типа map, то там проверка индексов опускается?
Итерация в for происходит по ссылке или по значению?
Я не совсем понимаю вопрос. Даже в C++ — итератор — отдельный класс. В Julia нет таких языковых понятий как ссылка и значение. Любое присвоение следует воспринимать как копирование ссылки (это не так на уровне машинного кода, но справедливо для модели языка). C-шных операторов &a, *a здесь тоже нет.
Правильно ли я понимаю что если использовать встроенные функции языка типа map, то там проверка индексов опускается?
Опять не понимаю вопрос. Если вопрос о том, выполняется ли реально внутренняя проверка, когда мы пишем a[i], чтобы i не вылетел за пределы, то ответ — не знаю. Скорее всего, не проверяется. Иначе на это придётся тратить процессорное время. Но при работе с итератором всегда идёт вычисление следующего элемента с проверкой границы.
Собственно, в примерах выше, я всего лишь продемонстрировал как избегать использование прямого обращения по индексу, чтобы не ошибиться с границами индексов. Это, конечно, ещё не Руби с его Enumerable. Но уже далеко не C++, с его довольно ограниченными итераторами.
Имел ввиду
list = [1 2 3]
for item in list
item = item + 1
end
так будет работать? В rust/c++ можно захватывать по ссылке и по значению в такого рода for-конструкциях.
Да я имел ввиду насколько проверки замедляют код. Например если писать что-то типа
for (i=0;i<n;i++) {
x[i] = x[i]+1
}
То в некоторых случаях даже если в оператор есть проверка на i < n то много компиляторов умеют оптимизировать её, посколько такая же проверка содержится в условии цикла. Хотя для большинства случаев как Вы заметли лучше использовать синтаксис map, map-reduce, range-based-for.
Поэтому в rust например гарантируется что доступ к плохому индексу будет ругаться (panic). Поэтому стало интересно как аналогичную проблему решает Julia.
list = [1 2 3]
for item in list
item = item + 1
end
У Julia всё просто. Воспринимаем переменную как ссылку на объект, но помним, что большинство операций не изменяют исходный объект (чистые функции и всё такое....). Соответственно, этот пример — бесполезен. item — локальная переменная. item + 1 не изменяет объект, на который ссылалась item.
julia> list = [1 2 3]
1×3 Array{Int64,2}:
1 2 3
julia> for item in list
item = item + 1
end
julia> list
1×3 Array{Int64,2}:
1 2 3
А вот такой пример приведёт к модификации (метод с суффиксом `!` является модифицирующим):
julia> list = [[1],[2],[3]]
3-element Array{Array{Int64,1},1}:
[1]
[2]
[3]
julia> for item in list
push!(item, 1)
end
julia> list
3-element Array{Array{Int64,1},1}:
[1, 1]
[2, 1]
[3, 1]
И такой пример также приведёт к модификации по той же причине. item ссылается на автономные объекты-массивы, каждый из которых может быть изменён:
julia> list = [[1],[2],[3]]
3-element Array{Array{Int64,1},1}:
[1]
[2]
[3]
julia> for item in list
item .+= 1
end
julia> list
3-element Array{Array{Int64,1},1}:
[2]
[3]
[4]
Касаемо оптимизаций Julia на граничных значениях циклов — не отвечу. Скорее всего, что-то делает. В сущности, у неё есть возможность посмотреть генерируемый LLVM-код. По нему можно проверить.
Ага т.е. похоже на питон, базовые типы immutable a объекты mutable?
А есть веарина указать что функция принимает типа const vector<int>&
?
Просто интересуюсь, вроде бы затея с диспетчиризацией типов мне понравилась, но важность расстановки пробелов и переносов строк немного испугала. Узнаю детали дальше, но может проще было бы самому открыть документацию...
Ну и на счёт возвращаемых значений. В норме функция именно возвращает новое значение, а не модифицирует входные аргументы. Такой подход и в Ruby, кстати.
А вариант функции с модификацией, когда надо изменить именно тот массив, который передан на вход, автоматически означает, что имя функции будет с суффиксом `!`.
Спасибо, джулия стала чуть понятнее после этой статьи!
6 нежданчиков от Джулии