Julia版本1.5已经发布。发布是有时间限制的,因此没有围绕特定的功能进行计划,但这一次我们似乎走运了:相当多的主要开发结合在一起,使得1.5特别令人兴奋。让我们来看看一些亮点吧。
此版本带来了人们期待已久的重大优化,可以显著减少某些工作负载中的堆分配。要理解它,稍微了解一下Julia的对象模型会有所帮助。Julia既有可变类型的对象,也有不变类型的对象。使用struct声明的新记录(复合)类型是不可变的,而如果您想要可变的记录,则必须使用可变的struct来声明新类型。该语言自动为每种类型选择内存布局和调用约定,通常会尝试与C/C++兼容。通常,可变对象必须存在于堆上的单个位置,因此可变对象将通过引用进行存储和传递(除非编译器可以证明这无关紧要)。另一方面,不可变对象为编译器提供了更大的灵活性。例如,包含两个值的不可变结构可以通过引用传递给函数,就像具有相同字段的可变结构一样,但也可以只通过在寄存器中传递这两个值来传递它-因为没有需要与值保持同步的内存位置,因为无论它们来自哪里,都不能修改。
在此版本之前,对不可变对象的布局优化有一个很大的限制:如果不可变对象指向堆分配的可变对象,那么它本身就需要被堆分配。这是垃圾收集器和代码生成器工作方式的产物:对象字段和GC根之间存在一对一的对应关系会更简单。对于1.5,Jameson Nash进行了重新设计来修复这个问题,允许编译器跟踪对象字段中的多个根。
这项工作的结果是,任意不可变对象-无论它们是否具有引用可变对象的字段-现在都可以通过值进行堆栈分配、传递和返回,并内联存储在数组和其他对象中。简而言之,引用变量值的不可变结构现在与只引用其他不可变对象的不可变结构一样有效。(结构可以堆栈分配到一些基于大小的限制,但在实践中不太可能超过这些限制。)。
这是一个大问题,因为许多重要的抽象只能通过将可变对象包装在结构中来实现。Julia中的经典示例是array";views";(SubArray类型),它将可变数组与一些关于如何将索引转换为视图的元数据以及如何将索引转换为原始数组的元数据包装在一起。视图的目的是能够将数组的一部分传递给函数,而无需复制它。但是,如果视图对象本身需要堆分配,我们会发现自己处于使用视图消除分配本身会导致分配的不幸情况中:在以前的Julia版本中,您必须在复制数组片段或使用视图但必须分配视图对象之间做出选择。尽管视图对象很小并且分配成本相对较低,但是任何数量的分配都会抑制优化并触发垃圾收集,这在性能关键型代码中都可能是痛苦的。对于一些用户来说,消除这种分配已经足够重要,因为UnsafeArray包的存在只是为了允许在不分配的情况下创建视图。
在1.5中,这个艰难的选择消失了:使用视图不再强制分配。这意味着在绝大多数情况下不再需要不安全阵列。不用说,消除对复杂的性能解决方法的需要始终是编译器开发人员的最高愿望之一。要实际查看这一点,下面是一个简单的函数,用于对矩阵的n x n邻域求和:
函数sum_Neighborhood(A,n::int)返回[sum(A[i:i+n-1,j:j+n-1])for i=1:n:size(A,1),j=1:n:size(A,2)]end。
注意:这使用@view宏来创建每个n x n区域的就地视图。我们可以使用BenchmarkTools包检查其资源使用情况。以下是Julia v1.4中的结果:
Julia>;使用BenchmarkToolsjulia>;x=rand(1000,1000);Julia>;SUM_Neighborhood($x,2)BenchmarkTools.Trial:内存估计:17.17MiB分配估计:250004-最短时间:4.510毫秒(0.00%GC)中值时间:5.320毫秒(0.00%GC)平均时间:5.411毫秒(5.25%GC)最长时间:11.100毫秒(11.41%GC)-样本:923EVAL/样本:1。
试验:内存估计:1.91MiB分配估计:2-最短时间:3.479毫秒(0.00%GC)中值时间:4.007毫秒(0.00%GC)平均时间:4.297毫秒(2.03%GC)最长时间:8.191毫秒(29.20%GC)-样本:1164个事件/样本:1。
加速实际上并不是很大,这证明了朱莉娅的分配器是非常高效的,但请注意分配上的差异:1.4时为250004,1.5时为2%。不同之处在于所有那些不再需要堆分配的视图对象。
改进对并行性的支持是当前工作的一个主要重点。早在V0.5中,线程就被作为一个实验性特性引入,从那时起,几乎每个版本都增强了线程安全性并添加了新特性。特别值得一提的是,1.3版本对于线程来说是一个重要的里程碑版本,因为它为可组合的多线程引入了@spawn构造(以及所有支持的基础设施),就像Go的Goroutines一样,但是关注的是高性能计算。
在这一点上,许多Julia程序员都在使用线程,并且出现了像多线程CSV解析这样的重大性能优势。因此,我们觉得试验性的标签不再是真正合适的,在这个版本中,我们将大部分线程API标记为稳定。我们显式地记录了剩余的限制和API中仍可能更改的部分。以下是本版本中与线程相关的工作的简要概述:
新的@线程:请求当前缺省线程循环调度的静态语法,允许我们在将来更改缺省调度。
随着开发人员开始利用线程API的稳定性,我们非常期待在生态系统中获得更多的性能优势。
作为一种贪婪的语言,我们经常试图两全其美。这并不总是那么容易,编译时间和运行时性能之间的权衡一直是一个特别令人沮丧的问题。为了提供良好的初始用户体验,我们使用的默认优化级别是-O2,大致类似于GCC或clang中的-O2选项。这对于计算内核和基准测试非常有用,但并不是所有代码都是性能关键型的,而且Julia用户一直在加载越来越多更大的包用于绘图和其他非内循环支持任务。为了帮助减少此类包的编译延迟,现在可以在每个模块中提供优化级别提示。例如,Plots.jl指定@optlevel 1,表示它希望使用-O1优化级别,而不是默认的-O2。这将第一个情节的时间缩短了大约三分之一。
除了@optlevel之外,我们还一直在逐步解决其他编译器性能问题--有时一次只有1%或2%的大改进。浏览GitHub上的延迟标签可以大致了解其中所涉及的内容。这项工作的最终结果是,从版本1.4升级到1.5,加载Plots.jl包(使用绘图)的时间从9.8秒缩短到6.1秒(快38%),生成第一个绘图的时间从11.7秒缩短到7.8秒(快33%)。当然,您的里程数可能会有所不同-性能总是高度依赖于系统硬件和配置。但总体而言,与1.4相比,此版本大大改善了编译器延迟,后者已经比以前的版本更快了。
在传递关键字参数或构造命名元组时,值保存在与参数或字段名称相同的变量中是很常见的。例如,如果要打印彩色文本,并且有要在名为color的变量中使用的颜色,则需要编写printstyle(";text";,color=color)。总是键入相同的单词两次可能会很乏味,特别是在将几个关键字参数委托给另一个函数时。此版本添加了一种已经在其他几种语言(如Tyescript)中流行起来的便捷速记:
请注意color参数前的分号:这是Julia将此速记语法与将color作为第二个位置参数传递区分开来所必需的。类似的速记适用于命名元组:
当传递的值是结构中与关键字参数同名的字段(或使用object.field语法的任何其他内容)时,这些速记也有效:
虽然这一改变不允许程序员做任何他们以前不能做的事情,但它使编写使用关键字参数和命名元组的代码变得更加令人愉快、简洁和可读性更强。
变量作用域是编程语言中丰富得令人惊讶的设计难题来源。在大多数情况下,将程序划分为许多嵌套的作用域是一件很棒的事情:它有助于本地推理代码的含义,它可以实现优化,还可以防止一段代码中的更改意外地破坏遥远的、不相关的代码。所有好的功能都适用于大型编程。但是对于快速的实验性编程来说,担心每个变量的生存期是乏味的,特别是在交互工作时,将事物设为局部而不是全局的安全缺省有时会令人困惑和不便。
在Julia的0.x版本中,我们巧妙地猜测了用户希望变量是局部变量还是全局变量。基本规则是,在循环内部但在任何函数体之外-在所谓的软作用域中-当用户为变量赋值时,如果已有同名的全局变量,则会将其赋值,但如果没有同名的全局变量,则赋值将在循环的局部创建一个新变量。这是一个非常有效的启发式方法,在全局范围内尽可能接近函数体内部发生的事情。但是,也有一些问题:一些人发现它令人困惑和不一致,而且这使得不可能静态地确定代码的意义,因为在像Julia这样的动态语言中,不能静态地确定给定名称的全局变量是否存在。因此,对于Julia的1.0版,我们简化了规则:不再猜测用户的意思:如果您为局部作用域(函数或循环)中的变量赋值,并且没有该名称的局部变量,则该赋值将创建一个新的局部变量。句号。故事的结尾,非常简单的规则。
尽管1.0范围规则在纸面上很简单,但许多人觉得它不直观,令人讨厌。考虑以下示例:
有人会天真地猜测它打印的是数字55,而在Julia1.0之前,它就是这么做的。但是在1.0中,我们一致地使for循环引入了新的局部作用域,这意味着此代码将抛出一个未定义的变量错误,因为s被认为是循环的局部的:因为没有预先存在的局部s(只有全局s),并且在循环中分配了s,所以它是局部的,并且循环的第一次迭代尝试访问未定义的局部s。
在一个很长的顶级脚本中,您可能没有意识到您正在覆盖文件的不同部分中使用的全局变量,这种缺省的局部性假设可能会更好(实际上,当我们更改1.0的行为时发现了许多错误),但是对于交互式使用,这显然是不友好的。使用REPL进行调试也很痛苦:在函数体中工作的代码在没有额外全局注释的情况下在REPL中的工作方式不同。
这是一个足够大的问题-特别是对于新用户-IJulia的开发人员自己解决问题,并添加代码以恢复1.0之前的旧作用域行为来重写输入。让常用的Jupyter前端的行为与默认的REPL不同并不是一种好的情况,所以我们必须做些什么。
经过长时间的讨论和几个设计的考虑和原型,我们在v1.5中确定了以下解决方案:
如果文件中的代码与REPL中的代码行为不同,请求显式局部或全局声明以消除循环中变量的歧义,则打印警告。
我们认为这是在不做出破坏性改变的情况下所能做的最好的事情。可以说,在不从根本上改变作用域或变量声明的工作方式的情况下,即使允许破坏性更改,这也是最好的选择。它有几个理想的属性:
在文件中,在局部作用域内通过为全局赋值而意外破坏全局是一个真正的问题,您会得到一个明确的警告,提示您澄清这种破坏是否是故意的。
此更改修复了Julia新用户的常见绊脚石,以及喜欢使用REPL进行调试的老用户的烦恼,同时不牺牲该语言在大规模可靠编程中的适用性。
Julia长期以来一直有一个功能强大且广泛使用的接口来调用C函数。虽然功能还不错,但一些贪婪的程序员(我们最喜欢的那类)指出,语法并不美观:C调用看起来不太像普通的函数调用。例如,对strlen的调用如下所示:
啊-现在它看起来像是对strlen的调用,类型使用自然的Julian语法指定。
Julia擅长模拟,所以随机数对语言的很多用户都很重要。对于这个版本,Rafael Fourquet,Random标准库的主要架构师之一,也是一个多产的贡献者,在一些流行的情况下实现了一些令人印象深刻的算法改进。第一个是在生成正态分布的双精度浮点数时的重大改进。在Julia 1.5中调用Randn(1000)的速度几乎是Julia 1.4的两倍。生成随机布尔值也变得更快:Rand(Bool,1000)几乎快了6倍。最后,从离散集合中进行采样也变得更快:Rand(1:100,1000)的速度提高了25%。
有一个新的命令行选项--bug-report=rr,它使记录和上传rr跟踪变得非常简单,以帮助修复bug。此功能在另一篇博客文章中有详细描述。
Julia附带了一个名为PKG的内置包管理器。过去,PKG直接从GitHub、GitLab、BitBucket或其他任何托管它们的地方下载软件包。虽然这是引导软件包生态系统的一种很好的方法,但它有很多缺点:
消失的资源:如果一个包的repo消失了,因为维护人员删除了它,使它成为私有的,或者它的托管服务关闭了,那么就没有人能再安装那个包了。我们想让朱莉娅的用户免受左垫的影响。
缺乏洞察力:Julia项目不知道安装了多少软件包。GitHub等人。有此信息,但不与我们分享。如果能知道哪些软件包使用得最多,对社区来说将是非常有益的。
与Git/GitHub结合:如果您要从Git托管服务安装包,这会将包管理器绑定到该托管服务的API和/或Git协议。没有任何固有的要求Julia包必须使用GIT开发或由GIT托管服务提供。
防火墙问题:很多使用防火墙的组织都会阻止git和/或ssh。阻止访问代码托管服务并不少见,因为IT可能需要控制人们使用什么软件。将单个服务器作为安装Julia包的唯一联系点,并使用标准协议(如HTTPS)将大大减轻防火墙问题。如果在防火墙内设置缓存代理服务器很简单,那就更好了。
性能:虽然从GitHub下载软件包在北美可能效果很好,但在世界其他地方就没那么快了。我们听说中国和澳大利亚的用户需要几十分钟才能完成PKG操作。我们希望各地的Julia用户都能有很好的安装软件包的体验。
在Julia1.4中引入了一种获取包的新方法,称为PKG协议,它解决了所有这些问题。PKG客户端使用简单的HTTPS协议连接到PKG服务器,而不是从托管软件包的地方下载软件包注册表、软件包tarball和工件的新版本-安装和使用软件包所需的一切。该协议是在1.4中引入的,但在默认情况下并未使用:我们希望有时间对其进行测试,确保其工作正常,并构建所需的服务器基础设施。在1.5版本中,我们改变了开关,使PKG协议成为Julia获得包裹的默认方式。现在,默认情况下,所有内容都是从https://pkg.julialang.org下载的(正如非常基本的登录页面告诉你的那样,它不是一个网站),它由世界各地的十几台pkg服务器提供服务,确保任何地方的每个人都能获得安装和更新Julia软件包的良好体验。
请享受此次发布,如果您遇到任何问题或有任何建议,请一如既往地让我们知道。我们希望在大约四个月后回来报告1.6版的更多进展!