Главная » Хабрахабр » 6 нежданчиков от Джулии

6 нежданчиков от Джулии

Там реализовано полноценное введение в язык для тех, у кого мало опыта в программировании (остальным будет полезно для общего развития), так же имеется введение в машинное обучение и куча заданий для закрепления материала. Наконец-таки появилось русскоязычное руководство по языку Julia.

Опытные могут пробежаться по экспресс курсу или ознакомиться с книгой How to Think Like a Computer Scientist Во время поисков наткнулся на курс программирования для экономистов (помимо Джулии там есть и Питон).

Далее предоставлен перевод материала из блога Christopher Rackauckas 7 Julia Gotchas and How to Handle Them

Я люблю её, это то, что я считаю самым мощным и интуитивно понятным языком, который я когда-либо использовал. Позвольте мне начать с того, что Julia замечательный язык. Тем не менее, есть некоторые 'подводные камни', хитрые мелочи, о которых вам нужно знать. Это, несомненно, мой любимый язык. Смысл этого поста состоит в том, чтобы помочь вам ускорить этот процесс, разоблачая некоторые из наиболее распространенных ошибок, предлагающих альтернативные методы программирования. У каждого языка есть они, и одна из первых вещей, которую вы должны сделать, чтобы овладеть языком, это выяснить, что они из себя представляют и как их избежать.

Разработчики Julia хотели иметь четко определенные правила поведения. Julia — хороший язык для понимания происходящего, потому что в ней нет магии. Однако это может означать, что вам придется напрягать голову, чтобы понять, почему происходит именно то, а не другое. Это означает, что все поведение может быть объяснено. Вы увидите, что есть некоторые очень похожие шаблоны, и как только вы их осознаете, вы больше не опростоволоситесь ни на одном из них. Вот почему я не просто собираюсь изложить некоторые общие проблемы, но я также собираюсь объяснить, почему они возникают. Однако, как только вы это освоите, вы в полной мере сможете использовать лаконичность Джулии при получении производительности C / Fortran. Из-за этого у Джулии немного более крутая кривая обучения по сравнению с более простыми языками, такими как MATLAB / R / Python. А теперь копнём глубже.

Нежданчик раз: REPL (терминал) имеет глобальную область видимости

Кто-то скажет: "Я слышал, Джулия быстра!", откроет REPL, быстро запишет какой-то хорошо знакомый алгоритм и выполнит этот скрипт. Это, безусловно, самая распространенная проблема, о которой сообщают новые пользователи Julia. После его выполнения смотрят на время и говорят: "Подождите секунду, почему это медленно, как на Питоне?" Поскольку это такая важная и распространенная проблема, давайте потратим немного времени на изучение причин, по которым это происходит, чтобы понять, как этого избежать.

Небольшое отступление: почему Джулия быстра

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

Типоспецифичность определяется основным принципом дизайна Юлии: множественной диспетчеризацией. Если вам нужна полная история, ознакомьтесь с некоторыми заметками, которые я написал для предстоящего семинара. Когда вы пишете код:

function f(a,b) return 2a+b
end

На языке Юлии, функция — это абстракция, а то, что на самом деле вызывается, это метод. кажется что это только одна функция, но на самом деле здесь создается большое количество методов. 0,3. Если вы вызовете f(2. Если вы вызовете f(2,3), то Джулия запустит другой скомпилированный код, который принимает два целых числа и возвращает значение 2a + b. 0), то Джулия запустит скомпилированный код, который принимает два числа с плавающей запятой и возвращает значение 2a + b. И это применяется повсюду: оператор + на самом деле является функцией, которая будет вызывать методы в зависимости от типов, которые он видит. Функция f является абстракцией или сокращением для множества различных методов, имеющих одинаковую форму, и такая схема использования символа f для вызова всех этих различных методов называется множественной диспетчеризацией. 0,3. Julia на самом деле получает свою скорость, потому что скомпилированный ею код знает свои типы, и поэтому скомпилированный код, который вызывает f (2. Вы можете проверить это с помощью макроса code_native, чтобы увидеть скомпилированную сборку: 0), является именно скомпилированным кодом, который вы получите, определив ту же функцию в C / Fortran.

@code_native f(2.0,3.0)

pushq %rbp
movq %rsp, %rbp
Source line: 2
vaddsd %xmm0, %xmm0, %xmm0
vaddsd %xmm1, %xmm0, %xmm0
popq %rbp
retq
nop

Это та же самая скомпилированная сборка, которую вы ожидаете от функции в C / Fortran, и она отличается от кода сборки для целых чисел:

@code_native f(2,3) pushq %rbp
movq %rsp, %rbp
Source line: 2
leaq (%rdx,%rcx,2), %rax
popq %rbp
retq
nopw (%rax,%rax)

Суть: REPL / Global Scope не допускает специфику типов

Прежде всего, обратите внимание, что REPL является глобальной областью действия, потому что Джулия разрешает вложенную область видимости для функций. Это подводит нас к основной мысли: REPL / Global Scope работает медленно, потому что не допускает спецификацию типа. Например, если мы определим

function outer() a = 5 function inner() return 2a end b = inner() return 3a+b
end

Это потому, что Юлия позволяет вам захватить а из внешней функции во внутреннюю функцию. то увидим, что этот код работает. Но теперь давайте подумаем о том, как функция будет компилироваться в этой ситуации. Если вы примените эту идею рекурсивно, то поймете, что самой высокой областью является область, которая является непосредственно REPL (которая является глобальной областью действия модуля Main). Реализуем то же самое, но используя глобальные переменные:

a=2.0; b=3.0
function linearcombo() return 2a+b
end
ans = linearcombo()

и

a = 2; b = 3
ans2= linearcombo()

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

Громоздко

pushq %rbp
movq %rsp, %rbp
pushq %r15
pushq %r14
pushq %r12
pushq %rsi
pushq %rdi
pushq %rbx
subq $96, %rsp
movl $2147565792, %edi # imm = 0x800140E0
movabsq $jl_get_ptls_states, %rax
callq *%rax
movq %rax, %rsi
leaq -72(%rbp), %r14
movq $0, -88(%rbp)
vxorps %xmm0, %xmm0, %xmm0
vmovups %xmm0, -72(%rbp)
movq $0, -56(%rbp)
movq $10, -104(%rbp)
movq (%rsi), %rax
movq %rax, -96(%rbp)
leaq -104(%rbp), %rax
movq %rax, (%rsi)
Source line: 3
movq pcre2_default_compile_context_8(%rdi), %rax
movq %rax, -56(%rbp)
movl $2154391480, %eax # imm = 0x806967B8
vmovq %rax, %xmm0
vpslldq $8, %xmm0, %xmm0 # xmm0 = zero,zero,zero,zero,zero,zero,zero,zero,xmm0[0,1,2,3,4,5,6,7] vmovdqu %xmm0, -80(%rbp)
movq %rdi, -64(%rbp)
movabsq $jl_apply_generic, %r15
movl $3, %edx
movq %r14, %rcx
callq *%r15
movq %rax, %rbx
movq %rbx, -88(%rbp)
movabsq $586874896, %r12 # imm = 0x22FB0010
movq (%r12), %rax
testq %rax, %rax
jne L198
leaq 98096(%rdi), %rcx
movabsq $jl_get_binding_or_error, %rax
movl $122868360, %edx # imm = 0x752D288
callq *%rax
movq %rax, (%r12)
L198:
movq 8(%rax), %rax
testq %rax, %rax
je L263
movq %rax, -80(%rbp)
addq $5498232, %rdi # imm = 0x53E578
movq %rdi, -72(%rbp)
movq %rbx, -64(%rbp)
movq %rax, -56(%rbp)
movl $3, %edx
movq %r14, %rcx
callq *%r15
movq -96(%rbp), %rcx
movq %rcx, (%rsi)
addq $96, %rsp
popq %rbx
popq %rdi
popq %rsi
popq %r12
popq %r14
popq %r15
popq %rbp
retq
L263:
movabsq $jl_undefined_var_error, %rax
movl $122868360, %ecx # imm = 0x752D288
callq *%rax
ud2
nopw (%rax,%rax)

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

a = 1
for i = 1:100 a += a + f(a)
end

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

Как избежать проблемы

Самый простой способ — всегда оборачивать ваши скрипты в функции. Есть несколько способов избежать этой проблемы. Например, предыдущий код примет вид:

function geta(a) # can also just define a=1 here for i = 1:100 a += a + f(a) end return a
end
a = geta(1)

Еще одна вещь, которую вы можете сделать, это определить ваши переменные как константы. Это даст вам тот же результат, но, поскольку компилятор может специализироваться на типе a, он предоставит скомпилированный код, который вы хотите.

const b = 5

Есть небольшая причуда, что Джулия на самом деле позволяет вам изменять значение константы, но не типа. Делая это, вы сообщаете компилятору, что переменная не изменится, и, таким образом, он сможет специализировать весь код, который использует ее, на типе, которым она является в настоящее время. Тем не менее, обратите внимание, что есть некоторые небольшие причуды: Таким образом, вы можете использовать const чтоб сообщить компилятору, что вы не будете изменять тип.

const a = 5
f() = a
println(f()) # Prints 5
a = 6
println(f()) # Prints 5
# WARNING: redefining constant a

это не работает, как ожидалось, потому что компилятор, понимая, что он знает ответ на f () = a (так как a является константой), просто заменил вызов функции на ответ, давая другое поведение, чем если бы a не был постоянной.

Мораль: не пишите свои сценарии непосредственно в REPL, всегда заключайте их в функцию.

Нежданчик два: Нестабильность типов

Позвольте мне задать вопрос, что произойдет, когда ваши типы могут измениться? Итак, я только что высказал мнение о том, насколько важна специализация кода для типов данных. Такая проблема известна как нестабильность типа. Если вы догадались: "Ну, вы и в этом случае не можете специализировать скомпилированный код", тогда вы правы. Например, давайте посмотрим на: Они могут отображаться по-разному, но одним из распространенных примеров является то, что вы инициализируете значение простым, но не обязательно тем типом, которым оно должно быть.

function g() x=1 for i = 1:10 x = x/2 end return x
end

Поэтому, если мы начали с x = 1, целое число изменится на число с плавающей запятой, и, таким образом, функция должна скомпилировать внутренний цикл, как если бы он мог быть любого типа. Обратите внимание, что 1/2 — это число с плавающей точкой в ​​Julia. Если бы вместо этого у нас было:

function h() x=1.0 for i = 1:10 x = x/2 end return x
end

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

Нативная портянка

pushq %rbp
movq %rsp, %rbp
pushq %r15
pushq %r14
pushq %r13
pushq %r12
pushq %rsi
pushq %rdi
pushq %rbx
subq $136, %rsp
movl $2147565728, %ebx # imm = 0x800140A0
movabsq $jl_get_ptls_states, %rax
callq *%rax
movq %rax, -152(%rbp)
vxorps %xmm0, %xmm0, %xmm0
vmovups %xmm0, -80(%rbp)
movq $0, -64(%rbp)
vxorps %ymm0, %ymm0, %ymm0
vmovups %ymm0, -128(%rbp)
movq $0, -96(%rbp)
movq $18, -144(%rbp)
movq (%rax), %rcx
movq %rcx, -136(%rbp)
leaq -144(%rbp), %rcx
movq %rcx, (%rax)
movq $0, -88(%rbp)
Source line: 4
movq %rbx, -104(%rbp)
movl $10, %edi
leaq 477872(%rbx), %r13
leaq 10039728(%rbx), %r15
leaq 8958904(%rbx), %r14
leaq 64(%rbx), %r12
leaq 10126032(%rbx), %rax
movq %rax, -160(%rbp)
nopw (%rax,%rax)
L176:
movq %rbx, -128(%rbp)
movq -8(%rbx), %rax
andq $-16, %rax
movq %r15, %rcx
cmpq %r13, %rax
je L272
movq %rbx, -96(%rbp)
movq -160(%rbp), %rcx
cmpq $2147419568, %rax # imm = 0x7FFF05B0
je L272
movq %rbx, -72(%rbp)
movq %r14, -80(%rbp)
movq %r12, -64(%rbp)
movl $3, %edx
leaq -80(%rbp), %rcx
movabsq $jl_apply_generic, %rax
vzeroupper
callq *%rax
movq %rax, -88(%rbp)
jmp L317
nopw %cs:(%rax,%rax)
L272:
movq %rcx, -120(%rbp)
movq %rbx, -72(%rbp)
movq %r14, -80(%rbp)
movq %r12, -64(%rbp)
movl $3, %r8d
leaq -80(%rbp), %rdx
movabsq $jl_invoke, %rax
vzeroupper
callq *%rax
movq %rax, -112(%rbp)
L317:
movq (%rax), %rsi
movl $1488, %edx # imm = 0x5D0
movl $16, %r8d
movq -152(%rbp), %rcx
movabsq $jl_gc_pool_alloc, %rax
callq *%rax
movq %rax, %rbx
movq %r13, -8(%rbx)
movq %rsi, (%rbx)
movq %rbx, -104(%rbp)
Source line: 3
addq $-1, %rdi
jne L176
Source line: 6
movq -136(%rbp), %rax
movq -152(%rbp), %rcx
movq %rax, (%rcx)
movq %rbx, %rax
addq $136, %rsp
popq %rbx
popq %rdi
popq %rsi
popq %r12
popq %r13
popq %r14
popq %r15
popq %rbp
retq
nop

против

Аккуратное ассемблерное заклинание

pushq %rbp
movq %rsp, %rbp
movabsq $567811336, %rax # imm = 0x21D81D08
Source line: 6
vmovsd (%rax), %xmm0 # xmm0 = mem[0],zero
popq %rbp
retq
nopw %cs:(%rax,%rax)

Такая разница в количестве вычислений, чтоб получить одно и то же значение!

Как найти и справиться с нестабильностью типов

В этот момент вы можете спросить: "Ну, почему бы просто не использовать C, чтобы не приходилось искать эти нестабильности?" Ответ таков:

  1. Их легко найти
  2. Они могут быть полезны
  3. Вы можете справиться с нестабильностью с помощью функциональных барьеров

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

    @code_warntype g()

получим анализ

Variables: #self#::#g x::ANY #temp#@_3::Int64 i::Int64 #temp#@_5::Core.MethodInstance #temp#@_6::Float64 Body: begin x::ANY = 1 # line 3: SSAValue(2) = (Base.select_value)((Base.sle_int)(1,10)::Bool,10,(Base.box)(Int64,(Base.sub_int)(1,1)))::Int64 #temp#@_3::Int64 = 1 5: unless (Base.box)(Base.Bool,(Base.not_int)((#temp#@_3::Int64 === (Base.box)(Int64,(Base.add_int)(SSAValue(2),1)))::Bool)) goto 30 SSAValue(3) = #temp#@_3::Int64 SSAValue(4) = (Base.box)(Int64,(Base.add_int)(#temp#@_3::Int64,1)) i::Int64 = SSAValue(3) #temp#@_3::Int64 = SSAValue(4) # line 4: unless (Core.isa)(x::UNION,Float64)::ANY goto 15 #temp#@_5::Core.MethodInstance = MethodInstance for /(::Float64, ::Int64) goto 24 15: unless (Core.isa)(x::UNION{FLOAT64,INT64},Int64)::ANY goto 19 #temp#@_5::Core.MethodInstance = MethodInstance for /(::Int64, ::Int64) goto 24 19: goto 21 21: #temp#@_6::Float64 = (x::UNION{FLOAT64,INT64} / 2)::Float64 goto 26 24: #temp#@_6::Float64 = $(Expr(:invoke, :(#temp#@_5), :(Main./), :(x::Union{Float64,Int64}), 2)) 26: x::ANY = #temp#@_6::Float64 28: goto 5 30: # line 6: return x::UNION{FLOAT64,INT64} end::UNION{FLOAT64,INT64}

Он будет использовать любой тип, который не обозначен как strict type, то есть это абстрактный тип, который необходимо помещать в коробку / проверять на каждом шаге. Обратите внимание, что в начале мы говорим, что тип x — Any. Это говорит нам о том, что тип х изменился, вызвав трудности. Мы видим, что в конце мы возвращаем x как UNION {FLOAT64, INT64}, что является еще одним нестрогим типом. Если мы вместо этого посмотрим на code_warntype для h, мы получим все строгие типы:

@code_warntype h() Variables: #self#::#h x::Float64 #temp#::Int64 i::Int64 Body: begin x::Float64 = 1.0 # line 3: SSAValue(2) = (Base.select_value)((Base.sle_int)(1,10)::Bool,10,(Base.box)(Int64,(Base.sub_int)(1,1)))::Int64 #temp#::Int64 = 1 5: unless (Base.box)(Base.Bool,(Base.not_int)((#temp#::Int64 === (Base.box)(Int64,(Base.add_int)(SSAValue(2),1)))::Bool)) goto 15 SSAValue(3) = #temp#::Int64 SSAValue(4) = (Base.box)(Int64,(Base.add_int)(#temp#::Int64,1)) i::Int64 = SSAValue(3) #temp#::Int64 = SSAValue(4) # line 4: x::Float64 = (Base.box)(Base.Float64,(Base.div_float)(x::Float64,(Base.box)(Float64,(Base.sitofp)(Float64,2)))) 13: goto 5 15: # line 6: return x::Float64 end::Float64

Таким образом, нестабильность типов не трудно найти. Это указывает на то, что функция стабильна по типу и будет компилироваться по существу в оптимальный C-код. Зачем разрешать нестабильность типов? Что сложнее, так это найти правильный дизайн. Идея заключается в том, что во многих случаях вы хотите найти компромисс между производительностью и надежностью. Это давний вопрос, который привел к тому, что динамически типизированные языки доминируют на игровом поле сценариев.

В Julia вы можете написать свою функцию так, чтобы, если бы все они были целыми числами, она хорошо скомпилировалась, а если бы все они были числами с плавающей запятой, она также хорошо скомпилировалась. Например, вы можете прочитать таблицу с веб-страницы, в которой целые смешаны с числами с плавающей запятой. Это все еще будет работать. А если они смешанные? Но Джулия прямо скажет вам (через code_warntype), когда вы пожертвуете производительностью. Это гибкость / удобство, которое мы знаем и любим из такого языка, как Python / R.

Как справиться с нестабильностями типов

Прежде всего, если вам нравится что-то вроде C / Fortran, где ваши типы объявлены и не могут измениться (что обеспечивает стабильность типов), вы можете сделать это в Julia: Есть несколько способов справиться с нестабильностями типов.

local a::Int64 = 5

Но поскольку преобразование не будет автоматически округляться, оно, скорее всего, вызовет ошибки). Это делает a 64-разрядным целым числом, и если будущий код попытается изменить его, будет выдано сообщение об ошибке (или будет выполнено правильное преобразование. Менее сложный способ справиться с этим — с помощью утверждений типа. Посыпьте их вокруг своего кода, и вы получите стабильность типов, аля, C / Fortran. Например: Здесь вы помещаете тот же синтаксис на другую сторону знака равенства.

a = (b/c)::Float64

Если это не так, попробуйте выполнить автоматическое преобразование. Оно как бы говорит: "вычислите b / c и убедитесь, что на выходе есть Float64. Размещение таких конструкций поможет вам убедиться, что вы знаете, какие типы участвуют. Если преобразование не может быть легко выполнено, выведите ошибку". Например, допустим, вы хотите иметь надежный код, но пользователь дает вам что-то сумасшедшее, типа: Однако в некоторых случаях нестабильность типов необходима.

arr = Vector{Union{Int64,Float64}}(undef, 4)
arr[1]=4
arr[2]=2.0
arr[3]=3.2
arr[4]=1

Фактическим типом элемента для массива является Union {Int64, Float64}, который, как мы видели ранее, был нестрогим, что может привести к проблемам. что представляет собой массив 4x1 целых чисел и чисел с плавающей запятой. Это означает, что наивно выполнять арифметику с этим массивом, например: Компилятор знает только, что каждое значение может быть целым числом или числом с плавающей запятой, но не какой элемент какого типа.

function foo{T,N}(array::Array{T,N}) for i in eachindex(array) val = array[i] # do algorithm X on val end
end

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

function inner_foo{T<:Number}(val::T) # Do algorithm X on val
end function foo2{T,N}(array::Array{T,N}) for i in eachindex(array) inner_foo(array[i]) end
end

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

Хороший программист Julia получает в свое распоряжение и то, и другое, чтобы максимизировать производительность и / или производительность при необходимости. Таким образом, я надеюсь, что вы видите, что Юлия предлагает хорошее сочетание производительности строгой типизации и удобства динамической типизации.

Нежданчик 3: Eval работает на глобальном уровне

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

macro defa() :(a=5)
end

Код Джулии — это выражения, и, следовательно, метапрограммирование — это сборка выражений). заменит любой экземпляр defa на код a = 5 (:(a = 5) — это quoted expression.

Однако иногда вам может потребоваться непосредственно оценить сгенерированный код. Вы можете использовать это, чтобы построить любую сложную программу на Julia, которую пожелаете, и поместить ее в функцию как тип действительно умного сокращения. В общем, вы должны стараться избегать eval, но есть некоторые коды, где это необходимо, например, моя новая библиотека для передачи данных между различными процессами для параллельного программирования. Юлия дает вам функцию eval или макрос @eval для этого. Тем не менее, обратите внимание, что если вы используете его:

@eval :(a=5)

Однако исправление такое же, как и для глобальных нестабильностей / типов. тогда это будет оцениваться в глобальном масштабе (REPL). Например:

function testeval() @eval :(a=5) return 2a+5
end

Но мы можем, например, ввести локальную переменную и присвоить ей тип: не даст хорошего скомпилированного кода, так как a было по существу объявлено в REPL.

function testeval() @eval :(a=5) b = a::Int64 return 2b+5
end

Так что работать с eval не сложно, вы просто должны помнить, что он работает в REPL. Здесь b — это локальная переменная, и компилятор может сделать вывод, что ее тип не изменится, и, таким образом, мы имеем стабильность типов и живем в стране хорошей производительности.

Нежданчик 4: Как разбивать выражения

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

Просто убедитесь, что вы помните, как заканчиваются функции. Простое правило, верно? Например:

a = 2 + 3 + 4 + 5 + 6 + 7 +8 + 9 + 10+ 11+ 12+ 13
a

Почему? Похоже, что она будет оценивать до 90, но вместо этого дает 27. Поскольку a = 2 + 3 + 4 + 5 + 6 + 7 является полным выражением, поэтому оно будет иметь значение a = 27, а затем пропускается глупость +8 + 9 + 10+ 11+ 12+ 13, Чтобы продолжить строку, нам нужно было убедиться, что выражение не завершено:

a = 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10+ 11+ 12+ 13

Это может сбить вас с толку в первый раз, но потом привыкнете. Это даст 90, как мы хотели.

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

Например: Более сложная проблема связана с определениями массивов.

x = rand(2,2)
a = [cos(2*pi.*x[:,1]).*cos(2*pi.*x[:,2])./(4*pi) -sin(2.*x[:,1]).*sin(2.*x[:,2])./(4)] b = [cos(2*pi.*x[:,1]).*cos(2*pi.*x[:,2])./(4*pi) - sin(2.*x[:,1]).*sin(2.*x[:,2])./(4)]

Первый даст вам (2,2) матрицу, а второй — (1-мерный) вектор размера 2. с первого взгляда можно подумать, что a и b — это одно и то же, но это не так! Чтобы увидеть, в чем проблема, вот более простая версия:

a = [1 -2] b = [1 - 2]

Во втором есть выражение: 1-2. В первом случае есть два числа: 1 и -2. Обычно очень приятно писать: Это из-за специального синтаксиса для определений массива.

a = [1 2 3 -4 2 -3 1 4]

Тем не менее, за удобство приходится расплачиваться. и получать матрицу 2x4. Однако эту проблему также легко избежать: вместо конкатенации с использованием пробела используйте функцию hcat:

a = hcat(cos(2*pi.*x[:,1]).*cos(2*pi.*x[:,2])./(4*pi),-sin(2.*x[:,1]).*sin(2.*x[:,2])./(4))

Задача решена!

Подводный камень №5: Представления, Копирование и Глубокая копия

Представление (View) — виртуальная (логическая) таблица, представляющая собой поименованный запрос (алиас к запросу), который будет подставлен как подзапрос при использовании представления.

Содержимое представления динамически вычисляется на основании данных, находящихся в реальных таблицах. В отличие от обычных таблиц реляционной БД, представление не является самостоятельной частью набора данных, хранящегося в базе. Изменение данных в реальной таблице БД немедленно отражается в содержимом всех представлений, построенных на основании этой таблицы.

"Массив" на самом деле является представлением о непрерывном пятне памяти, которое используется для хранения значений. Один из способов добиться хороших результатов работы Джулии — работа с "представлениями". Это дает интересное (и полезное (иногда)) поведение. "Значение" массива — это указатель на ячейку памяти (и информация о его типе). Например, если мы запустим следующий код:

a = [3;4;5] b = a
b[1] = 1

е. тогда в конце мы получим, что a — это массив [1; 4; 5], т. Причина в том, что b = a устанавливает значение b равным значению a. изменение b изменяет a. Это очень полезно, так как позволяет сохранять один и тот же массив в разных формах. Поскольку значение массива является его указателем на ячейку памяти, то, что фактически получает b, не является новым массивом, скорее оно получает указатель на ту же ячейку памяти (именно поэтому изменение b меняет a). Например, мы можем иметь матрицу и векторную форму матрицы, используя:

a = rand(2,2) # Makes a random 2x2 matrix
b = vec(a) # Makes a view to the 2x2 matrix which is a 1-dimensional array

Обратите внимание, что все это время массивы не копировались, и поэтому эти операции были слишком дешевыми (то есть, нет причин избегать их в чувствительном к производительности коде). Теперь b является вектором, но изменение b все еще меняет a, где b индексируется путем считывания столбцов. Обратите внимание, что синтаксис для нарезки массива создаст копию в правой части. Теперь немного подробностей. Например:

c = a[1:2,1]

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

d = @view a[1:2,1] e = view(a,1:2,1)

(Другая функция, которая создает представления, — это reshape, которая позволяет изменять форму массива.) Если этот синтаксис находится слева, то это представление. И d, и e — это одно и то же, и изменение d или e изменит a, потому что оба не будут копировать массив, а просто создавать новые переменные, которые указывают только на первый столбец а. Например:

a[1:2,1] = [1;2]

Так как копировать-то? изменит a, потому что в левой части a[1:2,1] совпадает с view (a, 1:2,1), что указывает на ту же память, что и a. Ну, мы можем использовать функцию копирования:

b = copy(a)

Если мы уже определили a, есть удобная копия copy! Теперь, поскольку b является копией a, а не представлением, изменение b не изменит a. Но теперь давайте сделаем немного более сложный массив. (B, a) на месте, которая по существу будет проходить по циклу и записывать значения a в местоположения a (но для этого необходимо, чтобы b был уже определен и с правильным размером). Например, Vector {Vector}:

a = [ [1, 2, 3], [4, 5], [6, 7, 8, 9] ]

Что происходит, когда мы копируем? Каждый элемент а является вектором.

b = copy(a)
b[1][1] = 10
a

3-element Array{Array{Int64,1},1}: [10, 2, 3] [4, 5] [6, 7, 8, 9]

Почему это случилось? Обратите внимание, что это также изменит a[1][1] на 10! Но значения a были массивами, поэтому мы скопировали указатели в ячейки памяти на b, поэтому b фактически указывает на те же массивы. Мы использовали copy для копирования значений a. Чтобы исправить это, используем deepcopy:

b = deepcopy(a)

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

Головная боль №6: Временные распределения, векторизация и функции In-Place

В Julia вы, возможно, слышали, что "девекторизованный код лучше". В MATLAB / Python / R вам советуют использовать векторизацию. е. Векторизованные коды дают временные выделения (т. По этой причине вы захотите объединить ваши векторизованные операции и записать их на месте (in-place), чтобы избежать распределения. они создают промежуточные массивы, которые не нужны, и, как отмечалось ранее, выделения массивов являются дорогостоящими и замедляют ваш код). Функция in-place (mutable function) — это функция, которая обновляет значение, а не возвращает значение. Что я имею в виду на месте? Например, если вы написали: Если вы собираетесь постоянно работать с массивом, это позволит вам продолжать использовать один и тот же массив вместо создания новых массивов на каждой итерации.

function f() x = [1;5;6] for i = 1:10 x = x + inner(x) end return x
end
function inner(x) return 2x
end

Очевидно, нам не нужно продолжать создавать новые массивы. затем каждый раз, когда вызывается inner, он будет создавать новый массив, который будет возвращать 2x. Таким образом, вместо этого у нас может быть кэш-массив y, который будет содержать вывод примерно так:

function f() x = [1;5;6] y = Vector{Int64}(3) for i = 1:10 inner(y,x) for i in 1:3 x[i] = x[i] + y[i] end copy!(y,x) end return x
end
function inner!(y,x) for i=1:3 y[i] = 2*x[i] end nothing
end

inner!(y, x) ничего не возвращает, но меняет y. Копнём глубже. (y, x) будет молча изменять значения у. Поскольку y является массивом, значение y является указателем на фактический массив, и, поскольку в функции эти значения были изменены, inner! Они обычно обозначаются знаком ! И обычно меняют первый аргумент (это просто так условились). Функции, которые делают это, называются mutable (ну, допустим "мутаторы").

Точно так же copy!(y, x) — это встроенная функция, которая записывает значения x в y, обновляя последний. Таким образом, при вызове inner!(y, x) массив не выделяется. Создаются только два массива: начальный массив для x и начальный массив для y. Как видите, это означает, что каждая операция изменяет только значения массивов. Поскольку распределение массивов стоит дорого, вторая функция будет работать быстрее. Первая функция создавала новый массив каждый раз, когда вызывался x + inner(x), и, таким образом, в первой функции было создано 11 массивов.

Вот тут-то и происходит слияние петель (loop-fusion). Хорошо, что мы можем работать быстро, но синтаксис немного раздулся, когда нам пришлось выписывать циклы. 5 вы можете использовать . символ для векторизации любой функции (также известной как широковещание (broadcast), потому что она на самом деле вызывает функцию широковещания). Начиная с Julia v0. Если вы просто применили f к x и создали новый массив, то x = x + f. Хотя круто, что f.(x) — это то же самое, что применять f к каждому значению x, но круче то, что циклы сливаются. Однако вместо этого мы можем обозначить все как векторизованные операции: (x) будет иметь копию.

x .= x .+ f.(x)

.= Будет делать поэлементное равенство, так что это, по сути, станет кодом

for i = 1:length(x) x[i] = x[i] + f(x[i])
end

Таким образом, другой способ написать нашу функцию был бы:

function f() x = [1;5;6] for i = 1:10 x .= x .+ inner.(x) end return x
end
function inner(x) return 2x
end

Вот как вы можете использовать синтаксис языка сценариев, но при этом получать скорость, подобную C / Fortran. Поэтому мы все еще получаем краткий векторизованный синтаксис MATLAB / R / Python, но эта версия не создает временных массивов и, следовательно, будет быстрее.

Вывод: выучите правила, поймите их и катайтесь как сыр в масле

Изучите правила хорошо, и все это будет второй натурой. Еще раз повторим: у Юлии нет магии компилятора, только простые правила. В первый раз, когда вы сталкиваетесь с ошибкой, сложно понять откуда счастья привалило. Я надеюсь, что это поможет вам при знакомстве с Джулией. Но как только вы разберетесь, то уже будете готовы.

Соедините это с фьюжн-трансляцией и метапрограммированием, и вы получите безумную производительность для объема кода, который вы пишете! Как только вы по-настоящему поймете эти правила, ваш код будет скомпилирован до C / Fortran, в то время как он написан на кратком высокоуровневом языке сценариев.

Оставьте комментарий, объясняющий нежданчик и как с этим справиться. Вот вам вопрос: какие выкидоны Джулии я пропустил? [Моим должен быть тот факт, что в Javascript внутри функции var x = 3 делает x локальным, а x = 3 делает x глобальным. Кроме того, ради интереса, какие ваши любимые ошибки из других языков? Это дало некоторые безумные ошибки, из-за которых я больше не хочу использовать Javascript!] Автоматические глобалы внутри функций?


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

[Перевод] История транзистора, часть 2: из горнила войны

Другие статьи цикла: История реле История электронных компьютеров История транзистора Горнило войны подготовило почву для появления транзистора. С 1939 по 1945 года технические знания из области полупроводников невероятно сильно разрослись. И тому была одна простая причина: радар. Самая важная технология ...

Что можно сделать через разъем OBD в автомобиле

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