-
-
Notifications
You must be signed in to change notification settings - Fork 398
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Create containers with map instead of for loops #2070
Conversation
ecb7915
to
2d24a94
Compare
2d24a94
to
05f4242
Compare
Codecov Report
@@ Coverage Diff @@
## master #2070 +/- ##
========================================
- Coverage 91.4% 91.1% -0.3%
========================================
Files 33 38 +5
Lines 4246 4318 +72
========================================
+ Hits 3881 3934 +53
- Misses 365 384 +19
Continue to review full report at Codecov.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks promising! I expect to be able to do a real review over the weekend.
@@ -251,7 +251,7 @@ following. | |||
One way of adding a group of constraints compactly is the following: | |||
```jldoctest constraint_arrays; setup=:(model=Model(); @variable(model, x)) | |||
julia> @constraint(model, con[i = 1:3], i * x <= i + 1) | |||
3-element Array{ConstraintRef{Model,C,Shape} where Shape<:AbstractShape where C,1}: | |||
3-element Array{ConstraintRef{Model,MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64},MathOptInterface.LessThan{Float64}},ScalarShape},1}: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a pretty bad printing experience.
b6b9fa3
to
3a7fa95
Compare
3a7fa95
to
63ff9cc
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is great, it addresses a design issue that goes back quite a long time. Could you run benchmarks to make sure there's no significant regression?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good, I still recommend some benchmarks.
on the `IteratorSize` of the elements of `prod.iterators`. | ||
Cartesian product of the iterators `prod.iterators`. It is the same iterator as | ||
`Base.Iterators.ProductIterator` except that it is independent of the | ||
`IteratorSize` of the elements of `prod.iterators`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this could benefit from a longer comment about why the behavior of Base.Iterators.ProductIterator
wasn't sufficient and required a wrapper. (Maybe outside the docstring.) What breaks if we use Base.Iterators.ProductIterator
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In fact, every test passed with Iterators.ProductIterator
but
https://github.com/JuliaOpt/JuMP.jl/blob/1a4f663504606464292caf2db2d60acba6991af3/examples/cutting_stock_column_generation.jl#L149
fails.
Now I have added tests in test/Containers/
covering this feature.
I added benchmarks in the initial PR comment. |
The numbers from |
There is a small performance hit for creating function container(f::Function, indices,
::Type{SparseAxisArray})
mappings = map(I -> I => f(I...), indices)
data = Dict(mappings)
if length(mappings) != length(data)
error("Repeated index ...")
end
return SparseAxisArray(Dict(data))
end because we allocate function container(f::Function, indices,
::Type{SparseAxisArray})
mappings = Base.Generator(I -> I => f(I...), indices)
data = Dict(mappings)
if length(mappings) != length(data)
error("Repeated index ...")
end
return SparseAxisArray(Dict(data))
end but then |
Same as `Dict{K, V}` but errors if constructed from an iterator with duplicate | ||
keys. | ||
""" | ||
struct NoDuplicateDict{K, V} <: AbstractDict{K, V} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to define a new type that's only used in one function? What about a function that takes a generator, constructs a Dict
with errors on duplicates, and returns Dict
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
constructs a
Dict
with errors on duplicates
How do you do that ? That's exactly NoDuplicateDict
is trying to do
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't dig too deeply, but my question was why you can't define a function like:
function dict_no_duplicates{K, V}(it) where {K, V}
dict = Dict{K, V}()
for (k, v) in it
if haskey(dict, k)
error(...)
else
dict[k] = v
end
end
return dict
end
This doesn't require defining a new type. Maybe I'm missing something related to dict_with_eltype
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The issue is that we don't know the type of the keys and values. The challenge is that Julia wants to infer it in a way that's friendly with Julia compiler and inference system.
So it starts iterating, assumes the type is always the same so it build the container with the type of the first element and then if a new element does not fit, it creates a new container with a wider type, copy and continue.
This is what is done in map
(that DenseAxisArray rely on) and dict_with_eltype
(that SparseAxisArray rely on).
There seems to be something fishy with julia> f() = (it = Containers.nested(() -> 1:1000, condition = iseven); @time map(i -> 1, it); return)
f (generic function with 2 methods)
julia> f()
0.020841 seconds (79.05 k allocations: 3.991 MiB)
julia> f()
0.000392 seconds (3.50 k allocations: 125.422 KiB)
julia> f() = (it = Iterators.filter(iseven, 1:1000); @time map(i -> 1, it); return)
f (generic function with 2 methods)
julia> f()
0.000013 seconds (10 allocations: 8.406 KiB)
julia> f()
0.000023 seconds (10 allocations: 8.406 KiB) The fact that EDIT should be improved by 1192fd8 |
The performance issue with |
Same as `Dict{K, V}` but errors if constructed from an iterator with duplicate | ||
keys. | ||
""" | ||
struct NoDuplicateDict{K, V} <: AbstractDict{K, V} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't dig too deeply, but my question was why you can't define a function like:
function dict_no_duplicates{K, V}(it) where {K, V}
dict = Dict{K, V}()
for (k, v) in it
if haskey(dict, k)
error(...)
else
dict[k] = v
end
end
return dict
end
This doesn't require defining a new type. Maybe I'm missing something related to dict_with_eltype
.
Currently, when creating a container, JuMP macros first allocate the container and then fill it with for loops.
The disadvantage of this approach is that we need to determine the type beforehand so the
eltype
is for instance not concrete for@constraint
and@expression
only works forAffExpr
(see #525).With this PR, we rely on
map
to create the array holding the values of the container hence we don't need to determine theeltype
beforehand hence also removing the need forvariable_type
andconstraint_type
(but we should keep them to avoid making a breaking change).The PR also lays out foundations for creating constrained variables as it was needed to update the creation of SDP and symmetric matrix of variables with the new design.
As a proof of concept that the container logic is now completely decoupled from JuMP, the
Containers
submodule has now a@container
.Closes #525
Benchmarks
For the
test/perf/speed.jl
benchmark, before the PR:After the PR:
The speedup is between 25% and 50% (i.e. 2x) which is nice to see!
I suspect the speedup to come from the fact that the containers of constraint now have concrete element type. This was the case in JuMP v0.18 IIRC but not anymore for JuMP v0.19 so this PR may help with #1403
For the
test/perf/axis_constraints.jl
benchmark, before this PR:After this PR:
We see a speedup on computing the sum of the duals and less allocation because the
eltype
of the containers are concrete.