AnyMath 文档

Julia函数

本文档将说明函数、方法定义和方法表的工作方式。

方法表

Julia中的每个函数都是泛型函数。 泛型函数在概念上是一个单一的函数,但由许多定义或方法组成。 泛型函数的方法存储在方法表中。 有一个全局方法表(类型 方法表)命名 核心。方法表. 方法(如调用)上的任何默认操作都使用该表。

函数调用

接到电话 f(x,y),执行以下步骤:首先,形成元组类型, 元组{typeof(f), typeof(x), typeof(y)}. 请注意,函数本身的类型是第一个元素。 这是因为函数本身与其他参数对称地参与方法查找。 此元组类型在全局方法表中查找。 但是,系统随后可以缓存结果,因此稍后可以跳过这些步骤以进行类似的查找。

此分派过程由 jl_应用程序,它有两个参数:一个指向值数组的指针 f, x,而 y,以及值的数量(在这种情况下为3)。

在整个系统中,有两种处理函数和参数列表的Api:分别接受函数和参数的Api,以及接受单个参数结构的Api。 在第一种API中,"arguments"部分不包含有关函数的信息,因为它是单独传递的。 在第二种API中,函数是参数结构的第一个元素。

例如,以下用于执行调用的函数只接受 阿格斯 指针,所以args数组的第一个元素将是要调用的函数:

jl_value_t *jl_apply(jl_value_t **args, uint32_t nargs)

相同功能的此入口点分别接受该功能,因此 阿格斯 数组不包含函数:

jl_value_t *jl_call(jl_function_t *f, jl_value_t **args, int32_t nargs);

添加方法

鉴于上述调度过程,从概念上讲,添加新方法所需要的只是(1)元组类型和(2)方法主体的代码。 jl_method_def 实现此操作。

创建泛型函数

由于每个对象都是可调用的,因此创建泛型函数不需要特别的东西。 因此 jl_new_generic_function 简单地创建一个新的单例(0大小)子类型 功能 并返回其实例。 函数可以有一个助记符"显示名称",用于调试信息和打印对象时。 例如, 基地。罪. 按照惯例,创建的_type_的名称与函数名相同,具有 # 预先准备好。 所以 打字(罪)基地。#罪.

闭包

闭包只是一个可调用的对象,其字段名对应于捕获的变量。 例如,下面的代码:

function adder(x)
    return y->x+y
end

被降低到(大致):

struct ##1{T}
    x::T
end

(_::##1)(y) = _.x + y

function adder(x)
    return ##1(x)
end

构造函数

构造函数调用只是对类型的调用,对定义在 类型{T}.

建造工程

"内建"函数,定义于 核心 模块,是:

@ast MarkdownAST。文件()do 马克顿斯特。[医]代码锁("", "<: === _abstracttype_apply_iterate_call_in_world_total_compute_sparams\n_defaultctors_equiv_typedef_expr_primitivetype_setsuper! _structtype\n_svec_len_svec_ref_typebody! _typevar apply_type compilerbarrier\ncurrent_scope donotdelete fieldtype finalizer get_binding_type getfield getglobal\nifelse invoke_in_world invokelatest isa isdefined isdefinedglobal\nmemorynew memoryref_isassigned memoryrefget memoryrefmodify! memoryrefnew\nmemoryrefoffset memoryrefreplace! memoryrefset! memoryrefsetonce! memoryrefswap!\nmodifyfield! 修改全球! nfields替换场! 替换掉! 塞特菲尔德!\nsetfieldonce! setglobal! setglobalonce! sizeof svec swapfield! swapglobal! throw\nthrow_methoderror元组typeassert typeof") 结束

这些主要是单例对象,所有这些对象的类型都是 建造,建造,它是 功能. 它们的目的是在运行时公开使用"jlcall"调用约定的入口点:

jl_value_t &ast;(jl_value_t&ast;, jl_value_t&ast;&ast;, uint32_t)

关键字参数

关键字参数通过向kwcall函数添加方法来工作。 此函数通常是"关键字参数排序器"或"关键字排序器",然后调用函数的内部主体(匿名定义)。 Kwsorter函数中的每个定义都具有与普通方法表中的某些定义相同的参数,除了具有单个 命名的,命名的 参数加在前面,它给出传递的关键字参数的名称和值。 Kwsorter的工作是根据名称将关键字参数移动到其规范位置,并计算和替换任何需要的默认值表达式。 结果是一个正常的位置参数列表,然后将其传递给另一个编译器生成的函数。

理解该过程的最简单方法是查看如何降低关键字参数方法定义。 守则:

function circle(center, radius; color = black, fill::Bool = true, options...)
    # draw
end

实际上产生_three_方法定义。 第一个函数接受所有参数(包括关键字参数)作为位置参数,并包含方法体的代码。 它有一个自动生成的名称:

function #circle#1(color, fill::Bool, options, circle, center, radius)
    # draw
end

第二种方法是原始的普通定义 函数,它处理没有传递关键字参数的情况:

function circle(center, radius)
    #circle#1(black, true, pairs(NamedTuple()), circle, center, radius)
end

这只是调度到第一个方法,传递默认值。 应用于rest参数的命名元组,以提供键值对迭代。 请注意,如果方法不接受rest关键字参数,则此参数不存在。

最后是kwsorter定义:

function (::Core.kwcall)(kws, circle, center, radius)
    if haskey(kws, :color)
        color = kws.color
    else
        color = black
    end
    # etc.

    # put remaining kwargs in `options`
    options = structdiff(kws, NamedTuple{(:color, :fill)})

    # if the method doesn't accept rest keywords, throw an error
    # unless `options` is empty

    #circle#1(color, fill, pairs(options), circle, center, radius)
end

编译器效率问题

当与Julia的"默认情况下专注于所有参数"设计结合使用时,为每个函数生成新类型会对编译器资源使用产生潜在的严重后果。 事实上,这种设计的初始实现需要更长的构建和测试时间、更高的内存使用率以及比基线大近2倍的系统映像。 在一个天真的实现中,问题已经足够糟糕,使系统几乎无法使用。 为了使设计切实可行,需要进行一些重大的优化。

第一个问题是函数值参数的不同值的函数过度专业化。 许多函数只是将参数"传递"到其他地方,例如传递到另一个函数或存储位置。 对于可能传入的每个闭包,这些函数不需要专门化。 幸运的是,这种情况很容易通过简单地考虑函数是否_calls_它的参数之一来区分(即参数出现在某处的"头部位置"中)。 性能关键的高阶函数,如 地图 当然调用他们的参数函数,所以仍然会像预期的那样专业化。 这种优化是通过记录在 分析-变量 在前端通过。 何时 cache_method功能 传递到声明为 任何功能,它的行为就好像 @nospecialize 应用了注释。 这种启发式在实践中似乎非常有效。

下一个问题涉及方法表的结构。 实证研究表明,绝大多数动态调度的调用都涉及一个或两个参数。 反过来,许多这些情况可以通过只考虑第一个论点来解决。 (旁白:单一派遣的支持者根本不会对此感到惊讶。 然而,这个论点意味着"多重调度在实践中很容易优化",因此我们应该使用它,not"我们应该使用单一调度"!). 因此,方法表和缓存基于从左到右的决策树在结构上分割,因此允许有效的最近邻搜索。

前端为所有闭包生成类型声明。 最初,这是通过生成普通类型声明来实现的。 然而,这产生了大量的构造函数,所有这些都是微不足道的(简单地将所有参数传递给 新的). 由于方法是部分有序的,因此插入所有这些方法都是O(n2),再加上它们太多而无法保留。 这是通过生成优化 结构_类型 表达式直接(绕过默认构造函数生成),并使用 新的 直接创建闭包实例。 不是最漂亮的事,但你做你该做的。

下一个问题是 @测试 宏,它为每个测试用例生成一个0参数闭包。 这不是真正必要的,因为每个测试用例只需运行一次。 因此, @测试 被修改为扩展为try-catch块,该块记录测试结果(true、false或引发的异常)并对其调用测试套件处理程序。