Skip to content

Commit 28df5cd

Browse files
authored
Implement MutableArithmetics (#331)
* Implement MutableArithmetics * Add usage example in README
1 parent 8e66c4e commit 28df5cd

File tree

7 files changed

+163
-0
lines changed

7 files changed

+163
-0
lines changed

Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ version = "2.0.9"
77
[deps]
88
Intervals = "d8418881-c3e1-53bb-8760-2df7ec849ed5"
99
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
10+
MutableArithmetics = "d8a4904e-b15c-11e9-3269-09a3773c0cb0"
1011
RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01"
1112

1213
[compat]
1314
Intervals = "0.5, 1.0, 1.3"
15+
MutableArithmetics = "0.2.15"
1416
RecipesBase = "0.7, 0.8, 1"
1517
julia = "1"
1618

README.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,73 @@ julia> p + q
106106
ERROR: Polynomials must have same variable.
107107
```
108108

109+
#### Construction and Evaluation
110+
111+
While polynomials of type `Polynomial` are mutable objects, operations such as
112+
`+`, `-`, `*`, always create new polynomials without modifying its arguments.
113+
The time needed for these allocations and copies of the polynomial coefficients
114+
may be noticeable in some use cases. This is amplified when the coefficients
115+
are for instance `BigInt` or `BigFloat` which are mutable themself.
116+
This can be avoided by modifying existing polynomials to contain the result
117+
of the operation using the [MutableArithmetics (MA) API](https://github.com/jump-dev/MutableArithmetics.jl).
118+
Consider for instance the following arrays of polynomials
119+
```julia
120+
using Polynomials
121+
d, m, n = 30, 20, 20
122+
p(d) = Polynomial(big.(1:d))
123+
A = [p(d) for i in 1:m, j in 1:n]
124+
b = [p(d) for i in 1:n]
125+
```
126+
In this case, the arrays are mutable objects for which the elements are mutable
127+
polynomials which have mutable coefficients (`BigInt`s).
128+
These three nested levels of mutable objects communicate with the MA
129+
API in order to reduce allocation.
130+
Calling `A * b` requires approximately 40 MiB due to 2 M allocations
131+
as it does not exploit any mutability. Using
132+
```julia
133+
using MutableArithmetics
134+
const MA = MutableArithmetics
135+
MA.operate(*, A, b)
136+
```
137+
exploits the mutability and hence only allocate approximately 70 KiB due to 4 k
138+
allocations. If the resulting vector is already allocated, e.g.,
139+
```julia
140+
z(d) = Polynomial([zero(BigInt) for i in 1:d])
141+
c = [z(2d - 1) for i in 1:m]
142+
```
143+
then we can exploit its mutability with
144+
```julia
145+
MA.mutable_operate!(MA.add_mul, c, A, b)
146+
```
147+
to reduce the allocation down to 48 bytes due to 3 allocations. These remaining
148+
allocations are due to the `BigInt` buffer used to store the result of
149+
intermediate multiplications. This buffer can be preallocated with
150+
```julia
151+
buffer = MA.buffer_for(MA.add_mul, typeof(c), typeof(A), typeof(b))
152+
MA.mutable_buffered_operate!(buffer, MA.add_mul, c, A, b)
153+
```
154+
then the second line is allocation-free.
155+
156+
The `MA.@rewrite` macro rewrite an expression into an equivalent code that
157+
exploit the mutability of the intermediate results.
158+
For instance
159+
```julia
160+
MA.@rewrite(A1 * b1 + A2 * b2)
161+
```
162+
is rewritten into
163+
```julia
164+
c = MA.operate!(MA.add_mul, MA.Zero(), A1, b1)
165+
MA.operate!(MA.add_mul, c, A2, b2)
166+
```
167+
which is equivalent to
168+
```julia
169+
c = MA.operate(*, A1, b1)
170+
MA.mutable_operate!(MA.add_mul, c, A2, b2)
171+
```
172+
173+
*Note that currently, only the `Polynomial` implements the API and it only
174+
implements part of it.*
175+
109176
### Integrals and Derivatives
110177

111178
Integrate the polynomial `p` term by term, optionally adding a constant

src/Polynomials.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ include("common.jl")
1919

2020
# Polynomials
2121
include("polynomials/standard-basis.jl")
22+
include("polynomials/mutable-arithmetics.jl")
2223
include("polynomials/Polynomial.jl")
2324
include("polynomials/ImmutablePolynomial.jl")
2425
include("polynomials/SparsePolynomial.jl")

src/polynomials/Polynomial.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ struct Polynomial{T <: Number, X} <: StandardBasisPolynomial{T, X}
5050
end
5151

5252
@register Polynomial
53+
@register_mutable_arithmetic Polynomial
5354

5455

5556

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using MutableArithmetics
2+
const MA = MutableArithmetics
3+
4+
function _resize_zeros!(v::Vector, new_len)
5+
old_len = length(v)
6+
if old_len < new_len
7+
resize!(v, new_len)
8+
for i in (old_len + 1):new_len
9+
v[i] = zero(eltype(v))
10+
end
11+
end
12+
end
13+
14+
"""
15+
add_conv(out::Vector{T}, E::Vector{T}, k::Vector{T})
16+
Returns the vector `out + fastconv(E, k)`. Note that only
17+
`MA.mutable_buffered_operate!` is implemented.
18+
"""
19+
function add_conv end
20+
21+
# The buffer we need is the buffer needed by the `MA.add_mul` operation.
22+
# For instance, `BigInt`s need a `BigInt` buffer to store `E[x] * k[i]` before
23+
# adding it to `out[j]`.
24+
function MA.buffer_for(::typeof(add_conv), ::Type{Vector{T}}, ::Type{Vector{T}}, ::Type{Vector{T}}) where {T}
25+
return MA.buffer_for(MA.add_mul, T, T, T)
26+
end
27+
function MA.mutable_buffered_operate!(buffer, ::typeof(add_conv), out::Vector{T}, E::Vector{T}, k::Vector{T}) where {T}
28+
for x in eachindex(E)
29+
for i in eachindex(k)
30+
j = x + i - 1
31+
out[j] = MA.buffered_operate!(buffer, MA.add_mul, out[j], E[x], k[i])
32+
end
33+
end
34+
return out
35+
end
36+
37+
"""
38+
@register_mutable_arithmetic
39+
Register polynomial type (with vector based backend) to work with MutableArithmetics
40+
"""
41+
macro register_mutable_arithmetic(name)
42+
poly = esc(name)
43+
quote
44+
MA.mutability(::Type{<:$poly}) = MA.IsMutable()
45+
46+
function MA.promote_operation(::Union{typeof(+), typeof(*)},
47+
::Type{$poly{S,X}}, ::Type{$poly{T,X}}) where {S,T,X}
48+
R = promote_type(S,T)
49+
return $poly{R,X}
50+
end
51+
52+
function MA.buffer_for(::typeof(MA.add_mul),
53+
::Type{<:$poly{T,X}},
54+
::Type{<:$poly{T,X}}, ::Type{<:$poly{T,X}}) where {T,X}
55+
V = Vector{T}
56+
return MA.buffer_for(add_conv, V, V, V)
57+
end
58+
59+
function MA.mutable_buffered_operate!(buffer, ::typeof(MA.add_mul),
60+
p::$poly, q::$poly, r::$poly)
61+
ps, qs, rs = coeffs(p), coeffs(q), coeffs(r)
62+
_resize_zeros!(ps, length(qs) + length(rs) - 1)
63+
MA.mutable_buffered_operate!(buffer, add_conv, ps, qs, rs)
64+
return p
65+
end
66+
end
67+
end

test/Project.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
[deps]
22
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
3+
MutableArithmetics = "d8a4904e-b15c-11e9-3269-09a3773c0cb0"
34
OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881"
45
RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01"
56
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"

test/StandardBasis.jl

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1209,3 +1209,27 @@ end
12091209
end
12101210
end
12111211
end
1212+
1213+
using MutableArithmetics
1214+
const MA = MutableArithmetics
1215+
1216+
function alloc_test(f, n)
1217+
f() # compile
1218+
@test n == @allocated f()
1219+
end
1220+
1221+
@testset "Mutable arithmetics" begin
1222+
d = m = n = 4
1223+
p(d) = Polynomial(big.(1:d))
1224+
z(d) = Polynomial([zero(BigInt) for i in 1:d])
1225+
A = [p(d) for i in 1:m, j in 1:n]
1226+
b = [p(d) for i in 1:n]
1227+
c = [z(2d - 1) for i in 1:m]
1228+
buffer = MA.buffer_for(MA.add_mul, typeof(c), typeof(A), typeof(b))
1229+
@test buffer isa BigInt
1230+
c = [z(2d - 1) for i in 1:m]
1231+
MA.mutable_buffered_operate!(buffer, MA.add_mul, c, A, b)
1232+
@test c == A * b
1233+
@test c == MA.operate(*, A, b)
1234+
@test 0 == @allocated MA.mutable_buffered_operate!(buffer, MA.add_mul, c, A, b)
1235+
end

0 commit comments

Comments
 (0)