-
Notifications
You must be signed in to change notification settings - Fork 10
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
add an optimization for immutable structs without type arguments #1
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -79,6 +79,50 @@ Base.show(io::IO, u::Uninitialized) = print(io, "uninit") | |||||
# islazyfield(::Type{Foo}, s::Symbol) = s === :a || s === :b | ||||||
function islazyfield end | ||||||
|
||||||
# For immutable types without type parameters we create a Box | ||||||
# for the lazy fields instead of making the whole struct mutable | ||||||
# https://github.com/JuliaLang/julia/issues/35053 | ||||||
mutable struct Box{T} | ||||||
x::T | ||||||
end | ||||||
Base.getindex(b::Box) = b.x | ||||||
Base.setindex!(b::Box, v) = b.x = v | ||||||
Base.convert(::Type{Box{T}}, x) where {T} = Box{T}(x) | ||||||
Base.show(io::IO, b::Box) = show(io, b.x) | ||||||
|
||||||
|
||||||
# This is a replication of the Nothing and Missing conversion functionality from Base. | ||||||
if isdefined(Base, :typesplit) | ||||||
nonuninittype(::Type{T}) where {T} = Base.typesplit(T, Uninitialized) | ||||||
else | ||||||
nonuninittype(::Type{T}) where {T} = Core.Compiler.typesubtract(T, Uninitialized) | ||||||
end | ||||||
promote_rule(T::Type{Uninitialized}, S::Type) = Union{S, Uninitialized} | ||||||
function promote_rule(T::Type{>:Uninitialized}, S::Type) | ||||||
R = nonuninittype(T) | ||||||
R >: T && return Any | ||||||
T = R | ||||||
R = promote_type(T, S) | ||||||
return Union{R, Uninitialized} | ||||||
end | ||||||
|
||||||
function nonuninittype_checked(T::Type) | ||||||
R = nonuninittype(T) | ||||||
R >: T && error("could not compute non-uninit type") | ||||||
return R | ||||||
end | ||||||
|
||||||
# These are awful | ||||||
Base.convert(::Type{T}, x::T) where {T>:Uninitialized} = x | ||||||
Base.convert(::Type{T}, x) where {T>:Uninitialized} = convert(nonuninittype_checked(T), x) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Out of curiosity, I'm guessing you didn't use
Suggested change
because of method ambiguities? At that point, why don't you just write the constructor for parametric types, like you suggested? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Because I don't know how to write it in a way that will work properly. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
these conversions seem to be needed |
||||||
Base.convert(::Type{T}, x) where T>:Union{Uninitialized, Nothing} = convert(nonuninittype_checked(T), x) | ||||||
Base.convert(::Type{T}, x) where T>:Union{Uninitialized, Missing} = convert(nonuninittype_checked(T), x) | ||||||
Base.convert(::Type{T}, x) where T>:Union{Uninitialized, Missing, Nothing} = convert(nonuninittype_checked(T), x) | ||||||
# Ambiguity resolution | ||||||
Base.convert(::Type{T}, x::T) where T>:Union{LazilyInitializedFields.Uninitialized, Nothing} = x | ||||||
Base.convert(::Type{T}, x::T) where T>:Union{LazilyInitializedFields.Uninitialized, Missing} = x | ||||||
Base.convert(::Type{T}, x::T) where T>:Union{Nothing, Missing, LazilyInitializedFields.Uninitialized} = x | ||||||
|
||||||
|
||||||
struct NonLazyFieldException <: Exception | ||||||
T::DataType | ||||||
|
@@ -96,6 +140,8 @@ end | |||||
Base.showerror(io::IO, err::UninitializedFieldException) = | ||||||
print(io, "field `", err.s, "` in struct of type `$(err.T)` is not initialized") | ||||||
|
||||||
@inline _setfield!(x, s::Symbol, v) = !isimmutable(x) ? setfield!(x, s, v) : getfield(x, s)[] = v | ||||||
@inline _getfield(x, s::Symbol) = !isimmutable(x) ? getfield(x, s) : getfield(x, s)[] | ||||||
|
||||||
""" | ||||||
init!(a, s::Symbol) | ||||||
|
@@ -104,7 +150,7 @@ Function version of [@init!](@ref). | |||||
""" | ||||||
@inline function init!(x::T, s::Symbol, v) where {T} | ||||||
islazyfield(T, s) || throw(NonLazyFieldException(T, s)) | ||||||
return setfield!(x, s, v) | ||||||
return _setfield!(x, s, v) | ||||||
end | ||||||
|
||||||
_check_setproperty_expr(expr, s) = | ||||||
|
@@ -156,7 +202,7 @@ Function version of [@isinit](@ref). | |||||
""" | ||||||
@inline function isinit(x::T, s) where {T} | ||||||
islazyfield(T, s) || throw(NonLazyFieldException(T, s)) | ||||||
!(getfield(x, s) isa Uninitialized) | ||||||
!(_getfield(x, s) isa Uninitialized) | ||||||
end | ||||||
""" | ||||||
@isinit a.b | ||||||
|
@@ -199,7 +245,7 @@ Function version of [`@uninit!`](@ref). | |||||
|
||||||
@inline function uninit!(x::T, s::Symbol) where {T} | ||||||
islazyfield(T, s) || throw(NonLazyFieldException(T, s)) | ||||||
return setfield!(x, s, uninit) | ||||||
return _setfield!(x, s, uninit) | ||||||
end | ||||||
|
||||||
""" | ||||||
|
@@ -269,6 +315,11 @@ end | |||||
|
||||||
function lazy_struct(expr) | ||||||
mutable, structdef, body = expr.args | ||||||
has_typevar = expr.args[2] isa Expr && expr.args[2].head === :curly | ||||||
# We cannot use a box for the lazy value in case we have type | ||||||
# parameters due to https://github.com/JuliaLang/julia/issues/35053 | ||||||
use_box = !mutable && !has_typevar | ||||||
expr.args[1] = !use_box | ||||||
structname = if structdef isa Symbol | ||||||
structdef | ||||||
elseif structdef isa Expr && structdef.head === :curly | ||||||
|
@@ -277,11 +328,16 @@ function lazy_struct(expr) | |||||
error("internal error: unhandled expression $expr") | ||||||
end | ||||||
|
||||||
expr.args[1] = true # make mutable | ||||||
lazyfield = QuoteNode[] | ||||||
for (i, arg) in enumerate(body.args) | ||||||
if arg isa Expr && arg.head === :macrocall && arg.args[1] === Symbol("@lazy") | ||||||
body.args[i] = macroexpand(@__MODULE__, arg) | ||||||
# a::Union{A, B} -> a::Union{A, B, Uninitialized} | ||||||
expanded = macroexpand(@__MODULE__, arg) | ||||||
if use_box | ||||||
# Union{A, B, Uninitialized} -> Box{Uninitialized{A, B, Uninitialized}} | ||||||
expanded.args[2] = :($Box{$(expanded.args[2])}) | ||||||
end | ||||||
body.args[i] = expanded | ||||||
name = body.args[i].args[1] | ||||||
@assert name isa Symbol | ||||||
push!(lazyfield, QuoteNode(name)) | ||||||
|
@@ -301,13 +357,14 @@ function lazy_struct(expr) | |||||
function Base.getproperty(x::$(esc(structname)), s::Symbol) | ||||||
if $(LazilyInitializedFields).islazyfield($(esc(structname)), s) | ||||||
r = Base.getfield(x, s) | ||||||
$(use_box ? :(r = r[]) : nothing) | ||||||
r isa $Uninitialized && throw(UninitializedFieldException(typeof(x), s)) | ||||||
return r | ||||||
end | ||||||
return Base.getfield(x, s) | ||||||
end | ||||||
end) | ||||||
if !mutable | ||||||
if !mutable && !use_box | ||||||
push!(ret.args, quote | ||||||
function Base.setproperty!(x::$(esc(structname)), s::Symbol, v) | ||||||
error("setproperty! for struct of type `", $(esc(structname)), "` has been disabled") | ||||||
|
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 sounds like it would always generate worse code than just making it mutable, so calling it an optimization seems misleading. Perhaps say something like "this will make a larger, slower, fragmented layout in order to make just those fields mutable"?
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.
Having those fields non mutable is a feature. The fact that is not faster is a compiler performance bug. It's up to you to make it fast ;).
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.
You're semantically requiring it to be slower though, so there's not much the compiler can do to undo that (other than convert it back to a mutable struct, if you're very lucky). You requiring the computer to do more work at runtime.
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.
It can do whatever it wants as long as
setfield
on the fields that are suppose to be immutable, errors. But we are all just beating around the bush that we don't haveThere 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.
Because
Ref
(orBox
) is mutable, it needs its own identity (must be GC'able independently) and so it cannot be allocated inline instruct Foo
(AFAICT). So you're stuck with n_lazy_fields small allocations instead of one bigger allocation, which was @vtjnash's point.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.
Yes, but the current implementation is the only way to get the desired semantics. Whenever there is a better way, I'll switch to it. And I probably won't merge this PR until there is one.