常见问题
普通
你为什么不编译Matlab/Python/R/。.. 朱莉娅的密码?
由于许多人熟悉其他动态语言的语法,并且已经用这些语言编写了大量代码,因此很自然地想知道为什么我们不简单地将Matlab或Python前端连接到Julia后端(或将代码"转译"到Julia),以便在不需要程序员学习新语言的情况下获得Julia的所有性能优势。 很简单,对吧?
主要问题是Julia编译器没有什么特别之处:我们使用常规编译器(LLVM),它包含使用其他语言的开发人员不会知道的任何内容。 事实上,Julia编译器在许多方面比其他动态语言(例如,PyPy或LuaJIT)的编译器简单得多。 Julia的性能优势几乎主要源于这种语言的前端:语言的语义允许 Julia中写得很好的程序_为编译器提供额外的capabilities_以创建高效的代码和内存模板。 如果您尝试在Julia中编译Matlab或Python代码,我们的编译器将受到Matlab或Python语义的限制,并且生成的代码不会比这些语言的现有编译器更好(甚至可能更糟)。 语义的关键作用也解释了为什么几个现有的Python编译器(例如,Numba和Pythran)试图只优化语言的一小部分(例如,Numpy数组和标量操作),并且对于这个子集,它们至少已经 从事这些项目的人非常聪明,创造了美好的东西,但是修改一个被设计为被解释的语言的编译器是一个非常困难的任务。
Julia语言的优点是,良好的性能不仅限于"内置"类型和操作的一小部分,它允许您创建在任意用户定义类型上运行的高级类型范围代码,同时保持快速和内存 像Python这样的语言中的类型根本没有为编译器提供足够的信息来提供类似的功能,所以一旦你开始使用这些语言作为Julia的前端,就会出现障碍。
出于类似的原因,自动翻译到Julia通常也会生成不可读的,缓慢的,非惯用的代码,这对于来自另一种语言的本地Julia端口来说不是一个好的开始。
另一方面,语言的_interoperation_非常有用:我们需要在其他语言中使用高质量的Julia代码(反之亦然)! 确保这一点的最好方法不是使用"转译器",而是在语言之间应用简单的调用方法。 我们正在努力工作,从内置的ccall工具(用于调用C和Fortran库)开始,以包结束。 https://github.com/JuliaInterop [JuliaInterop],将Julia连接到Python,Matlab,C++ 等等。
公众空气污染指数
Julia如何定义其公共API?
公开提供https://en.wikipedia.org/wiki/API Julia的[API]是来自基础库和标准库的公共符号文档中描述的行为。 函数、类型和常量不是公共API的一部分,除非它们是公开可用的,即使它们有文档字符串或在文档中描述。 此外,只有公共符号的记录行为是公共API的一部分。 公共符号的无证行为是一个内部特征。
公共字符是标记为"公共foo"或"导出foo"的字符。
换句话说,
-
公共符号的记录行为是公共API的一部分。;
-
公共符号的未记录行为不是公共API的一部分。;
-
私有符号的记录行为不是公共API的一部分。;
-
私有符号的未记录行为不是公共API的一部分。
公共符号的完整列表可以在"名称(MyModule)"模块中找到。
鼓励包作者以类似的方式定义他们的公共Api。
Julia公共API中的所有组件都包含在文档中https://semver.org /[SemVer],因此在Julia2.0发布之前,它们不会被删除或显着更改。
有一个有用的未记录函数/类型/常量。 我可以使用它们吗?
如果您使用非公开可用的API,Julia更新可能会破坏您的代码。 如果代码是自包含的,则将其复制到项目中很有用。 如果你想依赖一个复杂的,非公开可用的API,特别是从一个稳定的包中使用它时,打开它很有用https://github.com/JuliaLang/julia/issues [问题]或https://github.com/JuliaLang/julia/pulls [拉取请求]开始讨论将其转换为公共API。 但是,我们并没有阻止创建提供稳定公共接口的包的尝试,依赖于非公开可用的Julia实现细节并消除不同版本Julia之间的差异。
会议及REPL
如何删除内存中的对象?
在Julia语言中没有MATLAB’clear’函数的模拟;一旦名称在Julia会话中定义(技术上在`Main`模块中),它就始终存在。
如果您担心内存使用情况,则始终可以使用消耗较少内存的其他对象替换对象。 例如,如果`A’是您不再需要的大小为千兆字节的数组,则可以使用`a=nothing’释放内存。 内存将在下次启动垃圾回收器时释放;您可以通过使用强制执行此操作 'GC。gc()`。 此外,尝试使用’A’极有可能导致错误,因为大多数方法都没有在`Nothing`类型中定义。
如何更改会话中的类型声明?
您可能已经确定了类型,然后意识到您需要添加一个新字段。 如果你在REPL中尝试这个,你会得到一个错误:
ERROR: invalid redefinition of constant MyType
不能重新定义`Main’模块中的类型。
虽然这在开发新代码时可能不方便,但有一个很好的解决方法。 模块可以通过重新定义它们来替换,如果您将所有新代码放在模块中,则可以重新定义类型和常量。 您不能将类型名称导入到`Main`中,然后期望能够在那里重新定义它们,但您可以使用模块名称来解析范围。 换句话说,在开发时,您可以使用类似这样的工作流程:
include("mynewcode.jl") # это определяет модуль MyModule
obj1 = MyModule.ObjConstructor(a, b)
obj2 = MyModule.somefunction(obj1)
# Получаем ошибку. Меняем что-то в mynewcode.jl
include("mynewcode.jl") # перезагружаем модуль
obj1 = MyModule.ObjConstructor(a, b) # старые объекты более не действительны, необходимо повторное конструирование
obj2 = MyModule.somefunction(obj1) # в этот раз все сработало!
obj3 = MyModule.someotherfunction(obj2, c)
...
剧本
如何验证当前文件是否作为主脚本运行?
当文件作为主脚本使用`julia文件执行时。jl`,您可能希望激活附加功能,例如命令行参数处理。 要确定以这种方式执行文件,请确保’abspath(PROGRAM_FILE)==@__FILE__+`具有值`true'。
但是,建议不要编写执行脚本和导入库功能的文件。 如果您需要既可作为库也可作为脚本使用的功能,最好将其编写为库,然后将其导入到单独的脚本中。
如何拦截脚本中的CTRL-C?
当您尝试完成Julia脚本的执行时,从julia文件开始。jl`,使用键盘快捷键CTRL+C(SIGINT),异常 'InterruptException'。 要在Julia脚本停止执行之前运行某些代码,这可能是由CTRL-C引起的,或者可能是其他原因,请使用 'atexit'。 或者,您可以使用’julia-e’include(popfirst!(ARGS))'文件。jl’执行脚本并拦截块中的’InterruptException' '尝试'。 请记住,使用此策略 'PROGRAM_FILE'不会被设置。
如何使用`#传递`julia`的参数!/usr/bin`env'?
在所谓的shebang字符串中向julia传递参数,例如'#!/usr/bin/env julia—startup-file=no',在许多平台(bsd,macOS,Linux)上不起作用,其内核与shell不同,不会用空格字符分隔参数。 一个简单的解决方法是使用’env-S’参数,其中参数字符串由空格分隔为几个单独的参数,就像在shell中发生的那样。:
#!/usr/bin/env -S julia --color=yes --startup-file=no
@show ARGS # вставьте сюда любой код Julia
'Env-S’参数首次出现在FreeBSD6.0(2005)、macOS Sierra(2016)和GNU/Linux coreutils8.30(2018)中。 |
为什么’run’不支持`*'或管道从脚本运行外部程序?
Julia函数 'run'直接运行外部程序,无需调用https://en.wikipedia.org/wiki/Shell_ (计算)[操作系统shell](不像函数'system("。..")在其他语言中,如Python,R或C)。 这意味着’run’不执行通配符扩展。 `*
(https://en.wikipedia.org/wiki/Glob_ (编程)["globbing"]),并且也没有解释https://en.wikipedia.org/wiki/Pipeline_ (Unix)类型为`|或
>'的[shell pipelines]。
但是,您仍然可以使用Julia函数使用globbing和pipelines。 例如,内置函数 'pipeline'允许您以与shell pipeline相同的方式链接外部程序和文件,并且https://github.com/vtjnash/Glob.jl [格洛布。jl包]实现POSIX兼容的globbing。
当然,您可以通过shell显式地将shell和命令字符串传递给`run`来运行程序,例如$run(`sh-c"ls>files。txt"')$使用https://en.wikipedia.org/wiki/Bourne_shell [Bourne shell]Unix,但您通常应该使用真正的Julia脚本,例如$run(pipeline('ls',"files.txt"))$。 我们默认避免使用shell的原因是https://julialang.org/blog/2012/03/shelling-out-sucks /[这是由于许多缺点]:通过shell运行进程速度慢,涉及引用特殊字符,错误处理不好,可移植性存在问题。 (Python开发人员来到https://www.python.org/dev/peps/pep-0324/#motivation [得出类似的结论]。)
变量和赋值
为什么我从一个简单的循环中得到一个`UndefVarError'?
你可能有这样的代码:
x = 0 while x < 10 x += 1 end
并且您注意到它在交互式环境(例如Julia REPL)中工作正常,但是当您尝试在脚本或其他文件中执行此代码时,您会得到一个Undef UndefVarError:`x`not defined$。 这是因为Julia通常要求您*显式地为局部作用域*中的全局变量赋值。
这里’x’是一个全局变量,'while’定义 局部区域,并且`x+=1`是对该局部区域中的全局变量赋值。
如上所述,Julia(1.5或更高版本)允许您在REPL(和许多其他交互式环境)中省略代码的`global`关键字,以简化学习(例如,从用于交互式启动的函数中复制和粘贴代码)。 但是,当您转到文件中的代码时,Julia需要对全局变量采用更有纪律的方法。 你至少有三个选择。
其他解释可以在手册部分找到. 关于软作用球。
功能
我将参数`x`传递给函数,在这个函数内部更改了它,但在变量`x`之外仍然保持不变。 为啥?
假设你正在调用一个函数,如下所示:
julia> x = 10
10
julia>函数change_value!(y)
y=17
结束
change_value! (泛型功能与1方法)
julia>change_value!(x)
17
julia>x#x保持不变!
10
在Julia中,变量`x`的绑定不能通过将`x`作为参数传递给函数来改变。 当调用’change_value!(x)`在上面的例子中,`y`是一个新创建的变量,最初与`x`的值相关联,即'10'。 然后’y’重新与常数`17’相关联,而外部区域的变量`x`保持不变。
但是,如果`x`链接到`Array`类型(或任何其他_change_类型)的对象,则不能从此函数从此数组中"取消链接"`x`,但可以更改其内容。 例如:
julia> x = [1,2,3]
3-element Vector{Int64}:
1
2
3
julia> function change_array!(A)
A[1] = 5
end
change_array! (generic function with 1 method)
julia> change_array!(x)
5
julia> x
3-element Vector{Int64}:
5
2
3
在这里,我们创建了’change_array!'函数,它将值`5`赋给传递数组的第一个元素(在调用位置与`x`相关联,并在函数中与`A`相关联)。 请注意,在调用函数后’x’仍然链接到同一个数组,但该数组的内容已被更改:变量`A`和`x`是指向同一个可变`数组`对象的单独链接。
我可以在函数中使用`using`或`import’吗?
不,禁止在函数内使用表达式’using’或’import'。 如果你想导入一个模块,但只在一个特定的函数或一组函数中使用它的符号,你有两个选择。
-
使用"导入":
import Foo function bar(...) # …ссылка на символы Foo через Foo.baz… end
此代码加载’Foo’模块并定义引用该模块的`Foo’变量,但不会将模块中的任何其他符号导入当前命名空间。 你指的是字符’Foo`由他们的限定名称’Foo。巴`等。
-
将您的函数包装在模块中:
module Bar
export bar
using Foo
function bar(...)
# …ссылка на Foo.baz как просто на baz…
end
end
using Bar
此代码从`Foo’导入所有字符,但仅在`Bar`模块内。
什么是运营商'。..'做?
`...'在函数定义中将多个参数组合为一个
在定义函数的上下文中,运算符是'。..'用于将许多不同的参数组合成一个参数。 使用''。..'将许多不同的参数组合成一个参数称为"合并":
julia> function printargs(args...)
println(typeof(args))
for (i, arg) in enumerate(args)
println("Arg #$i = $arg")
end
end
printargs (generic function with 1 method)
julia> printargs(1, 2, 3)
Tuple{Int64, Int64, Int64}
Arg #1 = 1
Arg #2 = 2
Arg #3 = 3
如果在Julia中更自由地使用ASCII字符,则合并运算符可以完全写成'<-。..'而不是'。..`.
`...'在函数调用中将一个参数拆分为许多不同的参数
不像使用运算符'。..',运算符''用于表示在定义函数时将许多不同的参数合并为一个参数。..在函数调用的上下文中应用时,还用于将单个函数参数拆分为许多不同的参数。 这是
的使用。..'被称为"分离":
julia> function threeargs(a, b, c)
println("a = $a::$(typeof(a))")
println("b = $b::$(typeof(b))")
println("c = $c::$(typeof(c))")
end
threeargs (generic function with 1 method)
julia> x = [1, 2, 3]
3-element Vector{Int64}:
1
2
3
julia> threeargs(x...)
a = 1::Int64
b = 2::Int64
c = 3::Int64
如果在Julia中更自由地使用ASCII字符,则除法运算符可以写成''。..->,而不是'。..
.
赋值的返回值是多少?
`='运算符总是返回表达式的右侧,因此:
julia> function threeint()
x::Int = 3.0
x # возвращает переменную x
end
threeint (generic function with 1 method)
julia> function threefloat()
x::Int = 3.0 # возвращает 3,0
end
threefloat (generic function with 1 method)
julia> threeint()
3
julia> threefloat()
3.0
同样地:
julia> function twothreetup()
x, y = [2, 3] # присваивает значения 2 – x, а 3 – y
x, y # возвращает кортеж
end
twothreetup (generic function with 1 method)
julia> function twothreearr()
x, y = [2, 3] # возвращает массив
end
twothreearr (generic function with 1 method)
julia> twothreetup()
(2, 3)
julia> twothreearr()
2-element Vector{Int64}:
2
3
类型、声明和构造函数
"类型稳定"是什么意思?
这意味着基于输入数据的类型来预测输出数据的类型。 特别是,这意味着输出数据的类型不能根据输入数据的值而改变。 以下代码对于类型不稳定:
julia> function unstable(flag::Bool)
if flag
return 1
else
return 1.0
end
end
unstable (generic function with 1 method)
它返回一个’Int’或 'Float64'取决于其参数的值。 由于Julia无法在编译时预测函数返回值的类型,因此使用它的任何计算都必须能够处理这两种类型的值,这使得创建快速机器代码变得困难。
为什么Julia会为一些看似有意义的操作发出`DomainError`?
有些操作具有数学意义,但它们失败了。:
julia> sqrt(-2.0)
ERROR: DomainError with -2.0:
sqrt was called with a negative real argument but will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
Stacktrace:
[...]
这种行为是类型的稳定性要求带来的不便。 的情况下 `sqrt'大多数用户希望’sqrt(2.0)`产生一个实数,如果它产生一个复数'1.4142135623730951+0.0im',就会不高兴。 写一个函数是可能的 `sqrt'仅在传输负数时切换到包含复数的输出数据(这是什么 'sqrt'在其他一些语言中也是如此),但在这种情况下,结果不会是 稳定的类型,并为功能 'sqrt'将具有较差的性能。
在这种情况和其他情况下,您可以通过选择输入数据类型来获得所需的结果,该输入数据类型传达了您接受输出数据类型的意愿,该输出数据类型可:
julia> sqrt(-2.0+0im)
0.0 + 1.4142135623730951im
如何限制或计算类型参数?
参数 参数类型可以包含类型或位值,类型本身决定它如何使用这些参数。 例如,'数组{Float64, 2}`由类型`Float64’参数化以表示其元素类型,整数值'2’表示其维度的数量。 在定义自己的参数类型时,可以使用子类型约束来声明某个参数必须是子类型(`<:')某种抽象类型或以前的类型参数。 但是,没有专用语法来声明参数必须是此类型的_value,这意味着您不能显式声明例如维度参数 'isa'是’struct’定义中的’Int'。 同样,您不能使用类型参数执行计算(包括简单的操作,如加法或减法)。 此外,这些类型的约束和关系可以通过在 类型构造函数。
例如,考虑以下内容。
struct ConstrainedType{T,N,N+1} # ПРИМЕЧАНИЕ. НЕДОПУСТИМЫЙ СИНТАКСИС
A::Array{T,N}
B::Array{T,N+1}
end
用户希望强制执行第三种类型的参数始终是第二个加一个。 这可以通过检查显式类型参数来实现。 内部构造方法(其中它可以与其他检查组合):
struct ConstrainedType{T,N,M}
A::Array{T,N}
B::Array{T,M}
function ConstrainedType(A::Array{T,N}, B::Array{T,M}) where {T,N,M}
N + 1 == M || throw(ArgumentError("second argument should have one more axis" ))
new{T,N,M}(A, B)
end
end
此检查通常不昂贵,因为编译器可能会省略对有效特定类型的检查。 如果第二自变量也被计算,则可能有利的是提供 外部构造函数方法:
ConstrainedType(A) = ConstrainedType(A, compute_B(A))
为什么Julia使用自己的机器整数算术?
Julia使用机器算术来计算整数。 这意味着Int值的范围是有限的,并且在两端环绕,以便整数的加法,减法和乘法可能导致溢出或反溢出,产生一些乍一看可能令人困惑的结果。:
julia> x = typemax(Int)
9223372036854775807
julia> y = x+1
-9223372036854775808
julia> z = -y
-9223372036854775808
julia> 2*z
0
显然,这与数学整数的行为方式有很大不同,您可能会认为高级编程语言向用户炫耀这一点远非理想。 然而,对于对数字的工作,效率和透明度的需求很高,替代方案甚至更糟。
一种可能的替代方法是检查每个整数操作是否溢出,并将结果提升为大整数类型,例如 'Int128'或 `BigInt'在溢出的情况下。 不幸的是,这导致每个整数操作的严重成本(想想周期计数器中的增量)-它需要代码生成在算术指令和分支处理潜在溢出之后在运行时执行溢出检查。 更糟糕的是,它使每个使用整数类型的计算都不稳定。 正如我们之前提到的, 类型稳定性很重要用于高效的代码生成。 如果您不能确定整数运算的结果将是整数,那么就不可能像C和Fortran编译器那样生成快速简单的代码。
这种方法的一个变体,避免了类型不稳定的出现,是合并类型’Int’和 `BigInt'转换为一个混合整数类型,当结果不再适合机器整数的大小时,它会在内部更改表示形式。 虽然这种方法乍一看有助于避免Julia代码级别的类型不稳定,但实际上它会导致掩盖问题,将所有相同的困难转移到实现这种混合整数类型的C代码。 使用这种方法,可以使代码工作,并且在许多情况下相当快,但也有一些缺点。 一个问题是整数和整数数组的内存中表示不再对应于c,Fortran和其他具有本机机器整数的语言使用的自然表示。 因此,为了与这些语言交互,我们最终需要引入我们自己的整数类型。 整数的任何无限表示都不能具有固定数量的位,因此不能嵌入到具有固定大小插槽的数组中-大整数值将始终需要位于动态内存中的单独存储。 而且,当然,无论您使用混合整数类型的实现有多智能,总有性能"陷阱"-性能意外恶化的情况。 复杂的表示、与C和Fortran的互操作性不足、在动态内存中没有额外存储的情况下无法表示整数数组以及不可预测的性能特性,使得即使是最智能的混合整数类型的实现也成为高性能数字工作的不良选择。
使用混合整数类型或前进到BigInts的替代方法是使用饱和整数算术,其中具有最大整数值的加法使其保持不变(并且类似地用于从最小整数值减去)。 这正是Matlab所做的。
>> int64(9223372036854775807) ans = 9223372036854775807 >> int64(9223372036854775807) + 1 ans = 9223372036854775807 >> int64(-9223372036854775808) ans = -9223372036854775808 >> int64(-9223372036854775808) - 1 ans = -9223372036854775808
乍一看,这似乎足够合理,因为9223372036854775807比—9223372036854775808更接近9223372036854775808,并且整数仍然以与C和Fortran兼容的自然方式用固定大小表示。 然而,饱和整数算术存在许多问题。 第一个也是最明显的问题是,这样的算术不像机器整数算术那样工作,因此实现饱和运算需要在每个机器操作后用整数创建指令,以检查过度填充或溢 'typemin(Int)或 'typemax(Int)视情况而定。 仅这一点就可以将每个整数操作从一个简单,快速的指令变成六个指令,可能涉及分支。 哎哟。 但更糟糕的是,饱和整数算术不是关联的。 考虑以下Matlab计算。
>> n = int64(2)^62 4611686018427387904 >> n + (n - 1) 9223372036854775807 >> (n + n) - 1 9223372036854775806
Это осложняет написание многих базовых алгоритмов работы с целыми числами, так как многие распространенные методики основаны на том, что машинное сложение с переполнением является ассоциативным. Рассмотрим нахождение срединной точки между целочисленными значениями lo
и hi
в Julia с использованием выражения (lo + hi) >>> 1
:
julia> n = 2^62
4611686018427387904
julia> (n + 2n) >>> 1
6917529027641081856
Понимаете? Нет проблем. Это правильная срединная точка между 2^62 и 2^63, несмотря на тот факт, что n + 2n
равно --4611686018427387904. Теперь попробуем это в Matlab:
>> (n + 2*n)/2 ans = 4611686018427387904
Ой. Добавление оператора >>>
в Matlab не поможет, потому что насыщение, которое происходит при сложении n
и 2n
, уже уничтожило информацию, необходимую для вычисления правильной срединной точки.
И дело не только в отсутствии ассоциативности, из-за чего программисты не могут использовать насыщенную арифметику в подобных методиках. Она еще сводит на нет почти все, что компиляторы могли бы сделать для оптимизации целочисленной арифметики. Например, так как целые числа в Julia используют нормальную машинную целочисленную арифметику, LLVM имеет возможность агрессивно оптимизировать простые маленькие функции типа f(k) = 5k-1
. Машинный код для этой функции следующий.
julia> code_native(f, Tuple{Int})
.text
Filename: none
pushq %rbp
movq %rsp, %rbp
Source line: 1
leaq -1(%rdi,%rdi,4), %rax
popq %rbp
retq
nopl (%rax,%rax)
Фактически тело функции — это простая инструкция leaq
, которая одновременно вычисляет умножение и сложение целых чисел. Когда f
оказывается встроенной в другую функцию, это даже еще полезней:
julia> function g(k, n)
for i = 1:n
k = f(k)
end
return k
end
g (generic function with 1 methods)
julia> code_native(g, Tuple{Int,Int})
.text
Filename: none
pushq %rbp
movq %rsp, %rbp
Source line: 2
testq %rsi, %rsi
jle L26
nopl (%rax)
Source line: 3
L16:
leaq -1(%rdi,%rdi,4), %rdi
Source line: 2
decq %rsi
jne L16
Source line: 5
L26:
movq %rdi, %rax
popq %rbp
retq
nop
Так как вызов f
становится встроенным, тело цикла заканчивается инструкцией leaq
. Далее рассмотрим, что происходит, если мы исправляем число итераций цикла:
julia> function g(k)
for i = 1:10
k = f(k)
end
return k
end
g (generic function with 2 methods)
julia> code_native(g,(Int,))
.text
Filename: none
pushq %rbp
movq %rsp, %rbp
Source line: 3
imulq $9765625, %rdi, %rax # imm = 0x9502F9
addq $-2441406, %rax # imm = 0xFFDABF42
Source line: 5
popq %rbp
retq
nopw %cs:(%rax,%rax)
Так как компилятор знает, что сложение и умножение целых чисел является ассоциативным и что умножение имеет приоритет над сложением (и то, и другое неверно для насыщенной арифметики), это может оптимизировать весь цикл, сведя его к простому умножению и сложению. Насыщенная арифметика полностью сводит на нет этот вид оптимизации, так как может привести к сбою ассоциативности и дистрибутивности на каждой итерации цикла, приводя к различным результатам в зависимости от того, на какой итерации произошел сбой. Компилятор может свернуть цикл, но не может алгебраически свести множество операций к меньшему количеству эквивалентных операций.
Наиболее разумной альтернативой использования целочисленной арифметики для автоматического переполнения является повсеместное применение арифметики с проверкой, выдающей ошибки, когда операции сложения вычитания и умножения приводят к переполнению, производя значения, не являющиеся корректными для значения. В этом посте в блоге Дэн Луу (Dan Luu) анализирует эту проблему и приходит к выводу, что вместо очевидных затрат, к которым должен приводить этот подход в теории, в действительности он приводит к существенным затратам из-за того, что компиляторы (LLVM и GCC) небезопасно оптимизируют проверки переполнения при сложении. Если это будет улучшено в будущем, мы сможем рассмотреть использование в Julia целочисленной арифметики с проверками по умолчанию, но на данный момент мы вынуждены учитывать возможность переполнения.
В настоящее же время безопасные, не приводящие к переполнению операции с целыми числами можно выполнять с помощью внешних библиотек, таких как SaferIntegers.jl. Обратите внимание, что, как указывалось ранее, использование этих библиотек существенно увеличивает время выполнения кода с использованием целочисленных типов с проверкой. Однако при ограниченном использовании это является проблемой в значительно меньшей степени, чем если бы они использовались для всех операций с целыми числами. Вы можете следить за состоянием обсуждения здесь.
Каковы возможные причины появления UndefVarError
во время удаленного выполнения?
Как говорит сообщение об ошибке, непосредственной причиной UndefVarError
на удаленном узле является то, что связь по этому имени не существует. Изучим некоторые возможные причины.
julia> module Foo
foo() = remotecall_fetch(x->x, 2, "Hello")
end
julia> Foo.foo()
ERROR: On worker 2:
UndefVarError: `Foo` not defined in `Main`
Stacktrace:
[...]
Закрытие x->x
несет ссылку на Foo
, а так как Foo
недоступна на узле 2, выдается UndefVarError
.
Глобальные переменные в модулях, отличных от Main
, не сериализуются по значению в удаленный узел. Отправляется только ссылка. Функции, которые создают глобальные связи (за исключением находящихся в модуле Main
), могут позже привести к выдаче UndefVarError
.
julia> @everywhere module Foo
function foo()
global gvar = "Hello"
remotecall_fetch(()->gvar, 2)
end
end
julia> Foo.foo()
ERROR: On worker 2:
UndefVarError: `gvar` not defined in `Main.Foo`
Stacktrace:
[...]
В примере выше @everywhere module Foo
определил Foo
на всех узлах. Однако вызов Foo.foo()
создал новую глобальную связь gvar
на локальном узле, но она не была обнаружена на узле 2, что привело к ошибке UndefVarError
.
Обратите внимание, что это не относится к глобальным переменным, созданным в модуле Main
. Глобальные переменные в модуле Main
сериализуются, и новые связи создаются в модуле Main
на удаленном узле.
julia> gvar_self = "Node1"
"Node1"
julia> remotecall_fetch(()->gvar_self, 2)
"Node1"
julia> remotecall_fetch(varinfo, 2)
name size summary
––––––––– –––––––– –––––––
Base Module
Core Module
Main Module
gvar_self 13 bytes String
Это не относится к объявлениям function
или struct
. Однако анонимные функции, связанные с глобальными переменными, сериализуются, как видно ниже.
julia> bar() = 1
bar (generic function with 1 method)
julia> remotecall_fetch(bar, 2)
ERROR: On worker 2:
UndefVarError: `#bar` not defined in `Main`
[...]
julia> anon_bar = ()->1
(::#21) (универсальная функция с 1 методом)
julia> remotecall_fetch(anon_bar, 2)
1
Устранение неполадок «метод не совпадает»: инвариантность параметрических типов и ошибки MethodError
Почему не получится объявить foo(bar::Vector{Real}) = 42
, а затем вызвать foo([1])
?
Как видно, если попробовать выполнить этот код, результатом будет MethodError
:
julia> foo(x::Vector{Real}) = 42
foo (generic function with 1 method)
julia> foo([1])
ERROR: MethodError: no method matching foo(::Vector{Int64})
The function `foo` exists, but no method is defined for this combination of argument types.
Closest candidates are:
foo(!Matched::Vector{Real})
@ Main none:1
Stacktrace:
[...]
Это происходит, потому что Vector{Real}
не является супертипом Vector{Int}
! Вы можете решить эту проблему, используя что-то вроде foo(bar::Vector{T}) where {T<:Real}
(или краткую форму foo(bar::Vector{<:Real})
, если статический параметр T
не требуется в теле функции). T
— это подстановочный символ: сначала следует указать, что он должен иметь подтип Real, а затем указать, что функция принимает Vector с элементами этого типа.
Та же самая проблема существует для любого составного типа Comp
, не только для Vector
. Если Comp
имеет объявленный параметр типа Y
, то другой тип Comp2
с параметром типа X<:Y
не является подтипом Comp
. Это инвариантность типа (в противоположность этому кортеж представляет собой ковариант типа в своих параметрах). Дополнительные объяснения см. в разделе Параметрические составные типы.
Почему Julia использует *
для конкатенации строк? Почему не +
или что-то другое?
Основной аргумент против +
состоит в том, что конкатенация строк не является коммутативной, в то время как +
обычно используется как коммутативный оператор. Хотя сообщество Julia признает, что другие языки используют различные операторы и символ *
может быть не знаком некоторым пользователям, он передает определенные алгебраические свойства.
Имейте в виду, что вы также можете использовать string(...)
для конкатенации строк (и других значений, преобразованных в строки); аналогично, можно использовать repeat
вместо ^
для повтора строк. Синтаксис интерполяции также полезен для конструирования строк.
Пакеты и модули
В чем разница между using и import?
Между using
и import
есть несколько различий (см. раздел Модули). Но также существует одно важное отличие, которое, на первый взгляд, может показаться неинтуитивным и с точки зрения синтаксиса очень незначительным. При загрузке модуля с помощью using
вы должны употребить function Foo.bar(...
, чтобы расширить функцию bar
модуля Foo
новым методом, а вместе с import Foo.bar
вы должны употребить только function bar(...
, и функция bar
модуля Foo
будет автоматически обновлена.
Причина, по которой довольно важно иметь для этого отдельный синтаксис, состоит в том, что вам не нужно случайно расширять функцию, о существовании которой вы не знали, потому что это может легко привести к ошибке. Наиболее вероятно, что это может произойти с методом, который принимает общий тип, например строку или целое число, потому что как вы, так и другой модуль могли определить метод для обработки такого общего типа. Если вы используете import
, то замените реализацию bar(s::AbstractString)
другого модуля вашей новой реализацией, которая может легко делать что-то совершенно другое (и нарушить все или многие использования других функций в модуле Foo, которые зависят от вызова bar).
Пустые или отсутствующие значения
Как значения null, пустые или отсутствующие значения работают в Julia?
В отличие от многих языков (например, C и Java), объекты Julia не могут иметь значение null по умолчанию. Когда для ссылки (переменной, поля объектов или элемента массива) отменена инициализация, при доступе к ней будет немедленно выдана ошибка. Эту ситуацию можно обнаружить с помощью функции isdefined
или isassigned
.
Некоторые функции используются только ради их побочных эффектов, и им не нужно возвращать значение. В этих случаях используется соглашение возвращать значение nothing
, которое представляет собой одинарный объект типа Nothing
. Это обычный тип без полей; в нем нет ничего особенного, за исключением этого соглашения и того, что REPL не выводит для него никаких данных. Некоторые конструкции языка, которые в противном случае не имели бы значения, также выдают nothing
, например if false; end
.
В ситуациях, в которых значение x
типа T
существует только время от времени, тип Union{T, Nothing}
может использоваться для типов аргументов функций, полей объектов и элементов массива в качестве эквивалента Nullable
, Option
или Maybe
в других языках. Если само значение может быть nothing
(особенно когда T
равно Any
), тип Union{Some{T}, Nothing}
является более подходящим, поскольку тогда x == nothing
означает отсутствие значения, а x == Some(nothing)
— наличие значения, равного nothing
. Функция something
позволяет отменять оборачивание объектов Some
и использовать значение по умолчанию вместо аргументов nothing
. Имейте в виду, что компилятор способен генерировать эффективный код при работе с аргументами или полями Union{T, Nothing}
.
Чтобы представить отсутствующие данные в статистическом смысле (NA
в R или NULL
в SQL), используйте объект missing
. Дополнительные сведения см. в разделе Missing Values
.
В некоторых языках пустой кортеж (()
) считается канонической формой пустых значений. Однако в Julia его лучше рассматривать просто как обычный кортеж, который почему-то содержит ноль значений.
Пустой (или «низший») тип, записываемый как Union{}
(пустой тип объединения), — это тип без значений и подтипов (за исключением его самого). В общем случае вам не понадобится использовать этот тип.
Память
Почему x += y
выделяет память, а x
и y
— это массивы?
В Julia x += y
заменяется во время снижения на x = x + y
. Для массивов это имеет такое последствие, что вместо хранения результата в том же расположении в памяти, что и x
, для хранения результата выделяется новый массив. Если вы предпочитаете изменять x
, используйте x .+= y
для обновления каждого элемента по отдельности.
Хотя некоторых это поведение может удивить, такой выбор сделан преднамеренно. Основная причина — наличие в Julia неизменяемых объектов, которые не могут изменять свое значение после создания. Действительно, число является неизменяемым объектом; выражения x = 5; x += 1
не изменяют значение 5
, они изменяют значение, связанное с x
. Для неизменяемого объекта единственный способ изменить значение — это повторно присвоить его.
Чтобы немного усилить впечатление, рассмотрим следующую функцию.
function power_by_squaring(x, n::Int)
ispow2(n) || error("This implementation only works for powers of 2")
while n >= 2
x *= x
n >>= 1
end
x
end
После вызова, подобного x = 5; y = power_by_squaring(x, 4)
, вы получите ожидаемый результат: x == 5 && y == 625
. Однако предположим теперь, что *=
при использовании с 矩阵ми изменило вместо этого левую часть выражения. Здесь возникают две проблемы.
-
Для общих квадратных матриц
A = A*B
не может быть реализовано без временного хранилища:A[1,1]
вычисляется и сохраняется в левой части выражения до того, как вы закончили использовать его в правой. -
Предположим, вы хотите выделить временную память для вычисления (что в значительной степени лишит смысла заставлять
*=
работать на месте); если вы использовали изменяемостьx
, то эта функция будет вести себя по-разному для изменяемых и неизменяемых входных данных. В частности, для неизменяемогоx
после вызова вы получили бы (в общем случае)y != x
, а для изменяемогоx
—y == x
.
Поскольку поддержка универсального программирования считается более важным, чем потенциальные оптимизации производительности, которых можно достичь другими средствами (например, с помощью транслирования или явных циклов), такие операторы, как +=
и *=
, работают, повторно связывая новые значения.
Асинхронный ввод-вывод и параллельные синхронные операции записи
Почему параллельные операции записи в тот же поток приводят к перемешанному выводу?
В то время как API ввода-вывода потоковой передачи данных работает синхронно, базовая реализация полностью асинхронна.
Рассмотрим выходные данные следующего кода.
julia> @sync for i in 1:3
@async write(stdout, string(i), " Foo ", " Bar ")
end
123 Foo Foo Foo Bar Bar Bar
Это происходит, потому что вызов write
синхронный, запись каждого аргумента влияет на другие задачи, ожидая, когда завершится эта часть ввода-вывода.
print
и println
«блокируют» поток во время вызова. Следовательно, изменение write
на println
в примере выше приводит к следующему коду:
julia> @sync for i in 1:3
@async println(stdout, string(i), " Foo ", " Bar ")
end
1 Foo Bar
2 Foo Bar
3 Foo Bar
Вы можете заблокировать свои операции записи с помощью ReentrantLock
следующим образом.
julia> l = ReentrantLock();
julia> @sync for i in 1:3
@async begin
lock(l)
try
write(stdout, string(i), " Foo ", " Bar ")
finally
unlock(l)
end
end
end
1 Foo Bar 2 Foo Bar 3 Foo Bar
Массивы
В чем различия между нульмерными массивами и 标量ами?
Нульмерные массивы являются массивами вида Array{T,0}
. Они ведут себя аналогично 标量ам, но имеются важные различия. Они заслуживают специального упоминания, поскольку это особый случай, который имеет смысл с точки зрения логики, принимая во внимание универсальное определение массивов, но, на первый взгляд, работа с ними может показаться немного неинтуитивной. Нульмерный массив определяется следующей строкой.
julia> A = zeros() 0-dimensional Array{Float64,0}: 0.0
В этом примере A
— это изменяемый контейнер, содержащий один элемент, который может быть задан A[] = 1.0
и получен с помощью A[]
. Все нульмерные массивы имеют одинаковые размеры (size(A) == ()
) и длину (length(A) == 1
). В частности, нульмерные массивы не являются пустыми. Если вы считаете это неинтуитивным, вот несколько идей, которые могут помочь понять определение Julia.
-
Нульмерные массивы являются «точкой» для «линии» 向量а и «плоскости» матрицы. Так же как в случае с линией, у которой отсутствует площадь (но которая все равно представляет набор объектов), у точки отсутствует длина и вообще какие-либо измерения (тем не менее она представляет объект).
-
Мы определяем
prod(())
как 1, а общее количество элементов в массиве — произведение размера. Размер нульмерного массива —()
, и поэтому его длина —1
. -
У нульмерных массивов изначально отсутствуют измерения, по которым вы выполняете индексирование, они просто являются
A[]
. Мы можем применить для них то же правило trailing one, как и для массивов всех других размерностей, так что вы действительно можете индексировать их какA[1]
,A[1,1]
и т. д.; см. раздел Опущенные и дополнительные индексы.
Также важно понимать отличия от обычных 标量ов. Скаляры не являются изменяемыми контейнерами (даже несмотря на то, что они являются итерируемыми и определяют такие вещи, как length
, getindex
, например 1[] == 1
). В частности, если x = 0.0
определено как 标量, будет ошибкой пытаться изменить его значение через x[] = 1.0
. Скаляр x
можно преобразовать в нульмерный массив, содержащий его через fill(x)
, и наоборот, нульмерный массив a
можно преобразовать в содержащий его 标量 через a[]
. Другое отличие состоит в том, что 标量 может участвовать в операциях линейной алгебры, таких как 2 * rand(2,2)
, но аналогичная операция c нульмерным массивом fill(2) * rand(2,2)
является ошибкой.
Почему мои тесты производительности для операций линейной алгебры отличаются от тестов для других языков?
Вы можете обнаружить, что простые стандартные блоки тестов производительности для линейной алгебры, такие как
using BenchmarkTools
A = randn(1000, 1000)
B = randn(1000, 1000)
@btime $A \ $B
@btime $A * $B
могут отличаться при сравнении с другими языками, например Matlab или R.
Так как подобные операции представляют собой очень тонкие оболочки вокруг соответствующих функций BLAS, весьма вероятно, что причина различий следующая:
-
библиотека BLAS, которую использует каждый язык;
-
количество параллельных потоков.
Julia компилирует и использует собственную копию OpenBLAS с количеством потоков, ограниченным в настоящее время 8
(или числом ядер).
Изменение параметров OpenBLAS или компилирование Julia с помощью другой библиотеки BLAS, например Intel MKL, может привести к повышению производительности. Вы можете использовать MKL.jl, пакет, который заставляет линейную алгебру Julia использовать Intel MKL BLAS и LAPACK вместо OpenBLAS, или поискать на форуме обсуждений советы по ручной настройке. Обратите внимание, что Intel MKL не может быть включен в пакет Julia, так как он не базируется на открытом исходном коде.
Вычислительный кластер
Как управлять кэшами предварительной компиляции в распределенных файловых системах?
При использовании Julia в высокопроизводительных вычислительных (HPC) центрах с общей файловой системой рекомендуется использовать общее хранилище (через переменную среды JULIA_DEPOT_PATH
). Начиная с Julia версии 1.10, несколько процессов Julia в функционально одинаковых рабочих процессах и использующих одно хранилище будут координироваться через блокировки pidfile, чтобы выполнять предварительную компиляцию только в одном процессе, пока остальные находятся в режиме ожидания. Процесс предварительной компиляции будет указывать, когда выполняются предварительная компиляция или ожидание другого процесса, который выполняет предварительную компиляцию. В неинтерактивном режиме сообщения передаются через @debug
.
Однако из-за кэширования двоичного кода отказ от кэша в версии 1.9 стал более строгим, и пользователям может потребоваться задать переменную среды JULIA_CPU_TARGET
соответствующим образом, чтобы получить единый кэш, пригодный для использования во всей среде HPC.
Выпуски Julia
Мне нужно использовать стабильную версию, LTS-версию или ночную версию Julia?
Стабильная версия Julia — это последняя выпущенная версия Julia, которая подойдет большинству пользователей. Она содержит последние функции и обладает улучшенной производительностью. Стабильная версия Julia указывается в соответствии с SemVer как v1.x.y. Новый вспомогательный (minor) выпуск Julia, соответствующий новой стабильной версии, выходит приблизительно каждые 4—5 месяцев через несколько недель тестирования в качестве релиз-кандидата. В отличие от LTS-версии, стабильная версия обычно не будет получать исправления ошибок после выпуска другой стабильной версии Julia. Однако всегда будет возможность обновления до следующего стабильного выпуска, так как каждый выпуск Julia v1.x продолжит выполнять код, написанный для более ранних версий.
Вы можете предпочесть версию LTS (Long Term Support, с долговременной поддержкой) Julia, если вам нужна очень стабильная база кода. Текущая LTS-версия Julia определена в соответствии с SemVer как v1.6.x; эта ветка продолжит получать исправления ошибок, пока не будет выбрана новая ветка LTS, после чего серия v1.6.x более не будет получать регулярные исправления ошибок, и всем пользователям, кроме самых консервативных, будет рекомендовано выполнить обновление до новой серии LTS-версии. Как разработчик пакетов вы можете предпочесть выполнять разработку для LTS-версии, чтобы максимально увеличить количество пользователей, которые могут использовать ваш пакет. В соответствии с SemVer код, написанный для v1.0, продолжит работать во всех будущих стабильных и LTS-версиях. В общем, если целевой платформой является LTS, можно разрабатывать и выполнять код в последней стабильной версии, чтобы использовать улучшенную производительность, пока не применяются новые функции (такие как добавленные функции библиотек или новые методы).
Вы можете предпочесть ночную версию Julia, если хотите использовать последние обновления языка и не возражаете, если доступная на сегодня версия иногда не будет работать. Как видно из названия, выпуски ночной версии выходят почти каждую ночь (в зависимости от стабильности инфраструктуры сборки). В целом использовать ночные выпуски достаточно безопасно — ваш код не загорится. Однако иногда могут случаться ухудшения и (или) возникать проблемы, которые будут устраняться после более тщательного тестирования перед выпуском. Возможно, вы захотите выполнить тестирование в ночной версии, чтобы гарантировать, что такие ухудшения, которые влияют на ваш вариант использования, будут обнаружены до выпуска.
Наконец, вы также можете рассмотреть возможность самостоятельной сборки Julia из исходного кода. Этот вариант предназначен в основном для пользователей, которым комфортно работать в командной строке или интересно учиться. Если такой пользователь — вы, вам также может быть интересно прочитать наши инструкции по сотрудничеству.
Ссылки на каждый из этих загружаемых типов можно найти на странице загрузки по адресу https://julialang.org/downloads/. Имейте в виду, что не все версии Julia доступны для всех платформ.
Как перенести список установленных пакетов после обновления моей версии Julia?
Каждая вспомогательная (minor) версия Julia имеет собственную среду по умолчанию. Как результат, после установки новой вспомогательной (minor) версии Julia пакеты, добавленные с помощью предыдущей вспомогательной версии, не будут доступны по умолчанию. Среда для данной версии Julia определяется файлами Project.toml
и Manifest.toml
в папке, имя которой соответствует номеру версии в .julia/environments/
, например .julia/environments/v1.3
.
Если вы устанавливаете новую вспомогательную (minor) версию Julia, скажем, 1.4
, и хотите использовать в ее среде по умолчанию те же пакеты, что и в предыдущей версии (например, 1.3
), вы можете скопировать содержимое файла Project.toml
из папки 1.3
в 1.4
. Затем в сеансе в новой версии Julia войдите в «режим управления пакетами», нажав клавишу ]
, и выполните команду instantiate
.
Эта операция разрешит набор допустимых пакетов из скопированного файла, совместимых с целевой версией Julia, и установит или обновит их, если они подходят. Если вы хотите воспроизвести не только набор пакетов, но также и версии, которые использовали в предыдущей версии Julia, вам также нужно скопировать файл Manifest.toml
перед выполнением команды Pkg instantiate
. Однако имейте в виду, что пакеты могут определять ограничения совместимости, на которые может повлиять изменение версии Julia, так что точный набор версий, которые были у вас в 1.3
, может не работать в 1.4
.