Profiling

CPU Profiling

有两种主要的方法来对 Julia 代码进行 CPU 性能分析:

Via @profile

在通过 @profile 宏为给定调用启用分析的地方。

julia> using Profile

julia> @profile foo()

julia> Profile.print()
Overhead ╎ [+additional indent] Count File:Line; Function
=========================================================
    ╎147  @Base/client.jl:506; _start()
        ╎ 147  @Base/client.jl:318; exec_options(opts::Base.JLOptions)
...

Triggered During Execution

已经在运行的任务也可以在任何用户触发的时间内进行固定时间段的分析。

要触发分析:

  • MacOS 和 FreeBSD(基于 BSD 的平台):使用 ctrl-t 或向 julia 进程发送 SIGINFO 信号,即 % kill -INFO $julia_pid
  • Linux:向 julia 进程发送 SIGUSR1 信号,即 % kill -USR1 $julia_pid
  • Windows:当前不支持。

首先,显示在信号抛出瞬间的单个堆栈跟踪,然后收集1秒的性能分析,接着在下一个让步点(对于没有让步点的代码,例如紧密循环,可能是在任务完成时)显示性能分析报告。

可选地将环境变量 JULIA_PROFILE_PEEK_HEAP_SNAPSHOT 设置为 1,以便自动收集 heap snapshot

julia> foo()
##== the user sends a trigger while foo is running ==##
load: 2.53  cmd: julia 88903 running 6.16u 0.97s

======================================================================================
Information request received. A stacktrace will print followed by a 1.0 second profile
======================================================================================

signal (29): Information request: 29
__psynch_cvwait at /usr/lib/system/libsystem_kernel.dylib (unknown line)
_pthread_cond_wait at /usr/lib/system/libsystem_pthread.dylib (unknown line)
...

======================================================================
Profile collected. A report will print if the Profile module is loaded
======================================================================

Overhead ╎ [+additional indent] Count File:Line; Function
=========================================================
Thread 1 Task 0x000000011687c010 Total snapshots: 572. Utilization: 100%
   ╎147 @Base/client.jl:506; _start()
       ╎ 147 @Base/client.jl:318; exec_options(opts::Base.JLOptions)
...

Thread 2 Task 0x0000000116960010 Total snapshots: 572. Utilization: 0%
   ╎572 @Base/task.jl:587; task_done_hook(t::Task)
      ╎ 572 @Base/task.jl:879; wait()
...

Customization

剖析的持续时间可以通过 Profile.set_peek_duration 进行调整。

配置报告按线程和任务进行细分。将一个无参数函数传递给 Profile.peek_report[] 以覆盖此设置。即 Profile.peek_report[] = () -> Profile.print() 以移除任何分组。这也可以被外部配置数据消费者覆盖。

Reference

Profile.@profileMacro
@profile

@profile <expression> 在运行您的表达式时进行定期的回溯。这些回溯会附加到内部回溯缓冲区中。

source

Profile 中的方法未被导出,需要通过例如 Profile.print() 的方式调用。

Profile.printFunction
print([io::IO = stdout,] [data::Vector = fetch()], [lidict::Union{LineInfoDict, LineInfoFlatDict} = getdict(data)]; kwargs...)

将分析结果打印到 io(默认是 stdout)。如果您没有提供 data 向量,将使用内部的累积回溯缓冲区。

关键字参数可以是以下任意组合:

  • format – 决定回溯是以(默认,:tree)还是不带(:flat)缩进的方式打印,缩进表示树结构。
  • C – 如果为 true,则显示来自 C 和 Fortran 代码的回溯(通常会被排除)。
  • combine – 如果为 true(默认),则合并对应于同一行代码的指令指针。
  • maxdepth – 限制在 :tree 格式中高于 maxdepth 的深度。
  • sortedby – 控制 :flat 格式中的顺序。:filefuncline(默认)按源代码行排序,:count 按收集样本的数量排序,:overhead 按每个函数自身产生的样本数量排序。
  • groupby – 控制任务和线程的分组,或不分组。选项有 :none(默认)、:thread:task[:thread, :task][:task, :thread],后两个提供嵌套分组。
  • noisefloor – 限制超过样本启发式噪声底线的帧(仅适用于格式 :tree)。建议尝试的值为 2.0(默认值为 0)。此参数隐藏 n <= noisefloor * √N 的样本,其中 n 是该行的样本数量,N 是被调用者的样本数量。
  • mincount – 限制打印输出仅包含至少有 mincount 次出现的行。
  • recur – 控制 :tree 格式中的递归处理。:off(默认)正常打印树。:flat 则压缩任何递归(按 ip),显示将任何自我递归转换为迭代器的近似效果。:flatc 也执行相同操作,但还包括 C 帧的折叠(可能在 jl_apply 周围做奇怪的事情)。
  • threads::Union{Int,AbstractVector{Int}} – 指定在报告中包含快照的线程。请注意,这并不控制在哪些线程上收集样本(这些样本也可能在另一台机器上收集)。
  • tasks::Union{Int,AbstractVector{Int}} – 指定在报告中包含快照的任务。请注意,这并不控制在哪些任务中收集样本。
Julia 1.8

groupbythreadstasks 关键字参数是在 Julia 1.8 中引入的。

Note

在 Windows 上的分析仅限于主线程。其他线程未被采样,因此不会显示在报告中。

source
print([io::IO = stdout,] data::Vector, lidict::LineInfoDict; kwargs...)

将剖析结果打印到 io。此变体用于检查由先前调用 retrieve 导出的结果。提供包含回溯的向量 data 和包含行信息的字典 lidict

有关有效关键字参数的说明,请参见 Profile.print([io], data)

source
Profile.initFunction
init(; n::Integer, delay::Real)

配置回溯之间的 delay(以秒为单位)和每个线程可以存储的指令指针数量 n。每个指令指针对应于一行代码;回溯通常由一长串指令指针组成。请注意,每个回溯使用 6 个空格来存储元数据和两个 NULL 结束标记。可以通过调用此函数而不带参数来获取当前设置,并且可以使用关键字或按顺序 (n, delay) 独立设置每个参数。

source
Profile.fetchFunction
fetch(;include_meta = true) -> data

返回配置文件回溯的缓冲区副本。请注意,data 中的值仅在当前会话的此机器上有意义,因为它依赖于 JIT 编译中使用的确切内存地址。此函数主要供内部使用;对于大多数用户来说,retrieve 可能是更好的选择。默认情况下,包括线程 ID 和任务 ID 等元数据。将 include_meta 设置为 false 以去除元数据。

source
Profile.retrieveFunction
retrieve(; kwargs...) -> data, lidict

“导出”分析结果为可移植格式,返回所有回溯的集合(data)和一个字典,该字典将data中的(会话特定)指令指针映射到存储文件名、函数名和行号的LineInfo值。此函数允许您保存分析结果以供将来分析。

source
Profile.callersFunction
callers(funcname, [data, lidict], [filename=<filename>], [linerange=<start:stop>]) -> Vector{Tuple{count, lineinfo}}

根据之前的性能分析运行,确定谁调用了特定的函数。提供文件名(可选地,提供函数定义的行号范围)可以帮助你区分重载的方法。返回值是一个向量,包含调用次数和关于调用者的行信息。可以选择性地提供从 retrieve 获得的回溯 data;否则,将使用当前的内部性能分析缓冲区。

source
Profile.clear_malloc_dataFunction
clear_malloc_data()

在使用 --track-allocation 运行 julia 时,清除任何存储的内存分配数据。执行您想要测试的命令(以强制 JIT 编译),然后调用 clear_malloc_data。然后再次执行您的命令,退出 Julia,并检查生成的 *.mem 文件。

source
Profile.get_peek_durationFunction
get_peek_duration()

获取通过 SIGINFOSIGUSR1 触发的 "peek" 配置文件的持续时间(以秒为单位),具体取决于平台。

source
Profile.set_peek_durationFunction
set_peek_duration(t::Float64)

设置通过 SIGINFOSIGUSR1 触发的 "peek" 配置文件的持续时间(以秒为单位),具体取决于平台。

source

Memory profiling

Profile.Allocs.@profileMacro
Profile.Allocs.@profile [sample_rate=0.1] expr

expr 期间发生的分配进行分析,同时返回结果和 AllocResults 结构体。

采样率为 1.0 将记录所有内容;0.0 将不记录任何内容。

julia> Profile.Allocs.@profile sample_rate=0.01 peakflops()
1.03733270279065e11

julia> results = Profile.Allocs.fetch()

julia> last(sort(results.allocs, by=x->x.size))
Profile.Allocs.Alloc(Vector{Any}, Base.StackTraces.StackFrame[_new_array_ at array.c:127, ...], 5576)

有关更多信息,请参阅 Julia 文档中的分析教程。

Julia 1.11

较旧版本的 Julia 在所有情况下无法捕获类型。在较旧版本的 Julia 中,如果您看到类型为 Profile.Allocs.UnknownType 的分配,这意味着分析器不知道分配了什么类型的对象。这主要发生在分配来自编译器生成的代码时。有关更多信息,请参见 issue #43688

自 Julia 1.11 以来,所有分配都应报告类型。

Julia 1.8

分配分析器是在 Julia 1.8 中添加的。

source

Profile.Allocs 中的方法未被导出,需要通过例如 Profile.Allocs.fetch() 的方式调用。

Profile.Allocs.printFunction
Profile.Allocs.print([io::IO = stdout,] [data::AllocResults = fetch()]; kwargs...)

将分析结果打印到 io(默认情况下为 stdout)。如果您不提供 data 向量,将使用累积回溯的内部缓冲区。

有关有效关键字参数的解释,请参见 Profile.print

source
Profile.Allocs.fetchFunction
Profile.Allocs.fetch()

获取记录的分配,并将其解码为可以分析的Julia对象。

source
Profile.Allocs.startFunction
Profile.Allocs.start(sample_rate::Real)

开始以给定的采样率记录分配。采样率为 1.0 将记录所有内容;0.0 将不记录任何内容。

source

Heap Snapshots

Profile.take_heap_snapshotFunction
Profile.take_heap_snapshot(filepath::String, all_one::Bool=false, streaming=false)
Profile.take_heap_snapshot(all_one::Bool=false; dir::String, streaming=false)

写入堆的快照,格式为 Chrome Devtools Heap Snapshot viewer 所期望的 JSON 格式(.heapsnapshot 扩展名),默认情况下将其写入当前目录中的文件($pid_$timestamp.heapsnapshot)(如果当前目录不可写,则写入 tempdir),或者写入给定的 dir,或者给定的完整文件路径,或 IO 流。

如果 all_one 为 true,则将每个对象的大小报告为 1,以便于计数。否则,报告实际大小。

如果 streaming 为 true,我们将把快照数据流式输出到四个文件中,使用 filepath 作为前缀,以避免将整个快照保存在内存中。此选项应在内存受限的情况下使用。这些文件可以通过调用 Profile.HeapSnapshot.assemble_snapshot() 进行重新组装,这可以在离线时完成。

注意:我们强烈建议出于性能原因将 streaming=true。根据部分重建快照需要将整个快照保存在内存中,因此如果快照很大,处理时可能会耗尽内存。流式传输允许您在工作负载完成后离线重建快照。如果您尝试在 streaming=false(默认情况下,为了向后兼容)时收集快照,并且您的进程被终止,请注意,这将始终将部分保存到您提供的文件路径相同的目录中,因此您仍然可以通过 assemble_snapshot() 在事后重建快照。

source

Profile 中的方法未被导出,需要通过例如 Profile.take_heap_snapshot() 的方式调用。

julia> using Profile

julia> Profile.take_heap_snapshot("snapshot.heapsnapshot")

跟踪和记录堆上的 Julia 对象。这仅记录 Julia 垃圾收集器已知的对象。未被垃圾收集器管理的外部库分配的内存将不会出现在快照中。

为了避免在记录快照时出现内存溢出,我们添加了一个流式选项,将堆快照流式输出到四个文件中,

julia> using Profile

julia> Profile.take_heap_snapshot("snapshot"; streaming=true)

其中“snapshot”是生成文件的前缀路径。

一旦快照文件生成,可以使用以下命令离线组装:

julia> using Profile

julia> Profile.HeapSnapshot.assemble_snapshot("snapshot", "snapshot.heapsnapshot")

生成的堆快照文件可以上传到 Chrome 开发者工具中查看。有关更多信息,请参见 chrome devtools docs。分析 Chromium 堆快照的另一种方法是使用 VS Code 扩展 ms-vscode.vscode-js-profile-flame

Firefox 的堆快照格式不同,目前可能 无法 用于查看由 Julia 生成的堆快照。