-
-
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
generated functions, next generation #23168
Conversation
Do we have any other meta node that changes the behavior of a program that doesn't have UB? |
I think there are basically two kinds of metadata: one that's consumed by the method definition process, and one that persists in the IR. I would allow method-def metadata to change program behavior, but not metadata that stays in the IR. There's no other extensible way to convey information between stages. It would be ok to use a different expression head (instead of |
I'm OK with this only in that I want it to become:
|
Yep, that's where I'm going with this :) |
In fact, we could lower the current
Then I was thinking of using this syntax in general:
|
I think we might want to bikeshed the name to give a better idea what this does (something like |
I was thinking about this, and I believe the compiler should not do this. The compiler should try to remain sound even if the implementations don't match. We can freely pick whether to use the generator or not, but we need to be consistent: we can't infer one implementation if there's a chance the other will be called at run time. |
We cannot possibly bound that chance (except in trivial cases like guaranteeing the generated function will always be called or never be called for a given system image build), so this would be exactly equivalent to saying "if it's a generated function, we can't infer it", which would be terrible. It's perfectly fine for the fallback implementation to be uninferrable, the generated tfunc just must never violate the monotonicity assumption. |
I think for a particular call site, if we want to infer anything other than |
OK, here is a more fleshed-out implementation. If a function contains a To replicate existing generated-only methods, I add a (very unattractively named) Open season for bikeshedding on all the names and such. As an added bonus, this makes it much easier to change the behavior in #12783 (if we want), since |
stagedfunction
expression head
How about renaming this to get rid of the vestigial "staged function" terminology? The symbol could be |
Oh, sorry, I see that you already did that. Carry on. |
85e88f7
to
bbb9284
Compare
src/method.c
Outdated
memcpy(&gargs[1+spl], args, nargs * sizeof(void*)); | ||
if (def->isva) { | ||
gargs[totargs-1] = jl_f_tuple(NULL, &gargs[1+spl+def->nargs-1], nargs - (def->nargs-1)); | ||
gargs[1+spl+def->nargs-1] = gargs[totargs-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.
could use a lot more spaces here to help legibility
src/method.c
Outdated
size_t world = generator->def.method->min_world; | ||
const char *F = jl_compile_linfo(&generator, (jl_code_info_t*)generator->inferred, world, &jl_default_cgparams).functionObject; | ||
fptr = jl_generate_fptr(generator, F, world); | ||
size_t spl = jl_svec_len(sparam_vals); |
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.
could use a better variable name
src/method.c
Outdated
@@ -275,9 +274,11 @@ JL_DLLEXPORT jl_code_info_t *jl_code_for_staged(jl_method_instance_t *linfo) | |||
jl_expr_t *ex = NULL; | |||
jl_value_t *linenum = NULL; | |||
jl_svec_t *sparam_vals = env; | |||
jl_method_instance_t *generator = linfo->def.method->generator; | |||
jl_value_t *generator = linfo->def.method->generator; | |||
jl_method_t *gen_meth = jl_gf_mtable(generator)->defs.leaf->func.method; |
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 super ugly
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.
Any suggestions?
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, some of this kind of complexity will go away if we eventually deprecate generated-only methods.
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.
Seems like the names either need to be a characteristic of the original function being optimized, or the generator should be just a single Method object (rather than a function)
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 problem is that it's possible to have a Method with a NULL source
field. Instead we could populate it with empty IR, or IR that throws an error, or IR that somehow otherwise indicates it doesn't actually work.
Dealing with an individual Method object is difficult, since the front end can't express it, and we needed to duplicate code from jl_call_method_internal
to call it.
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.
Either option there sounds OK. When #14919 is implemented, I expect the MethodTable type to vanish (anyways, that's what happens on my branch that fixes that issue), so I would like to avoid adding spurious dependencies on that type which will just need to be deleted.
Somehow this is causing |
I get:
|
bbb9284
to
9ebcd00
Compare
9ebcd00
to
944e754
Compare
Oh goodie, the problem went away... |
Is it possible to see a representative example how this is used? |
Here's one example
There are two complete implementations of this method: a generated one in the So the design I implemented looks like this:
That gives two implementations: a generated one where |
Ok, I think this is basically done. I think it also simplifies the implementation a bit, with less implemented in C. |
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 would add that this looks really like a local scope eval, except that of course the expression is only allowed to be constructed from compile time info. It does make seeing what the generator can see slightly strange.
doc/src/manual/metaprogramming.md
Outdated
|
||
In this style of definition, the code generation feature is essentially an optional | ||
optimization. | ||
The compiler will use it if convenient, but otherwise may choose to use the normal |
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.
So does this mean that if a non-generated version is too complicated to write/rewrite one should/can still use @generated
on the whole function, which will add the generated_only
metadata, and it'll be required to call the generator?
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.
Correct. You can still use @generated
on the whole function, but now we have the option of deprecating that if want, and requiring alternate implementations.
test/staged.jl
Outdated
y = (2x, typeof(x)) | ||
end | ||
push!(a, 2) | ||
return y |
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.
Can we have a test/do we support functions with multiple generated blocks?
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, multiple generated blocks should work. You just get two versions, one with the then
part of each generated block selected, and one with the else
parts.
One other feature I just noticed of this is that we can probably later make closure-lowering / comprehensions work (for the non-generated parts), which will be quite nice! |
base/multidimensional.jl
Outdated
ΔI = first(Rdest) - first(Rsrc) | ||
# TODO: restore when #9080 is fixed | ||
# for I in Rsrc | ||
# @inbounds dest[I+ΔI] = src[I] |
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.
remove comment?
@ncall $N tuple t | ||
end | ||
else | ||
Tuple(f(i) for i = 1:N) |
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.
Why is this attempting to use the Tuple
constructor? I realize this is sort of a special-case scenario where we know people won't attempt to copy this function elsewhere, but this is still just a rather surprising "gotcha" in behavior (since we'll need a different definition of this function for other top modules like Core.Inference).
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 don't understand the problem. Why will we need to add this definition to Core.Inference?
base/sysimg.jl
Outdated
M > N && throw(ArgumentError("input tuple of length $M, requested $N")) | ||
if @generated | ||
quote | ||
$(Expr(:meta, :inline)) |
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.
seems like the wrong place for this meta expression to get placed in the function
base/sysimg.jl
Outdated
if @generated | ||
quote | ||
$(Expr(:meta, :inline)) | ||
(t..., $(fill(:val, N-length(t.parameters))...)) |
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 new expression isn't inferable and defeats the whole point of having @generated
in this function
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's the same as before, just using fill
instead of a comprehension.
doc/src/manual/metaprogramming.md
Outdated
the allowable constructs. | ||
When a generated function is called, the expression it returns is compiled and then run. | ||
To make this efficient, the result is usually cached. And to make this inferable, only a limited | ||
subset of the language is usable during expression generation. Thus, generated functions provide a |
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 also true, but I think the original statement was trying to emphasize that some language features (closures, generators, global, etc. – e.g. anything that causes an additional side-effects) are not valid for use in the returned function body.
@@ -738,7 +738,8 @@ function length(mt::MethodTable) | |||
end | |||
isempty(mt::MethodTable) = (mt.defs === nothing) | |||
|
|||
uncompressed_ast(m::Method) = uncompressed_ast(m, isdefined(m, :source) ? m.source : m.generator.inferred) | |||
uncompressed_ast(m::Method) = isdefined(m,:source) ? uncompressed_ast(m, m.source) : |
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.
Conditionally returning the source code from a different semi-related method seems sort of random. I think it would be better to now just use the trivial definition (uncompressed_ast(m::Method) = uncompressed_ast(m, m.source)
)
src/method.c
Outdated
m->generator = jl_toplevel_eval(m->module, jl_exprarg(st, 1)); | ||
} | ||
JL_CATCH { | ||
} |
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.
Why are we actively ensuring the backtrace and error message get lost?
src/method.c
Outdated
else if (jl_expr_nargs(st) == 2 && jl_exprarg(st, 0) == (jl_value_t*)generator_sym) { | ||
m->generator = NULL; | ||
JL_TRY { | ||
m->generator = jl_toplevel_eval(m->module, jl_exprarg(st, 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.
I feel like our deserialize
code might be assuming that this function is effect-free / non-reentrant.
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, what sorts of code is this expecting to potentially encounter? The method-world-counter code, and the multithreaded-toplevel-lock code also strongly assume / require that this function must be effect-free and non-reentrant (both are currently quite bad at enforcing it, but that's besides the 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.
The expression it expects is (new (core GeneratedFunctionStub) globalvar constant constant constant constant)
, so all it does is read two global vars and allocate a struct. I could do that manually instead of calling eval
if you think it's necessary.
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 chosen if @generated
syntax seems oddly inconsistent with the feel of normal @generated
which (at least conceptually) applies to the function block following it just like a normal macro. How about putting the implementation of code rearrangement for normal-vs-generated parts inside the @generated
macro itself? Then perhaps some of the special purpose parser changes could go away?
Then we'd potentially have something like
@generated function foo(a)
if __generated__
quote
a + 1
end
else
a + 1
end
end
With the presence of if __generated__
signifying that code rearrangement and splicing should be performed by @generated
. (In the absence of this we'd have to assume generated_only
for compatibility.)
src/ast.scm
Outdated
(and (length= e 4) (eq? (car e) 'if) (equal? (cadr e) '(generated)))) | ||
|
||
(define (generator-meta? e) | ||
(and (length= e 3) (eq? (car e) 'meta) (eq? (cadr e) 'generator))) |
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 seems a little confusing to have Expr(:meta, :generator)
, which is a completely different and unrelated thing from Expr(:generator, ...)
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.
Fair point. I could use generated
instead.
doc/src/manual/metaprogramming.md
Outdated
the first block in `if @generated` is used, and a normal one where the `else` block is used. | ||
Notice that we added an error check to the top of the function. | ||
This code will be common to both versions, and is run-time code in both versions | ||
(in other words, it will be quoted and returned as an expression from the generated version). |
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'm curious: does the implementation allow for multiple such blocks of common code interspersed with optionally generated code?
[Edit: I see that this is already asked & answered elsewhere. Would be worth making it clear in the documentation]
A cool feature of |
a8bc71a
to
a5b540c
Compare
Cool, having other macros which expand to |
In this style of definition, the code generation feature is essentially an optional | ||
optimization. | ||
The compiler will use it if convenient, but otherwise may choose to use the normal | ||
implementation instead. |
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.
Some guidance on when to use this style versus the generated-only style would be good.
I love the proposal to generate just part of a function (and associated macro possibilities!), but I'm a bit worried by the debugging/testing fallout from having two definitions of a function. Does it mean that a faulty
|
It is essential that the two implementations do the same thing. Which version is used is a compiler implementation detail. |
Then it would be good to have a tool which can test both code paths. |
Very much agreed. Any ideas on what its mechanics should be? One possibility is a command line switch to disable |
a5b540c
to
e87f487
Compare
I feel like I'm going to want to test both generated and non-generated implementations of a given function within the same julia session. For example, to compare numerical results, to get coverage correct, or to just iterate on the code in a reasonably productive way. Is it possible to have a compiler annotation at the call site, in the spirit of Ideally I hope this new version of |
e87f487
to
d01475b
Compare
…onal optimizers use meta nodes instead of `stagedfunction` expression head
d01475b
to
7168542
Compare
Party time! I've got to try this out! |
This looks really cool. In the manual it states in the sub2ind example that the compiler may use the "then" part of the if - the generated part, or may use the "else" part of the if, the default part, and that it is up to the compiler. In julia I suppose many of us have a good feel from looking at some code whether or not static dispatch, unboxing and other optimisations occur, from looking at the code style and use of types etc (and of course we can check!). Is there any general advice on how to develop this kind of feel for |
Currently the |
Updated description: #23168 (comment)Now: #23168 (comment)
This also replaces the
stagedfunction
expression head with metadata. It's more flexible, there are fewer pieces of code that need to propagate thestaged
bit, and we won't need to worry about handlingstagedfunction
expressions anywhere.Interestingly,
stagedfunction
was neither an AST form nor an IR form, only returned from the@generated
macro and immediately lowered away, so it probably doesn't even need a deprecation.