-
Notifications
You must be signed in to change notification settings - Fork 48
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
Improve precompilation with concrete typing #436
Conversation
src/parse.jl
Outdated
@@ -150,6 +150,7 @@ function _parse_colorant(desc::AbstractString) | |||
|
|||
error("Unknown color: ", desc) | |||
end | |||
_parse_colorant(desc::AbstractString) = _parse_colorant(String(desc)) |
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 _parse_colorant
is an internal function, so I don't think we need to provide this method. Alternatively, we can force the conversion at higher levels (i.e. Base.parse
).
Base.parse(::Type{C}, desc::AbstractString) where {C<:Colorant} = _parse_colorant(C, supertype(C), String(desc))
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.
Also, I think we can rewrite the workaround as follows in Julia v1:
function Base.parse(::Type{C}, desc::AbstractString) where {C<:Colorant}
c = _parse_colorant(String(desc))::Colorant # BTW, we know that `c` is a `Colorant`
supertype(C) <: Colorant ? convert(C, c)::C : c
end
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.
Indeed I don't think we need the supertype
stuff at all now.
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.
That's right. However, I am concerned that convert
will result in a slight degradation in speed performance. (ColorTypes.jl defines the method to pass through and I'm not sure why it affects the speed. 😕)
Even so, it does improve the following strange behavior.:smile:
julia> parse(Colorant{Float32}, "red")
RGB{N0f8}(1.0,0.0,0.0) # current
RGB{Float32}(1.0f0,0.0f0,0.0f0) # force conversion
This is just for your reference and I don't think the slowdown is fatal.
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 optimizer elides the call to convert
when it's a no-op:
julia> @code_typed parse(Colorant, "red")
CodeInfo(
1 ─ %1 = invoke Colors._parse_colorant(_3::String)::Any
│ Core.typeassert(%1, Colors.Colorant)::Colorant
│ %3 = π (%1, Colorant)
└── return %3
) => Colorant
julia> @code_typed parse(RGB{N0f8}, "red")
CodeInfo(
1 ─ %1 = $(Expr(:static_parameter, 1))::Core.Const(RGB{N0f8}, false)
│ %2 = invoke Colors._parse_colorant(_3::String)::Any
│ Core.typeassert(%2, Colors.Colorant)::Colorant
│ %4 = π (%2, Colorant)
│ %5 = Colors.convert(%1, %4)::Any
│ Core.typeassert(%5, $(Expr(:static_parameter, 1)))::RGB{N0f8}
│ %7 = π (%5, RGB{N0f8})
└── return %7
) => RGB{N0f8}
The type-assert is necessary because _parse_colorant
has a sufficiently-complicated Union
that inference justifiably chooses to declare it Any
.
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.
Hmm, that is surprising. It doesn't seem to be fixed by the supertype
check. I am a bit puzzled. I'd stilll say this is an improvement, and that we can look into the optimizer issues later.
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 added a ::Colorant
assertion, thinking it would rather help the optimizer, but it seems to be the cause.:sob:
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 probably saw this, but for the benefit of all: xref JuliaLang/julia#37135.
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.
Check out this implementation:
function Base.parse(::Type{C}, desc::AbstractString) where {C<:Colorant}
c = _parse_colorant(String(desc))
return isconcretetype(C) ? C(c)::C : c
end
julia> @btime parse(Colorant, "red");
404.800 ns (5 allocations: 144 bytes)
julia> @btime parse(RGB{N0f8}, "red");
501.647 ns (5 allocations: 144 bytes)
Yet of course the second will be better if the return type is used in other code, since it will assure the compiler that it can do compile-time lookup of all the calls. Conversely, just knowing it's a colorant doesn't limit the space of future calls very much (e.g., think of convert(C2, c)
where C2
is some other colorspace--there are far too many choices for union/world-splitting).
I vote we go with this implementation?
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.
Hmm, that one was wrong...how about this:
function Base.parse(::Type{C}, desc::AbstractString) where {C<:Colorant}
c = _parse_colorant(String(desc))
return C === Colorant ? c :
isconcretetype(C) ? convert(C, c)::C : convert(C, c)
end
julia> @btime parse(Colorant, "red");
407.970 ns (5 allocations: 144 bytes)
julia> @btime parse(RGB{N0f8}, "red");
539.243 ns (5 allocations: 144 bytes)
julia> @btime parse(RGB, "red");
521.741 ns (5 allocations: 144 bytes)
How was that number measured? I think the value is highly dependent on the packages you load. The reason I have a concern with this is because of its relevance to PR #427. If necessary, I can split the PR #427 into fixes related to the As mentioned in JuliaGraphics/ColorTypes.jl#213 (comment), I'm beware of breaking changes in Colors.jl. |
ba20e29
to
be8085a
Compare
Codecov Report
@@ Coverage Diff @@
## master #436 +/- ##
==========================================
- Coverage 82.01% 81.31% -0.71%
==========================================
Files 10 10
Lines 862 867 +5
==========================================
- Hits 707 705 -2
- Misses 155 162 +7
Continue to review full report at Codecov.
|
MSC was being called with non-inferrable argument types and consequently was an invalidation tigger (and risk). This also specifies concrete types for some of our bigger functions, which makes them more likely to compile only once, and then uses small normalization wrappers to handle argument diversity. This cuts out more than a dozen invalidations.
be8085a
to
80f1641
Compare
Just |
I don't think that's necessary, this is quite orthogonal to that work. Which, by the way, is very welcome in its own right. |
I didn't see a clear difference on the nightly build (Commit bd65e85ca8) for Windows. However, the revised changes in this PR seem to make things better. |
OK to merge? |
It might be a good practice to check the performance with Edit: after: #436 (comment) julia> julia> versioninfo()
Julia Version 1.5.0
Commit 96786e22cc (2020-08-01 23:44 UTC)
Platform Info:
OS: Windows (x86_64-w64-mingw32)
CPU: Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz
WORD_SIZE: 64
LIBM: libopenlibm
LLVM: libLLVM-9.0.1 (ORCJIT, skylake)
julia> s = "lightgoldenrodyellow"; ss = SubString(s, 1)
"lightgoldenrodyellow"
julia> @btime parse(Colorant, $s);
737.390 ns (3 allocations: 128 bytes) # before
732.300 ns (3 allocations: 128 bytes) # after
julia> @btime parse(Colorant, $ss);
752.174 ns (3 allocations: 128 bytes) # before
753.381 ns (4 allocations: 176 bytes) # after I think it is OK to ignore the allocation within |
FYI JuliaLang/julia#37192 gave me this: julia> @btime parse(Colorant, "red");
341.559 ns (1 allocation: 16 bytes) compared to julia> @btime parse(Colorant, "red");
407.970 ns (5 allocations: 144 bytes) above. |
MSC
was being called during precompilation with non-inferrable argument types and consequently was an invalidation tigger (and risk). This also specifies concrete types for some of our bigger functions, which makes them more likely to compile only once, and then uses small normalization wrappers to handle argument diversity.This cuts out more than a dozen invalidations.