Skip to content

Commit 07ac4cd

Browse files
committed
Performance improvements: type stability and reduced allocations
Optimizations made: - `extract_coefficients` (util.jl): Fixed type instability with `findfirst` returning Union{Nothing, Int}. Now uses explicit type-stable loop. Also pre-computes array lengths outside loops for better performance. - `monomial_compress` (wronskian.jl): Replaced `Array{Any, 1}` with typed `Vector{Tuple{P, T}}` for type stability. Pre-computes parameter names in a Set for O(1) lookup instead of repeated map operations. - `massive_eval` (wronskian.jl): Uses typed containers (Set{Vector{Int}}, Dict{Vector{Int}, T}) instead of untyped. Pre-allocates working arrays and uses in-place operations with @inbounds. Pre-sizes result array. - `det_minor_expansion_inner` (elimination.jl): Replaced `in keys(cache)` with `haskey(cache)` for better performance. Pre-allocates Sets for discarded rows/cols. Uses `sort!` on mutable arrays instead of allocating. These changes improve type stability and reduce unnecessary allocations in hot code paths, particularly benefiting larger ODE systems. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 98ae3ab commit 07ac4cd

File tree

3 files changed

+76
-41
lines changed

3 files changed

+76
-41
lines changed

src/elimination.jl

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,30 @@ function det_minor_expansion_inner(
1111
if length(discarded[1]) == n
1212
return 1
1313
end
14-
if discarded in keys(cache)
14+
if haskey(cache, discarded)
1515
return cache[discarded]
1616
end
1717
result = 0
18-
row = minimum(setdiff(Set(1:n), Set(discarded[1])))
19-
dis_rows = Tuple(sort([[i for i in discarded[1]]; row]))
18+
# Use pre-allocated Set for better performance
19+
discarded_rows_set = Set(discarded[1])
20+
row = 0
21+
@inbounds for i in 1:n
22+
if !(i in discarded_rows_set)
23+
row = i
24+
break
25+
end
26+
end
27+
dis_rows = Tuple(sort!([discarded[1]..., row]))
2028
sign = 1
29+
discarded_cols_set = Set(discarded[2])
2130
for col in 1:n
22-
if !(col in discarded[2])
23-
dis_cols = Tuple(sort([[i for i in discarded[2]]; col]))
31+
if !(col in discarded_cols_set)
32+
dis_cols = Tuple(sort!([discarded[2]..., col]))
2433
result +=
2534
sign *
2635
m[row, col] *
2736
det_minor_expansion_inner(m, (dis_rows, dis_cols), cache)
28-
sign = -1 * sign
37+
sign = -sign
2938
end
3039
end
3140
if length(discarded[1]) > 1

src/util.jl

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,12 @@ Output:
192192
function extract_coefficients(poly::P, variables::Array{P, 1}) where {P <: MPolyRingElem}
193193
xs = gens(parent(poly))
194194
@assert all(in(xs), variables)
195-
cut_indices = map(v -> findfirst(x -> x == v, xs), variables)
195+
# Use a type-stable version by converting to Int explicitly
196+
cut_indices = Vector{Int}(undef, length(variables))
197+
for (j, v) in enumerate(variables)
198+
idx = findfirst(x -> x == v, xs)
199+
cut_indices[j] = idx::Int # Assert non-nothing for type stability
200+
end
196201
coeff_indices = setdiff(collect(1:length(xs)), cut_indices)
197202
coeff_vars = xs[coeff_indices]
198203

@@ -202,20 +207,25 @@ function extract_coefficients(poly::P, variables::Array{P, 1}) where {P <: MPoly
202207

203208
result = Dict{Vector{Int}, Tuple{Vector{Vector{Int}}, Vector{FieldType}}}()
204209

210+
n_cut = length(cut_indices)
211+
n_coeff = length(coeff_indices)
205212
@inbounds for i in 1:length(poly)
206213
coef = coeff(poly, i)
207214
evec = exponent_vector(poly, i)
208-
var_slice = [evec[i] for i in cut_indices]
215+
var_slice = Vector{Int}(undef, n_cut)
216+
for j in 1:n_cut
217+
var_slice[j] = evec[cut_indices[j]]
218+
end
209219
if !haskey(result, var_slice)
210220
monom_vect, coef_vect = Vector{Vector{Int}}(), Vector{FieldType}()
211221
sizehint!(monom_vect, 8)
212222
sizehint!(coef_vect, 8)
213223
result[var_slice] = (monom_vect, coef_vect)
214224
end
215225
monom_vect, coef_vect = result[var_slice]
216-
new_monom = Vector{Int}(undef, length(coeff_vars))
217-
for i in 1:length(new_monom)
218-
new_monom[i] = evec[coeff_indices[i]]
226+
new_monom = Vector{Int}(undef, n_coeff)
227+
for j in 1:n_coeff
228+
new_monom[j] = evec[coeff_indices[j]]
219229
end
220230
push!(monom_vect, new_monom)
221231
push!(coef_vect, coef)

src/wronskian.jl

Lines changed: 46 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -22,38 +22,38 @@ end
2222

2323
function monomial_compress(io_equation, params::Array{<:MPolyRingElem, 1})
2424
params_xs = isempty(params) ? empty(params) : gens(parent(first(params)))
25+
# Pre-compute param string names for faster lookup
26+
param_names = Set(var_to_str(p, xs = params_xs) for p in params)
2527
other_vars = [
26-
v for v in gens(parent(io_equation)) if
27-
!(var_to_str(v) in map(p -> var_to_str(p, xs = params_xs), params))
28+
v for v in gens(parent(io_equation)) if !(var_to_str(v) in param_names)
2829
]
2930
coeffdict = extract_coefficients(io_equation, other_vars)
3031
expvect = collect(keys(coeffdict))
3132
coeffs = collect(values(coeffdict))
3233
termlist = map(x -> prod(other_vars .^ x), expvect)
3334

34-
echelon_form = Array{Any, 1}()
35+
# Use typed arrays instead of Array{Any, 1}
36+
P = eltype(coeffs)
37+
T = eltype(termlist)
38+
echelon_form = Vector{Tuple{P, T}}()
39+
sizehint!(echelon_form, length(coeffs))
3540
for (c, p) in zip(coeffs, termlist)
3641
for i in 1:length(echelon_form)
3742
basis_c = echelon_form[i][1]
38-
coef = coeff(c, leading_monomial(basis_c)) // leading_coefficient(basis_c)
43+
lm = leading_monomial(basis_c)
44+
coef = coeff(c, lm) // leading_coefficient(basis_c)
3945
if coef != 0
4046
c = c - coef * basis_c
41-
echelon_form[i][2] += coef * p
47+
# Update in place by creating new tuple
48+
echelon_form[i] = (echelon_form[i][1], echelon_form[i][2] + coef * p)
4249
end
4350
end
4451
if c != 0
45-
push!(echelon_form, [c, p])
52+
push!(echelon_form, (c, p))
4653
end
4754
end
4855

4956
result = ([a[1] for a in echelon_form], [a[2] for a in echelon_form])
50-
#s = 0
51-
#for (a, b) in zip(result[1], result[2])
52-
# s += parent_ring_change(a, parent(io_equation)) * parent_ring_change(b, parent(io_equation))
53-
#end
54-
#println("====================")
55-
#println(s - io_equation)
56-
5757
return result
5858
end
5959

@@ -136,49 +136,65 @@ of lower degree are cached and used to compute the values of the monomials of hi
136136
"""
137137
function massive_eval(polys, eval_dict)
138138
R = parent(first(values(eval_dict)))
139-
point = [get(eval_dict, v, zero(R)) for v in gens(parent(first(polys)))]
139+
poly_ring = parent(first(polys))
140+
poly_gens = gens(poly_ring)
141+
point = [get(eval_dict, v, zero(R)) for v in poly_gens]
140142
n = length(point)
141143

142-
monomials = Set()
144+
# Use typed Set for better performance
145+
monomials = Set{Vector{Int}}()
143146
for p in polys
144147
for exp in exponent_vectors(p)
145148
push!(monomials, exp)
146149
end
147150
end
148151

149-
cache = Dict()
150-
cache[[0 for i in 1:n]] = one(R)
152+
# Pre-allocate the zero vector once
153+
zero_vec = zeros(Int, n)
154+
cache = Dict{Vector{Int}, typeof(one(R))}()
155+
cache[zero_vec] = one(R)
151156
cached_monoms = ExpVectTrie(n)
152-
push!(cached_monoms, [0 for _ in 1:n])
157+
push!(cached_monoms, zero_vec)
153158

159+
# Cache unit vectors
154160
for i in 1:n
155-
var_exp = [(i != j) ? 0 : 1 for j in 1:n]
161+
var_exp = zeros(Int, n)
162+
var_exp[i] = 1
156163
cache[var_exp] = point[i]
157164
push!(cached_monoms, var_exp)
158165
end
159166

167+
# Pre-allocate working arrays
168+
computed = zeros(Int, n)
169+
exp_work = zeros(Int, n)
160170
for exp in sort!(collect(monomials), by = sum)
161-
if !(exp in keys(cache))
171+
if !haskey(cache, exp)
162172
monom_val = one(R)
163-
computed = [0 for i in 1:n]
164-
while sum(exp) > 0
165-
_, below = get_max_below(cached_monoms, exp)
173+
# Use in-place operations on working arrays
174+
fill!(computed, 0)
175+
copyto!(exp_work, exp)
176+
while sum(exp_work) > 0
177+
_, below = get_max_below(cached_monoms, exp_work)
166178
monom_val = monom_val * cache[below]
167-
exp = exp .- below
168-
computed = computed .+ below
169-
cache[computed] = monom_val
170-
push!(cached_monoms, computed)
179+
@inbounds for k in 1:n
180+
exp_work[k] -= below[k]
181+
computed[k] += below[k]
182+
end
183+
computed_copy = copy(computed)
184+
cache[computed_copy] = monom_val
185+
push!(cached_monoms, computed_copy)
171186
end
172187
end
173188
end
174189

175-
results = []
176-
for p in polys
190+
# Pre-size results array with correct type
191+
results = Vector{typeof(zero(R))}(undef, length(polys))
192+
for (pidx, p) in enumerate(polys)
177193
res = zero(R)
178194
for (exp, coef) in zip(exponent_vectors(p), coefficients(p))
179195
res += coef * cache[exp]
180196
end
181-
push!(results, res)
197+
results[pidx] = res
182198
end
183199
return results
184200
end

0 commit comments

Comments
 (0)