Skip to content

Commit 298f76e

Browse files
frankierstevengj
andauthored
Allow passing preallocated segsbuf for alloc reuse (#59)
* Reduce allocs by forcing specialisation for function type in adapt and evalrule * Allow preallocated segsbuf to be passed to quadgk * Update src/adapt.jl * Update test/runtests.jl * Update src/adapt.jl Co-authored-by: Steven G. Johnson <stevenj@mit.edu>
1 parent 9a62f94 commit 298f76e

File tree

4 files changed

+61
-15
lines changed

4 files changed

+61
-15
lines changed

src/QuadGK.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ and returns the approximate `integral = 0.746824132812427` and error estimate
2323
"""
2424
module QuadGK
2525

26-
export quadgk, quadgk!, gauss, kronrod
26+
export quadgk, quadgk!, gauss, kronrod, alloc_segbuf
2727

2828
using DataStructures, LinearAlgebra
2929
import Base.Order.Reverse

src/adapt.jl

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# integration with the order-n Kronrod rule and weights of type Tw,
44
# with absolute tolerance atol and relative tolerance rtol,
55
# with maxevals an approximate maximum number of f evaluations.
6-
function do_quadgk(f::F, s::NTuple{N,T}, n, atol, rtol, maxevals, nrm) where {T,N,F}
6+
function do_quadgk(f::F, s::NTuple{N,T}, n, atol, rtol, maxevals, nrm, segbuf) where {T,N,F}
77
x,w,gw = cachedrule(T,n)
88

99
@assert N 2
@@ -32,11 +32,13 @@ function do_quadgk(f::F, s::NTuple{N,T}, n, atol, rtol, maxevals, nrm) where {T,
3232
return (I, E) # fast return when no subdivisions required
3333
end
3434

35-
return adapt(f, heapify!(collect(segs), Reverse), I, E, numevals, x,w,gw,n, atol_, rtol_, maxevals, nrm)
35+
segheap = segbuf === nothing ? collect(segs) : (resize!(segbuf, N-1) .= segs)
36+
heapify!(segheap, Reverse)
37+
return adapt(f, segheap, I, E, numevals, x,w,gw,n, atol_, rtol_, maxevals, nrm)
3638
end
3739

3840
# internal routine to perform the h-adaptive refinement of the integration segments (segs)
39-
function adapt(f, segs::Vector{T}, I, E, numevals, x,w,gw,n, atol, rtol, maxevals, nrm) where {T}
41+
function adapt(f::F, segs::Vector{T}, I, E, numevals, x,w,gw,n, atol, rtol, maxevals, nrm) where {F, T}
4042
# Pop the biggest-error segment and subdivide (h-adaptation)
4143
# until convergence is achieved or maxevals is exceeded.
4244
while E > atol && E > rtol * nrm(I) && numevals < maxevals
@@ -116,7 +118,7 @@ end
116118
# Gauss-Kronrod quadrature of f from a to b to c...
117119

118120
"""
119-
quadgk(f, a,b,c...; rtol=sqrt(eps), atol=0, maxevals=10^7, order=7, norm=norm)
121+
quadgk(f, a,b,c...; rtol=sqrt(eps), atol=0, maxevals=10^7, order=7, norm=norm, segbuf=nothing)
120122
121123
Numerically integrate the function `f(x)` from `a` to `b`, and optionally over additional
122124
intervals `b` to `c` and so on. Keyword options include a relative error tolerance `rtol`
@@ -169,15 +171,35 @@ or `1/sqrt(x)` singularity).
169171
170172
For real-valued endpoints, the starting and/or ending points may be infinite. (A coordinate
171173
transformation is performed internally to map the infinite interval to a finite one.)
174+
175+
In normal usage, `quadgk(...)` will allocate a buffer for segments. You can
176+
instead pass a preallocated buffer allocated using `alloc_segbuf(...)` as the
177+
`segbuf` argument. This buffer can be used across multiple calls to avoid
178+
repeated allocation.
172179
"""
173-
quadgk(f, a, b, c...; kws...) =
174-
quadgk(f, promote(a, b, c...)...; kws...)
180+
quadgk(f, segs...; kws...) =
181+
quadgk(f, promote(segs...)...; kws...)
175182

176-
quadgk(f, a::T,b::T,c::T...;
177-
atol=nothing, rtol=nothing, maxevals=10^7, order=7, norm=norm) where {T} =
178-
handle_infinities(f, (a, b, c...)) do f, s, _
179-
do_quadgk(f, s, order, atol, rtol, maxevals, norm)
183+
function quadgk(f, segs::T...;
184+
atol=nothing, rtol=nothing, maxevals=10^7, order=7, norm=norm, segbuf=nothing) where {T}
185+
handle_infinities(f, segs) do f, s, _
186+
do_quadgk(f, s, order, atol, rtol, maxevals, norm, segbuf)
180187
end
188+
end
189+
190+
"""
191+
function alloc_segbuf(domain_type=Float64, range_type=Float64, error_type=Float64; size=1)
192+
193+
This helper will allocate a segment buffer for segments to a `quadgk(...)` call
194+
with the given `domain_type`, which is the same as the type of the integration
195+
limits, `range_type` i.e. the range of the function being integrated and
196+
`error_type`, the type returned by the `norm` given to `quadgk(...)` and
197+
starting with the given `size`. The buffer can then be reused across multiple
198+
compatible calls to `quadgk(...)` to avoid repeated allocation.
199+
"""
200+
function alloc_segbuf(domain_type=Float64, range_type=Float64, error_type=Float64; size=1)
201+
Vector{Segment{domain_type, range_type, error_type}}(undef, size)
202+
end
181203

182204
"""
183205
quadgk!(f!, result, a,b,c...; rtol=sqrt(eps), atol=0, maxevals=10^7, order=7, norm=norm)
@@ -199,8 +221,8 @@ For integrands whose values are *small* arrays whose length is known at compile-
199221
it is usually more efficient to use `quadgk` and modify your integrand to return
200222
an `SVector` from the [StaticArrays.jl package](https://github.com/JuliaArrays/StaticArrays.jl).
201223
"""
202-
function quadgk!(f!, result, a::T,b::T,c::T...; atol=nothing, rtol=nothing, maxevals=10^7, order=7, norm=norm) where {T}
224+
function quadgk!(f!, result, a::T,b::T,c::T...; atol=nothing, rtol=nothing, maxevals=10^7, order=7, norm=norm, segbuf=nothing) where {T}
203225
fx = result / oneunit(T) # pre-allocate array of correct type for integrand evaluations
204226
f = InplaceIntegrand(f!, result, fx)
205-
return quadgk(f, a, b, c...; atol=atol, rtol=rtol, maxevals=maxevals, order=order, norm=norm)
206-
end
227+
return quadgk(f, a, b, c...; atol=atol, rtol=rtol, maxevals=maxevals, order=order, norm=norm, segbuf=segbuf)
228+
end

src/evalrule.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Base.isless(i::Segment, j::Segment) = isless(i.E, j.E)
1212

1313
# Internal routine: approximately integrate f(x) over the interval (a,b)
1414
# by evaluating the integration rule (x,w,gw). Return a Segment.
15-
function evalrule(f, a,b, x,w,gw, nrm)
15+
function evalrule(f::F, a,b, x,w,gw, nrm) where {F}
1616
# Ik and Ig are integrals via Kronrod and Gauss rules, respectively
1717
s = convert(eltype(x), 0.5) * (b-a)
1818
n1 = 1 - (length(x) & 1) # 0 if even order, 1 if odd order

test/runtests.jl

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,28 @@ end
9494
end
9595
@test quadgk(x -> [cos(100x), sin(30x)], 0, 1) (I′,E′) ([-0.005063656411097513, 0.028191618337080532], 4.2100180879009775e-10)
9696
@test I === I′ # result is written in-place to I
97+
end
98+
99+
# This is enough for allocation currently caused by the do-lambda in quadgk(...)
100+
const smallallocbytes = 500
101+
102+
@testset "segbuf" begin
103+
# Should not need subdivision
104+
function id(x::Float64)::Float64
105+
1.0
106+
end
107+
# Should need subdivision
108+
function osc(x::Float64)::Float64
109+
(x - 0.3)^2 * sin(87(x + 0.07))
110+
end
111+
no_subdiv() = @timed quadgk(id, -1.0, 1.0)
112+
subdiv_alloc() = @timed quadgk(osc, -1.0, 1.0)
113+
segbuf = alloc_segbuf(size=1)
114+
subdiv_alloc_segbuf() = @timed quadgk(osc, -1.0, 1.0, segbuf=segbuf)
115+
no_subdiv() # warmup
116+
@test no_subdiv()[3] < smallallocbytes # [3] == .bytes starting in Julia 1.5
117+
subdiv_alloc() # warmup
118+
@test subdiv_alloc()[3] > smallallocbytes
119+
subdiv_alloc_segbuf() # warmup
120+
@test subdiv_alloc_segbuf()[3] < smallallocbytes
97121
end

0 commit comments

Comments
 (0)