BenchmarkTools 输出到 DataFrame
BenchmarkTools outputs to DataFrame
我正在尝试使用 BenchmarkTools
对函数的性能进行基准测试,如下例所示。我的目标是获取 @benchmark
作为 DataFrame 的输出。
在这个例子中,我正在对以下两个函数的性能进行基准测试:
"""Example function A: recodes negative values to 0"""
function negative_to_zero_a!(x::Array{<:Real,1})
for (i, v) in enumerate(x)
if v < 0
x[i] = zero(x[i]) # uses 'zero()'
end
end
end
"""Example function B: recodes negative values to 0"""
function negative_to_zero_b!(x::Array{<:Real,1})
for (i, v) in enumerate(x)
if v < 0
x[i] = 0 # does not use 'zero()'
end
end
end
这意味着改变以下向量:
int_a = [1, -2, 3, -4]
float_a = [1.0, -2.0, 3.0, -4.0]
int_b = copy(int_a)
float_b = copy(float_a)
然后我使用 BenchmarkTools
生成性能基准。
using BenchmarkTools
int_a_benchmark = @benchmark negative_to_zero_a!(int_a)
int_b_benchmark = @benchmark negative_to_zero_b!(int_b)
float_a_benchmark = @benchmark negative_to_zero_a!(float_a)
float_b_benchmark = @benchmark negative_to_zero_b!(float_b)
我现在想将四个 BenchmarkTools.Trial
对象中每一个的元素检索到类似于下面的 DataFrame 中。在该 DataFrame 中,每一行都包含给定 BenchmarkTools.Trial
对象的结果。例如
DataFrame("id" => ["int_a_benchmark", "int_b_benchmark", "float_a_benchmark", "float_b_benchmark"],
"minimum" => [15.1516, 15.631, 14.615, 14.271],
"median" => [15.916, 15.731, 15.916, 15.879],
"maximum" => [149.15, 104.108, 63.363, 116.181],
"allocations" => [0, 0, 0, 0],
"memory_bytes" => [0, 0, 0, 0])
4×6 DataFrame
Row │ id minimum median maximum allocations memory_estimate
│ String Float64 Float64 Float64 Int64 Int64
─────┼────────────────────────────────────────────────────────────────────────────
1 │ int_a_benchmark 15.1516 15.916 149.15 0 0
2 │ int_b_benchmark 15.631 15.731 104.108 0 0
3 │ float_a_benchmark 14.615 15.916 63.363 0 0
4 │ float_b_benchmark 14.271 15.879 116.181 0 0
如何将基准测试的结果检索到像这样的 DataFrame 中?
你可以做到,例如像这样:
julia> using Statistics, DataFrames, BenchmarkTools
julia> preprocess_trial(t::BenchmarkTools.Trial, id::AbstractString) =
(id=id,
minimum=minimum(t.times),
median=median(t.times),
maximum=maximum(t.times),
allocations=t.allocs,
memory_estimate=t.memory)
preprocess_trial (generic function with 1 method)
julia> output = DataFrame()
0×0 DataFrame
julia> for (fun, id) in [(sin, "sin"), (cos, "cos"), (log, "log")]
push!(output, preprocess_trial(@benchmark(sin(1)), id))
end
julia> output
3×6 DataFrame
Row │ id minimum median maximum allocations memory_estimate
│ String Float64 Float64 Float64 Int64 Int64
─────┼─────────────────────────────────────────────────────────────────
1 │ sin 0.001 0.001 0.1 0 0
2 │ cos 0.001 0.001 0.1 0 0
3 │ log 0.001 0.001 0.1 0 0
与 Julia 一样,有多种方法可以做你想做的事。我在这里展示的可能不是最简单的方法,而是一种希望展示有趣方法的方法,它允许进行一些概括。
但在我们开始之前,请注意:您的基准测试不太正确,因为您的函数会改变参数。为了进行正确的基准测试,您应该在每个 运行 之前复制您的数据,并且在每次执行函数时也这样做。您可以在此处找到更多信息:https://juliaci.github.io/BenchmarkTools.jl/dev/manual/#Setup-and-teardown-phases
所以,现在,我们假设您准备了这样的基准测试
int_a_benchmark = @benchmark negative_to_zero_a!(a) setup=(a = copy($int_a)) evals=1
int_b_benchmark = @benchmark negative_to_zero_b!(b) setup=(b = copy($int_b)) evals=1
float_a_benchmark = @benchmark negative_to_zero_a!(a) setup=(a = copy($float_a)) evals=1
float_b_benchmark = @benchmark negative_to_zero_b!(b) setup=(b = copy($float_b)) evals=1
主要思路如下。如果我们可以将基准数据表示为 DataFrame,那么我们可以将它们组合在一起作为一个大型 DataFrame 并进行所有必要的计算。
当然可以用超级简单的方法做到这一点,只需通过命令
df = DataFrame(times = int_a_benchmark.times, gctimes = int_a_benchmark.gctimes)
df.memory .= int_a_benchmark.memory
df.allocs .= int_a_benchmark.allocs
但这太无聊而且太逐字了(但很简单,应该在 99% 的时间里完成)。如果我们可以 DataFrame(int_a_benchmark)
并立即得到结果,那就太好了。
事实证明,这是可能的,因为 DataFrames 支持 Tables.jl 接口来处理 table-like 数据。您可以在 Tables.jl 的手册中阅读详细信息,但通常您需要定义一些有意义的东西,例如列的名称,列访问器和包将完成其他所有工作。我在这里显示结果,没有进一步的解释。
using Tables
Tables.istable(::Type{<:BenchmarkTools.Trial}) = true
Tables.columnaccess(::Type{<:BenchmarkTools.Trial}) = true
Tables.columns(m::BenchmarkTools.Trial) = m
Tables.columnnames(m::BenchmarkTools.Trial) = [:times, :gctimes, :memory, :allocs]
Tables.schema(m::BenchmarkTools.Trial) = Tables.Schema(Tables.columnnames(m), (Float64, Float64, Int, Int))
function Tables.getcolumn(m::BenchmarkTools.Trial, i::Int)
i == 1 && return m.times
i == 2 && return m.gctimes
i == 3 && return fill(m.memory, length(m.times))
return fill(m.allocs, length(m.times))
end
Tables.getcolumn(m::BenchmarkTools.Trial, nm::Symbol) = Tables.getcolumn(m, nm == :times ? 1 : nm == :gctimes ? 2 : nm == :memory ? 3 : 4)
我们可以看到它确实有效(几乎是神奇的)
julia> DataFrame(int_a_benchmark)
10000×4 DataFrame
Row │ times gctimes memory allocs
│ Float64 Float64 Int64 Int64
───────┼──────────────────────────────────
1 │ 309.0 0.0 0 0
2 │ 38.0 0.0 0 0
3 │ 25.0 0.0 0 0
4 │ 37.0 0.0 0 0
⋮ │ ⋮ ⋮ ⋮ ⋮
下一步是将所有数据帧合并到一个数据帧中。我们应该进行以下步骤:
- 将基准试验转换为数据框
- 添加带有相关基准名称的列
name
- 将它们全部组合在一起(使用
vcat
函数)
当然,您可以为每个数据帧一个接一个地执行所有这些步骤,但是它太长了(而且很无聊,是的)。相反,我们可以使用神奇的 mapreduce
函数,即所谓的 Do-Block syntax。 map
部分将准备必要的数据帧,reduce
将它们组合在一起
df_benchmark = mapreduce(vcat, zip([int_a_benchmark, int_b_benchmark, float_a_benchmark, float_b_benchmark],
["int_a_benchmark", "int_b_benchmark", "float_a_benchmark", "float_b_benchmark"])) do (x, y)
df = DataFrame(x)
df.name .= y
df
end
现在是最后一部分。我们有漂亮的大型 DataFrame,我们希望对其进行聚合。为此,我们可以使用 DataFrames
的 Split-Apply-Combine 策略
julia> combine(groupby(df_benchmark, :name),
:times => minimum => :minimum,
:times => median => :median,
:times => maximum => :maximum,
:allocs => first => :allocations,
:memory => first => :memory_estimate)
4×6 DataFrame
Row │ name minimum median maximum allocations memory_estimate
│ String Float64 Float64 Float64 Int64 Int64
─────┼────────────────────────────────────────────────────────────────────────────
1 │ int_a_benchmark 22.0 24.0 3252.0 0 0
2 │ int_b_benchmark 20.0 23.0 489.0 0 0
3 │ float_a_benchmark 21.0 23.0 134.0 0 0
4 │ float_b_benchmark 21.0 23.0 129.0 0 0
作为奖励,在 Chain.jl 包的帮助下,最后的计算看起来会更好:
using Chain
@chain df_benchmark begin
groupby(:name)
combine(:times => minimum => :minimum,
:times => median => :median,
:times => maximum => :maximum,
:allocs => first => :allocations,
:memory => first => :memory_estimate)
end
我正在尝试使用 BenchmarkTools
对函数的性能进行基准测试,如下例所示。我的目标是获取 @benchmark
作为 DataFrame 的输出。
在这个例子中,我正在对以下两个函数的性能进行基准测试:
"""Example function A: recodes negative values to 0"""
function negative_to_zero_a!(x::Array{<:Real,1})
for (i, v) in enumerate(x)
if v < 0
x[i] = zero(x[i]) # uses 'zero()'
end
end
end
"""Example function B: recodes negative values to 0"""
function negative_to_zero_b!(x::Array{<:Real,1})
for (i, v) in enumerate(x)
if v < 0
x[i] = 0 # does not use 'zero()'
end
end
end
这意味着改变以下向量:
int_a = [1, -2, 3, -4]
float_a = [1.0, -2.0, 3.0, -4.0]
int_b = copy(int_a)
float_b = copy(float_a)
然后我使用 BenchmarkTools
生成性能基准。
using BenchmarkTools
int_a_benchmark = @benchmark negative_to_zero_a!(int_a)
int_b_benchmark = @benchmark negative_to_zero_b!(int_b)
float_a_benchmark = @benchmark negative_to_zero_a!(float_a)
float_b_benchmark = @benchmark negative_to_zero_b!(float_b)
我现在想将四个 BenchmarkTools.Trial
对象中每一个的元素检索到类似于下面的 DataFrame 中。在该 DataFrame 中,每一行都包含给定 BenchmarkTools.Trial
对象的结果。例如
DataFrame("id" => ["int_a_benchmark", "int_b_benchmark", "float_a_benchmark", "float_b_benchmark"],
"minimum" => [15.1516, 15.631, 14.615, 14.271],
"median" => [15.916, 15.731, 15.916, 15.879],
"maximum" => [149.15, 104.108, 63.363, 116.181],
"allocations" => [0, 0, 0, 0],
"memory_bytes" => [0, 0, 0, 0])
4×6 DataFrame
Row │ id minimum median maximum allocations memory_estimate
│ String Float64 Float64 Float64 Int64 Int64
─────┼────────────────────────────────────────────────────────────────────────────
1 │ int_a_benchmark 15.1516 15.916 149.15 0 0
2 │ int_b_benchmark 15.631 15.731 104.108 0 0
3 │ float_a_benchmark 14.615 15.916 63.363 0 0
4 │ float_b_benchmark 14.271 15.879 116.181 0 0
如何将基准测试的结果检索到像这样的 DataFrame 中?
你可以做到,例如像这样:
julia> using Statistics, DataFrames, BenchmarkTools
julia> preprocess_trial(t::BenchmarkTools.Trial, id::AbstractString) =
(id=id,
minimum=minimum(t.times),
median=median(t.times),
maximum=maximum(t.times),
allocations=t.allocs,
memory_estimate=t.memory)
preprocess_trial (generic function with 1 method)
julia> output = DataFrame()
0×0 DataFrame
julia> for (fun, id) in [(sin, "sin"), (cos, "cos"), (log, "log")]
push!(output, preprocess_trial(@benchmark(sin(1)), id))
end
julia> output
3×6 DataFrame
Row │ id minimum median maximum allocations memory_estimate
│ String Float64 Float64 Float64 Int64 Int64
─────┼─────────────────────────────────────────────────────────────────
1 │ sin 0.001 0.001 0.1 0 0
2 │ cos 0.001 0.001 0.1 0 0
3 │ log 0.001 0.001 0.1 0 0
与 Julia 一样,有多种方法可以做你想做的事。我在这里展示的可能不是最简单的方法,而是一种希望展示有趣方法的方法,它允许进行一些概括。
但在我们开始之前,请注意:您的基准测试不太正确,因为您的函数会改变参数。为了进行正确的基准测试,您应该在每个 运行 之前复制您的数据,并且在每次执行函数时也这样做。您可以在此处找到更多信息:https://juliaci.github.io/BenchmarkTools.jl/dev/manual/#Setup-and-teardown-phases
所以,现在,我们假设您准备了这样的基准测试
int_a_benchmark = @benchmark negative_to_zero_a!(a) setup=(a = copy($int_a)) evals=1
int_b_benchmark = @benchmark negative_to_zero_b!(b) setup=(b = copy($int_b)) evals=1
float_a_benchmark = @benchmark negative_to_zero_a!(a) setup=(a = copy($float_a)) evals=1
float_b_benchmark = @benchmark negative_to_zero_b!(b) setup=(b = copy($float_b)) evals=1
主要思路如下。如果我们可以将基准数据表示为 DataFrame,那么我们可以将它们组合在一起作为一个大型 DataFrame 并进行所有必要的计算。
当然可以用超级简单的方法做到这一点,只需通过命令
df = DataFrame(times = int_a_benchmark.times, gctimes = int_a_benchmark.gctimes)
df.memory .= int_a_benchmark.memory
df.allocs .= int_a_benchmark.allocs
但这太无聊而且太逐字了(但很简单,应该在 99% 的时间里完成)。如果我们可以 DataFrame(int_a_benchmark)
并立即得到结果,那就太好了。
事实证明,这是可能的,因为 DataFrames 支持 Tables.jl 接口来处理 table-like 数据。您可以在 Tables.jl 的手册中阅读详细信息,但通常您需要定义一些有意义的东西,例如列的名称,列访问器和包将完成其他所有工作。我在这里显示结果,没有进一步的解释。
using Tables
Tables.istable(::Type{<:BenchmarkTools.Trial}) = true
Tables.columnaccess(::Type{<:BenchmarkTools.Trial}) = true
Tables.columns(m::BenchmarkTools.Trial) = m
Tables.columnnames(m::BenchmarkTools.Trial) = [:times, :gctimes, :memory, :allocs]
Tables.schema(m::BenchmarkTools.Trial) = Tables.Schema(Tables.columnnames(m), (Float64, Float64, Int, Int))
function Tables.getcolumn(m::BenchmarkTools.Trial, i::Int)
i == 1 && return m.times
i == 2 && return m.gctimes
i == 3 && return fill(m.memory, length(m.times))
return fill(m.allocs, length(m.times))
end
Tables.getcolumn(m::BenchmarkTools.Trial, nm::Symbol) = Tables.getcolumn(m, nm == :times ? 1 : nm == :gctimes ? 2 : nm == :memory ? 3 : 4)
我们可以看到它确实有效(几乎是神奇的)
julia> DataFrame(int_a_benchmark)
10000×4 DataFrame
Row │ times gctimes memory allocs
│ Float64 Float64 Int64 Int64
───────┼──────────────────────────────────
1 │ 309.0 0.0 0 0
2 │ 38.0 0.0 0 0
3 │ 25.0 0.0 0 0
4 │ 37.0 0.0 0 0
⋮ │ ⋮ ⋮ ⋮ ⋮
下一步是将所有数据帧合并到一个数据帧中。我们应该进行以下步骤:
- 将基准试验转换为数据框
- 添加带有相关基准名称的列
name
- 将它们全部组合在一起(使用
vcat
函数)
当然,您可以为每个数据帧一个接一个地执行所有这些步骤,但是它太长了(而且很无聊,是的)。相反,我们可以使用神奇的 mapreduce
函数,即所谓的 Do-Block syntax。 map
部分将准备必要的数据帧,reduce
将它们组合在一起
df_benchmark = mapreduce(vcat, zip([int_a_benchmark, int_b_benchmark, float_a_benchmark, float_b_benchmark],
["int_a_benchmark", "int_b_benchmark", "float_a_benchmark", "float_b_benchmark"])) do (x, y)
df = DataFrame(x)
df.name .= y
df
end
现在是最后一部分。我们有漂亮的大型 DataFrame,我们希望对其进行聚合。为此,我们可以使用 DataFrames
的 Split-Apply-Combine 策略julia> combine(groupby(df_benchmark, :name),
:times => minimum => :minimum,
:times => median => :median,
:times => maximum => :maximum,
:allocs => first => :allocations,
:memory => first => :memory_estimate)
4×6 DataFrame
Row │ name minimum median maximum allocations memory_estimate
│ String Float64 Float64 Float64 Int64 Int64
─────┼────────────────────────────────────────────────────────────────────────────
1 │ int_a_benchmark 22.0 24.0 3252.0 0 0
2 │ int_b_benchmark 20.0 23.0 489.0 0 0
3 │ float_a_benchmark 21.0 23.0 134.0 0 0
4 │ float_b_benchmark 21.0 23.0 129.0 0 0
作为奖励,在 Chain.jl 包的帮助下,最后的计算看起来会更好:
using Chain
@chain df_benchmark begin
groupby(:name)
combine(:times => minimum => :minimum,
:times => median => :median,
:times => maximum => :maximum,
:allocs => first => :allocations,
:memory => first => :memory_estimate)
end