-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
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
Enable operator-sensitive extension of element-type promotion #12292
Conversation
I'd love a few more words of explanation. (Besides, I enjoy danger.) |
Yes, with apologies, we cannot do this sort of thing. Program behavior cannot depend on return_types. There is no guarantee as to what that function will return. |
I know, it was a tongue-in-cheek response :-) We already have |
Oh wow, I see there is one in subarray. Looks like that is the only one. We need to remove that too. Don't know how I missed that. |
Well, it leads to interesting debugging sessions. |
heh, I had a bad feeling reading the title of the PR in my mail client. Unfortunately I feel that this restriction will only seem more arbitrary as inference gets better, but we have to be firm about that... |
and yeah, this one in a stagedfunction is incorrect if inference returns a non leaf in that case |
When inference breaks, in my experience it returns something wider than optimal. But at least that's still correct. In contrast, here's current master (a stripped-down version of SIUnits that hasn't gone out of its way to be type-unstable as a way of avoiding this bug): immutable MyType{T,N} <: Number
val::T
end
*{T}(x::MyType{T,1}, y::MyType{T,1}) = MyType{T,2}(x.val*y.val)
a = MyType{Int,1}[MyType{Int,1}(1)]
julia> a*MyType{Int,1}(1)
ERROR: MethodError: `convert` has no method matching convert(::Type{MyType{Int64,1}}, ::MyType{Int64,2})
This may have arisen from a call to the constructor MyType{Int64,1}(...),
since type constructors fall back to convert methods.
Closest candidates are:
MyType{T,N}(::Any)
call{T}(::Type{T}, ::Any)
convert{T<:Number}(::Type{T<:Number}, ::Base.Dates.Period)
...
in .* at arraymath.jl:110
in * at abstractarraymath.jl:55 In contrast, with this PR: julia> a*MyType{Int,1}(1)
1-element Array{MyType{Int64,2},1}:
MyType{Int64,2}(1) which is exactly what you want. To make life more interesting, let's try to break it following @carnaval's concern. Master: immutable MyType{T,N} <: Number
val::T
end
# This is deliberately type-unstable
*{S,T}(x::MyType{S,1}, y::MyType{T,1}) = x.val > y.val ? MyType{T,2}(x.val*y.val) : MyType{S,2}(x.val*y.val)
a = MyType{Int,1}[MyType{Int,1}(1)]
julia> a*MyType{Float32,1}(1.0f0)
ERROR: no promotion exists for MyType{Float32,1} and MyType{Int64,1}
in .* at arraymath.jl:108
in * at abstractarraymath.jl:55 This PR: julia> a*MyType{Float32,1}(1.0f0)
1-element Array{Union{MyType{Float32,2},MyType{Int64,2}},1}:
MyType{Int64,2}(1) There's absolutely nothing wrong with that, either. (And it's not a leaf type.) One giant pink inflatable mustache to the first person who gives a test case where this PR fails but master works. (Include shipping address and allow 6-8 weeks for delivery.) |
Doesn't the behavior of |
@timholy the one that was broken was in the subarray code (but I remember you saw it on the typeinf-free comphrension branch), where if But, in more generality, the problem with this kind of code is that it makes the fact that inference is enabled or not user visible. It is already the case today (typegoto, maybe cfunction ? although I'm not familiar with this one but it looks like you have to specify a return type explicitely here), but we should be removing such things not adding some. So, a concrete problem that could arise for example, is that the behavior of my program (e.g. my promoted array's type) could change at the whim of one of the various |
Only in the sense that whether a particular |
The bug you refer to was unfortunate---very sorry it caused you trouble. I'm completely fine with giving up that Aside from the fact that it usually works, the other good thing about this design is that in cases of trouble, just overload I'd argue that since we're currently in bad shape every place this comes up, and this makes things better, we should just fess up to the fact that we can't currently solve this problem without leveraging inference. We can aim for a more long-term fix in a future release. |
Yes, this "weak dependence" on type inference is not as bad. If you check the equivalent of Stefan is right about But there is a real problem to fix here. The leading approach is |
Also the promote_rule for SIUnits is arguably broken. Promoting two numbers is supposed to return two values of the same type. This is only meant to assist dispatch by mapping O(n^2) argument combinations to O(n). I agree that using promotion to determine the result of array + is broken; all it can do is convert values to a common type, and nothing more. |
@timholy but the problem IMO is that the failure mode of a wider array is visible behavior. Imagine we implement a heuristic that makes inference try harder on more powerful machines ? Or only when you precompile ? Or only when you pass I agree that those are not very practical concerns, but I feel this is an area we have to be more pedantic about since in the future we might have cases where we wouldn't be able to change the behavior of inference without breakage. (and no worries about the subarray bug obviously ;-)) |
Yes, many of the promotion rules in SIUnits pay loving attention to the type of the numeric coefficient, but completely punt on the units. That's quite ironic for a package whose raison d'etre is handling units. But I think defaulting to the generic types is basically defensive behavior necessitated by the bug that this PR fixes. |
@carnaval, I'd lobby for two inference engines, one stripped-down version specifically for handling this problem and the more powerful one for general use. |
What about using |
@timholy why not, but the stripped down version would have to have the characteristics of a static type system inference, that is, formal deduction rules for checking that only depends on syntax & an inference engine that bails out statically if the syntax is not explicit enough forcing you to add explicit type annotations |
@carnaval, that doesn't sound too bad to me. Especially since I am skeptical that there is another practical solution. |
The other practical solution is to do what |
Yeah, that would work too (EDIT: except for empty arrays, as I seemed to remember above but forgot here untill @mbauman reminded me below), but it does stink from a performance perspective. I imagine that operations like |
there is a way to write that so that if inference figures it out then it elides the slow path just as map does right ? |
I'm not convinced there is (currently), but I'd love to be proven wrong: julia> function foo{T}(x::Vector{T}, y::Number)
if isleaftype(T)
y
else
error("oops")
end
end
foo (generic function with 1 method)
julia> foo(rand(2), 3.5)
3.5
julia> @code_llvm foo(rand(2), 3.5)
define double @julia_foo_21033(%jl_value_t*, double) {
top:
%2 = alloca [5 x %jl_value_t*], align 8
%.sub = getelementptr inbounds [5 x %jl_value_t*]* %2, i64 0, i64 0
%3 = getelementptr [5 x %jl_value_t*]* %2, i64 0, i64 2
store %jl_value_t* inttoptr (i64 6 to %jl_value_t*), %jl_value_t** %.sub, align 8
%4 = getelementptr [5 x %jl_value_t*]* %2, i64 0, i64 1
%5 = load %jl_value_t*** @jl_pgcstack, align 8
%.c = bitcast %jl_value_t** %5 to %jl_value_t*
store %jl_value_t* %.c, %jl_value_t** %4, align 8
store %jl_value_t** %.sub, %jl_value_t*** @jl_pgcstack, align 8
store %jl_value_t* null, %jl_value_t** %3, align 8
%6 = getelementptr [5 x %jl_value_t*]* %2, i64 0, i64 3
store %jl_value_t* null, %jl_value_t** %6, align 8
%7 = getelementptr [5 x %jl_value_t*]* %2, i64 0, i64 4
store %jl_value_t* null, %jl_value_t** %7, align 8
%8 = load %jl_value_t** %4, align 8
%9 = getelementptr inbounds %jl_value_t* %8, i64 0, i32 0
store %jl_value_t** %9, %jl_value_t*** @jl_pgcstack, align 8
ret double %1
} I haven't looked at the code produced by |
I don't think there is any reason for all this gcframe traffic, this should end up as |
dest = zeros(10^6)
src = rand(10^6)
julia> @time Base.promote_to!(Base.IdFun(), 1, dest, src);
7.229 milliseconds (4 allocations: 144 bytes)
julia> @time copy!(dest, src);
2.877 milliseconds (4 allocations: 144 bytes) Not as bad as I'd feared, but it's still more than two-fold. |
Somehow type inference doesn't know anything about |
Oh man, I'd love to see this solved. I think that the major downside to using |
@yuyichao even if we would get tighter type info if inference special cased leaftype for branches, codegen should still not generate an obviously useless gcframe here |
It's a nice scheme. I suspect that the main limitation would be handling types that take multiple parameters (more than just Long-term, I suspect the best way to handle this would be a separate |
The parameter immutable OtherMeter{T, N} end
typicity{M1<:OtherMeter, M2<:OtherMeter}(::Base.MulFun, ::Type{M1}, ::Type{M2}) = Polytypic{2}() to obtain the desired behavior:
An alternative approach may become necessary if one is intent on treating argument signatures in which the location of the relevant parameter (be it typicity{M1<:Meter, M2<:OtherMeter}(::Base.MulFun, ::Type{M1}, ::Type{M2}) = Polytypic{0}()
degree_ind{T<:Meter}(::Type{T}) = 1
degree_ind{T<:OtherMeter}(::Type{T}) = 2
function promote_type_radical{R<:Meter, S<:OtherMeter}(::Type{R}, ::Type{S})
T = S.parameters[1]
return OtherMeter{T}
end
function promote_typicity{R, S}(::Polytypic{0}, ::Type{R}, ::Type{S})
degree = R.parameters[degree_ind(R)] + S.parameters[degree_ind(S)]
degree == 0 ? Unity : promote_type_radical(R, S){degree}
end julia> promote_op(*, Meter{1}, OtherMeter{Int, 1})
OtherMeter{Int64,2} Note that I've replaced the use of I suspect that most cases would be able to get by fine using the first approach, i.e. using the parameter in |
But suppose you have a type with 3 parameters and the My overall view is that this might be appropriate for a package but probably is not sufficiently general for Base. One might make this work for SIUnits-like types, but 20a0b84 doesn't seem to be implementable by this scheme. |
Barring a "give me a little more time to review" request or further discussion of the core elements of this PR, I've inserted a merge reminder into by calendar for first thing Wednesday morning. That gives folks another 48 hours. |
Looks like Images (and maybe other packages?) is using |
@timholy that's a good point. I do think it's possible to make it work -- one would need a complement to I wouldn't expect to cover all patterns with just the promote_typicity{R, S}(::Monotypic, ::Type{R}, ::Type{S}) = R to replace something like Base.promote_op{P<:Period,R<:Real}(::$F, ::Type{P}, ::Type{R}) = P with typicity{P<:Period, R<:Real}(::$F, ::Type{P}, Type{R}) = Monotypic() Of course, some cases wouldn't fall into a well-defined, pervasive return type promotion pattern. In these cases, it's probably easiest just to override |
Being able to infer the type of some arithmetic operations is useful. We already have functors here: https://github.com/JuliaLang/julia/blob/master/base/functors.jl What about we just add result_type{X,Y}(::AddFun, ::Type{X}, ::Type{Y}) = #... Also, the |
promote_op(F, R, S) computes the output type of F(r, s), where r::R and s::S. This fixes the problem that arises from relying on promote_type(R, S) when the result type depends on F.
Also adds developer documentation on overloading `promote_op`.
Rebased on static functors (and on the latest master). Not a Since there's no substantive change, I'll merge later today once CI passes. |
Hmm, I haven't seen this travis failure before: $ cp /tmp/julia/lib/julia/sys.ji local.ji && /tmp/julia/bin/julia -J local.ji -e 'true' && /tmp/julia/bin/julia-debug -J local.ji -e 'true' && rm local.ji
cp: cannot stat `/tmp/julia/lib/julia/sys.ji': No such file or directory
The command "cp /tmp/julia/lib/julia/sys.ji local.ji && /tmp/julia/bin/julia -J local.ji -e 'true' && /tmp/julia/bin/julia-debug -J local.ji -e 'true' && rm local.ji" exited with 1.
0.01s$ /tmp/julia/bin/julia -e 'versioninfo()'
/home/travis/build.sh: line 41: /tmp/julia/bin/julia: No such file or directory Oh, how I dream of world where CI failures make sense. I'll wait a few minutes before restarting it, in case anyone wants to poke at it. |
Scroll up. Download failure, I think. The caching server hasn't been doing all that great a job lately.
|
More specifically: curl: (22) The requested URL returned error: 403
make[2]: *** [libgit2-159061a8ce206b694448313a84387600408f6029.tar.gz] Error 22
...
curl: (22) The requested URL returned error: 403
make[2]: *** [utf8proc-85789180158ac7fff85b9f008828d6ac44f072ea.tar.gz] Error 22 Errors from parallel make are always terrible. I just search for the word "error" and try to find the first meaningful one. |
Aha, good eyes Matt. We could turn off parallel make on Travis if it would help make the error messages any more sensible, but the queue has actually been worse on Travis than on AppVeyor ever since the win64 freeze bug was resolved. We could also turn OSX Travis back off, it might help things along a little faster with the queue if it's worth potentially not automatically catching platform differences. |
Thanks for the tips, folks. At the current moment, I'm not sure Traivs-OSX is notably slower than Travis-Linux. |
Enable operator-sensitive extension of element-type promotion
@tkelman How hard it is to implement on travis the logic to skip the build if there's a new commit on the same PR like what we have on AppVeyor |
@yuyichao not very, let me find where I last put that code. I think I did something like that for libgit2 and it was on their master for a few days but then they reverted it. |
This fixes the long-standing #8027, providing much better generic fallbacks for elementwise operations involving arrays and array/scalar operations. The proximal motivation for tackling this was to fix the ambiguity warnings triggered by #12115, but this will also (given suitable type-stability improvements to SIUnits) finally allow
eltype([1Meter]+1Meter) == typeof(1Meter)
andeltype([1Meter]*(1Meter)) == typeof(1Meter^2)
. In some ways this turns out to be the (much easier) companion of #10525, defining operations that involve arrays in terms of operations defined for scalars. I think it will allow cleanup of a certain amount of torturous code.I had forgotten why we had
promote_array_type
; to save any reviewers the trouble, it turns out to be because we've decided thateltype([1.0f0]+1.0) == Float32
(i.e., the eltype of the Array wins, when working withReal
numbers). This rule obviously doesn't work for more generic types, but the exception does make sense so I've kept it.If this meets with approval, I'll see about applying it to #12115.