We have code
abstract type Parameters{T} end
abstract type ParametersFiniteStrain{N,T} <: Parameters{T} end
struct BirchMurnaghan{N,T} <: ParametersFiniteStrain{N,T}
x0::NTuple{N,T}
end
function BirchMurnaghan{3}(v0, b0, b′0)
T = Base.promote_typeof(v0, b0, b′0)
return BirchMurnaghan{3,T}(Tuple(convert(T, x) for x in (v0, b0, b′0)))
end
Consider which will be faster?
The type approach
First, using a type for the real EquationOfStateOfSolids
:
abstract type EquationOfStateOfSolids{T<:Parameters} end
struct EnergyEOSS{T} <: EquationOfStateOfSolids{T}
params::T
end
struct PressureEOSS{T} <: EquationOfStateOfSolids{T}
params::T
end
struct BulkModulusEOSS{T} <: EquationOfStateOfSolids{T}
params::T
end
function (eos::EnergyEOSS{<:BirchMurnaghan3rd})(v)
v0, b0, b′0 = eos.params.x0
x = cbrt(v0 / v)
y = x^2 - 1
return 9 / 16 * b0 * v0 * y^2 * (6 - 4 * x^2 + b′0 * y)
end
To make users feel a clean code, we also want to define
energyeq(p::Parameters) = EnergyEOSS(p)
pressureeq(p::Parameters) = PressureEOSS(p)
bulkmoduluseq(p::Parameters) = BulkModulusEOSS(p)
Now
julia> x = BirchMurnaghan{3}(1, 20, 300)
BirchMurnaghan{3,Int64}((1, 20, 300))
julia> eos = energyeq(x)
EquationsOfStateOfSolids.Collections.EnergyEOSS{BirchMurnaghan{3,Int64}}(BirchMurnaghan{3,Int64}((1, 20, 300)))
julia> eos(3)
-460.13550846092517
julia> using BenchmarkTools
julia> @btime eos(3)
42.890 ns (1 allocation: 16 bytes)
-460.13550846092517
julia> @btime $eos(3)
34.429 ns (0 allocations: 0 bytes)
-460.13550846092517
julia> @code_lowered eos(3)
CodeInfo(
1 ─ %1 = Base.getproperty(eos, :params)
│ %2 = Base.getproperty(%1, :x0)
│ %3 = Base.indexed_iterate(%2, 1)
│ v0 = Core.getfield(%3, 1)
│ @_5 = Core.getfield(%3, 2)
│ %6 = Base.indexed_iterate(%2, 2, @_5)
│ b0 = Core.getfield(%6, 1)
│ @_5 = Core.getfield(%6, 2)
│ %9 = Base.indexed_iterate(%2, 3, @_5)
│ b′0 = Core.getfield(%9, 1)
│ %11 = v0 / v
│ x = EquationsOfStateOfSolids.Collections.cbrt(%11)
│ %13 = x
│ %14 = Core.apply_type(Base.Val, 2)
│ %15 = (%14)()
│ %16 = Base.literal_pow(EquationsOfStateOfSolids.Collections.:^, %13, %15)
│ y = %16 - 1
│ %18 = 9 / 16
│ %19 = b0
│ %20 = v0
│ %21 = y
│ %22 = Core.apply_type(Base.Val, 2)
│ %23 = (%22)()
│ %24 = Base.literal_pow(EquationsOfStateOfSolids.Collections.:^, %21, %23)
│ %25 = x
│ %26 = Core.apply_type(Base.Val, 2)
│ %27 = (%26)()
│ %28 = Base.literal_pow(EquationsOfStateOfSolids.Collections.:^, %25, %27)
│ %29 = 4 * %28
│ %30 = 6 - %29
│ %31 = b′0 * y
│ %32 = %30 + %31
│ %33 = %18 * %19 * %20 * %24 * %32
└── return %33
)
The closure approach
This approach is much simpler, we just define
function energyeq(p)
v0, b0, b′0 = p.x0
function (v)
x = cbrt(v0 / v)
y = x^2 - 1
return 9 / 16 * b0 * v0 * y^2 * (6 - 4 * x^2 + b′0 * y)
end
end
and no need for EquationOfStateOfSolids
to be defined. But will closures affect speed?
julia> x = BirchMurnaghan{3}(1, 20, 300)
BirchMurnaghan{3,Int64}((1, 20, 300))
julia> eos = energyeq(x)
#3 (generic function with 1 method)
julia> typeof(eos)
EquationsOfStateOfSolids.Collections.var"#3#4"{Int64,Int64,Int64}
It seems from here that the answer is clear: the only difference between the "type" approach and the "closure" approach is that the "type" approach officially defines several types (EquationOfStateOfSolids
, etc.) but the other one just uses an internal name var"#3#4"{Int64,Int64,Int64}
. I have closed and reopened the REPL multiple times and the results are the same. But we still want to perform a benchmark:
julia> @btime eos(3)
43.101 ns (1 allocation: 16 bytes)
-460.13550846092517
julia> @btime $eos(3)
34.427 ns (0 allocations: 0 bytes)
-460.13550846092517
julia> @code_lowered eos(3)
CodeInfo(
1 ─ %1 = Core.getfield(#self#, :v0)
│ %2 = %1 / v
│ x = EquationsOfStateOfSolids.Collections.cbrt(%2)
│ %4 = x
│ %5 = Core.apply_type(Base.Val, 2)
│ %6 = (%5)()
│ %7 = Base.literal_pow(EquationsOfStateOfSolids.Collections.:^, %4, %6)
│ y = %7 - 1
│ %9 = 9 / 16
│ %10 = Core.getfield(#self#, :b0)
│ %11 = Core.getfield(#self#, :v0)
│ %12 = y
│ %13 = Core.apply_type(Base.Val, 2)
│ %14 = (%13)()
│ %15 = Base.literal_pow(EquationsOfStateOfSolids.Collections.:^, %12, %14)
│ %16 = x
│ %17 = Core.apply_type(Base.Val, 2)
│ %18 = (%17)()
│ %19 = Base.literal_pow(EquationsOfStateOfSolids.Collections.:^, %16, %18)
│ %20 = 4 * %19
│ %21 = 6 - %20
│ %22 = Core.getfield(#self#, :b′0)
│ %23 = %22 * y
│ %24 = %21 + %23
│ %25 = %9 * %10 * %11 * %15 * %24
└── return %25
)
Summary
By comparing their @code_lowered
, the closure approach maybe even faster, because the type approach need to unwrap
everytime:
v0, b0, b′0 = eos.params.x0
But this is done only once for the closure. That's why it has a shorter @code_lowered
.