Pull to refresh

Comments 19

UFO just landed and posted this here
Самое забавное, что если не оборачивать в функцию, то не будет работать с глобальными переменными:

# 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>&?
Просто интересуюсь, вроде бы затея с диспетчиризацией типов мне понравилась, но важность расстановки пробелов и переносов строк немного испугала. Узнаю детали дальше, но может проще было бы самому открыть документацию...

Если структура декларируется как structure, то она по-умолчанию неизменяема. Если mutable structure, то изменяема. Система типов у Julia несколько отличается от привычных императивных языков. Но довольно простая.

Ну и на счёт возвращаемых значений. В норме функция именно возвращает новое значение, а не модифицирует входные аргументы. Такой подход и в Ruby, кстати.

А вариант функции с модификацией, когда надо изменить именно тот массив, который передан на вход, автоматически означает, что имя функции будет с суффиксом `!`.

Спасибо, джулия стала чуть понятнее после этой статьи!

Sign up to leave a comment.

Articles