Skip to content
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

fix #20704, pure annotation should not skip method errors #20726

Merged
merged 1 commit into from
Feb 27, 2017

Conversation

JeffBezanson
Copy link
Member

There were two issues here:

  1. We should not inline a call to a @pure function as a constant unless the inferred argument type is a subtype of the method signature, to be sure there is no MethodError.
  2. Even if a function is marked @pure and is inferred to return a constant, we should not use the constant calling convention (CCC) for it unless we can prove it effect_free. That's because the CCC is equivalent to deleting the entire body of the function, which is not the same thing as calling the function at a different time.

Item (1) is an uncontroversial bug fix. Item (2) is more of an interesting corner case. I think most people would agree that if you put @pure on a function that prints, you shouldn't be surprised to see the printed output more or fewer times than you expected (including zero times). Similarly, consider

@pure function f()
    if isdir("/")
        error()
    end
    return 42
end

Here I'd even say it's ok to skip throwing the error, since who's to say whether isdir("/") is true in the imaginary land of pure functions. However I think method errors are different. We can only conclude that a function returns x if it actually calls a method that returns x. If the method that returns x is not called at all for a certain argument, it can't be the result even of a pure function.

# and to possibly enable more optimization in the future
me.const_api = true
end
if proven_pure || me.src.pure
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't inline me.src.pure functions, some of them can be very large

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, but we seem to have been doing that before! I guess I should just use proven_pure?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooops! and yes (merge with previous if statement)

It wasn't causing issues in the past since we also set me.const_api, so it should have managed to avoid actually inlining the whole function.

break
end
end
end
end
me.src.pure = ispure
if proven_pure
me.src.pure = true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem quite right, but do we ever actually look at this field? I suspect we should acknowledge that source.pure and inferred.pure and reflecting slightly different attributes (the former meaning @pure, the latter meaning proven_pure). There's a bit of a mis-match in terminology here though too, since we're using me.src.pure to mean the same as const_api / jlcall_api == 2. However, it's perfectly reasonable to know that a function is effect-free / pure, but not know what it returns.

@vtjnash
Copy link
Member

vtjnash commented Feb 21, 2017

While I agree with this PR and think we should make both changes 1 and 2, I don't think this fixes #20704. To fix that issue, we need to fix the condition here (https://github.com/JuliaLang/julia/pull/20726/files#diff-c0d983631c937ee5acfd0137cd7332deR3784) to prove that we've called the function at least once.

@JeffBezanson
Copy link
Member Author

I wanted to remove that block of code actually, but it broke a test in test/inference.jl that checks the behavior of a @pure function that calls rand(). I would rather say the behavior of such a function is undefined.

@JeffBezanson
Copy link
Member Author

@nanosoldier runbenchmarks(ALL, vs = ":master")

@StefanKarpinski
Copy link
Member

I would rather say the behavior of such a function is undefined.

Wouldn't that contradict the idea that @pure is just a way of giving the compiler permission to call the function at any time?

@JeffBezanson
Copy link
Member Author

Wouldn't that contradict the idea that @pure is just a way of giving the compiler permission to call the function at any time?

But when you call the function affects which random number you get. If you call it three times, will you get the same number or different ones? Depends on what the compiler decided to do, hence undefined.

@JeffBezanson
Copy link
Member Author

I should clarify I only meant this in a very narrow sense related to the test in question --- the test was checking that repeated calls returned the same value, which should not be mandated. We can continue to call the function at run time if we want to.

@nanosoldier
Copy link
Collaborator

Your benchmark job has completed - possible performance regressions were detected. A full report can be found here. cc @jrevels

@vtjnash
Copy link
Member

vtjnash commented Feb 22, 2017

I wanted to remove that block of code actually

If we can get rid of that, we can just get rid of @pure entirely. At least half of the reason it exists is to ensure that we can assume the functions are effect_free, entirely skip the cost of calling the function, and inline the result as a constant.

The test with rand() is just making sure that this facet is working.

@JeffBezanson
Copy link
Member Author

Yes, I see what you mean. I'll push a better version.

@JeffBezanson
Copy link
Member Author

@nanosoldier runbenchmarks(ALL, vs = ":master")


methsig = method.sig
if !(atype <: metharg)
return invoke_NF(argexprs, e.typ, atypes, sv, atype_unlimited,
invoke_data)
end

# check whether call can be inlined to just a quoted constant value
if isa(f, widenconst(ft)) && !method.isstaged && (method.source.pure || f === return_type) &&
are_args_const(atypes)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not sufficient to check that the result could have come from calling the pure function. We need to actually prove that it did come from calling the pure function. For example, we would be correct to infer that bar(1.0)::Const(1) in the original issue, even though calling bar(1.0) throws a method error.

Copy link
Member Author

@JeffBezanson JeffBezanson Feb 22, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we ever actually do that though? I believe any time these conditions are met, we will have called the function. I agree that's a bit brittle, but it's not easy to know where a type came from.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a simple example of the failure scenario – it doesn't even need arguments :P – but it's just a minor rewrite of #20704:

julia> function method_error end
method_error (generic function with 0 methods)

julia> Base.@pure a() = (method_error(); 1)
a (generic function with 1 method)

julia> b() = a()
b (generic function with 1 method)

julia> b()
1

I think we either need to recompute the Const here, or add a bool field to const (was_computed_by_apply_pure) that we can check here.

@nanosoldier
Copy link
Collaborator

Your benchmark job has completed - possible performance regressions were detected. A full report can be found here. cc @jrevels

@kshyatt kshyatt added the error handling Handling of exceptions by Julia or the user label Feb 22, 2017
@JeffBezanson
Copy link
Member Author

@nanosoldier runbenchmarks(ALL, vs = ":master")

@JeffBezanson
Copy link
Member Author

Ok, here's another attempt.

@@ -191,7 +193,7 @@ mutable struct InferenceState
vararg_type = rewrap(vararg_type, linfo.specTypes)
end
s_types[1][la] = VarState(vararg_type, false)
src.slottypes[la] = widenconst(vararg_type)
src.slottypes[la] = vararg_type
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can drop this part of the change. I don't foresee any need for inference or optimization to examine slottypes (it should be strictly suboptimal information relative to the dataflow-sensative types) – I think this is strictly consumed by codegen.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For slottypes in general that's true, but this was causing arguments to be given e.g. type Void instead of Const(nothing). Ideally we'd treat those the same anyway, but this was an easier way to achieve that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where are we using this info? Seems like this is a different bug that'll need tests if this is also causing issues.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The weird thing is that just a few lines above, we explicitly do

                if isa(atyp, DataType) && isdefined(atyp, :instance)
                    atyp = Const(atyp.instance)

and then call widenconst(atyp)! I'll try to find a test case.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found it: exprtype reads slottypes, and inlining and some other passes call exprtype. It takes a really obscure case to trigger it. For example f(x) = x(), where f is inferred on the type of a singleton pure function. x() won't get inlined in that case because it's not a Const.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E.g.

Base.@pure function f(x)
               rand()
               42
              end

julia> h(x) = x(nothing)
h (generic function with 1 method)

julia> code_typed(h, (typeof(f),))
1-element Array{Any,1}:
 CodeInfo(:(begin 
        return $(Expr(:invoke, MethodInstance for f(::Void), :(x), :(Main.nothing)))
    end))=>Int64

@@ -1569,7 +1571,7 @@ function pure_eval_call(f::ANY, argtypes::ANY, atype::ANY, vtypes::VarTable, sv:
try
value = Core._apply_pure(f, args)
# TODO: add some sort of edge(s)
return abstract_eval_constant(value)
return Const(value,true)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing space after the comma


methsig = method.sig
if !(atype <: metharg)
return invoke_NF(argexprs, e.typ, atypes, sv, atype_unlimited,
invoke_data)
end

# check whether call can be inlined to just a quoted constant value
if isa(f, widenconst(ft)) && !method.isstaged
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any idea why this isa check is here? AFAIK, we should be able to drop the f argument, but this check gave me pause, from uncertainty.

@nanosoldier
Copy link
Collaborator

Your benchmark job has completed - possible performance regressions were detected. A full report can be found here. cc @jrevels

@JeffBezanson JeffBezanson merged commit d3de8cc into master Feb 27, 2017
@vtjnash vtjnash deleted the jb/fix20704 branch February 27, 2017 22:11
@vtjnash
Copy link
Member

vtjnash commented Feb 27, 2017

Were all those regressions just noise then? It would have been nice to address those with at least a comment before merging.

@tkelman
Copy link
Contributor

tkelman commented Feb 27, 2017

those look too large and consistent to be noise

@JeffBezanson
Copy link
Member Author

Which set of results are you looking at?

@vtjnash
Copy link
Member

vtjnash commented Feb 28, 2017

The last one run (#20726 (comment)) had a max 2.38x slowdown and max 0.30x speedup. It seems neither was expected?

@JeffBezanson
Copy link
Member Author

We've seen the sqrtm slowdown before --- did we ever figure out what it was?

I'll look at the IR for some of the vector operations.

@vtjnash
Copy link
Member

vtjnash commented Feb 28, 2017

daily benchmark did worse overall as well: (https://github.com/JuliaCI/BaseBenchmarkReports/blob/03d8b02711ecfc121c7935b5a20c66645fdd3f0a/daily_2017_2_28/report.md). Maybe this commit broke the inliner heuristics?

@jrevels
Copy link
Member

jrevels commented Feb 28, 2017

JuliaCI/BaseBenchmarks.jl#65 is potentially relevant here.

@JeffBezanson
Copy link
Member Author

Found something: code for _broadcast_eltype is no longer getting fully replaced with the resulting type. I'll try to fix it.

@vtjnash
Copy link
Member

vtjnash commented Feb 28, 2017

Ah, that makes sense. It looks like that one would be fairly hard for it to prove is pure, given our current lack of a framework for propagating the purity computation. How did it manage to get elided before?

@Sacha0
Copy link
Member

Sacha0 commented Mar 6, 2017

Ref. #20802 (comment) for what seems to be another related regression. Best!

JeffBezanson added a commit that referenced this pull request Mar 6, 2017
JeffBezanson added a commit that referenced this pull request Mar 7, 2017
improve `effect_free` to fix regressions from #20726
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
error handling Handling of exceptions by Julia or the user
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants