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

Flesh out calls, indirect calls, and function pointers. #278

Closed
wants to merge 1 commit into from

Conversation

sunfishcode
Copy link
Member

The function pointer type and its operations (conversions, comparisons)
were previously only implied. This patch documents them like regular
types and operations.

It also lays the groundwork for a future with multiple-return-value
functions. This is done by eliminating the void type and defining
return types as a sequence. In the MVP, the sequence can have at most
one value.

@kg
Copy link
Contributor

kg commented Jul 22, 2015

This looks reasonable to me from a first glance. I'll try to review it again in depth later.

FWIW, this does seem to conflict with multiple past assumptions and ideas about our expression trees.

In the past we assumed assignments have a single target with an expression on the RHS. This introduces the idea of assignments to N targets, where right now we're constraining N to 0 or 1, but eventually it will be more.

  • This implies some sort of generalization, I think? I don't really know how we want to express this. I think maybe function calls become a special case and they can only assign their result to a local? Otherwise we need a generalized 'multiple assignment' concept and that's really confusing.
  • This also implies that the previously proposed idea of inferring subexpression types based on the expression of the outer type is no longer viable.

We probably need a way to indicate that one or more of a function's return values are unused. This will be important for optimization and general codegen, I think. With return values as out-pointer parameters, this is relatively simple because you just pass a nullptr and that turns into an easily eliminated if (p) branch on a constant.

@sunfishcode
Copy link
Member Author

The idea with multiple return values (in FutureFeatures.md) is that they'd only be able to participate in expression trees if they have exactly one result (a mild extension of the existing rule that they can only participate in expression trees if they have exactly one user). Consequently, the only things that would have to accept multiple results would be special forms of set_local and return, and in terms of the binary encoding, these could just reference the input node as if it were a regular operand and then just implicitly wire up all the operand's values to the appropriate sinks. From the binary encoding proposals I've seen, I believe this should fit in pretty naturally.

@kg
Copy link
Contributor

kg commented Jul 22, 2015

Maybe we narrow this down and have a concept of 'most recent function return values'? So instead of (strawman):

local @a, @b, ...
setlocals [@a, @b] (call @fn (const 1), const (2))
call @print_two (getlocal @a), (getlocal @b)

something like:

call @fn (const 1), (const 2)
call @print_two (retval 0), (retval 1)

Maybe we layer some validation rules onto this, like that retvals can only be used once, and accessing a retval that isn't currently valid traps. I think those can be trivially verified because they're sequential, as long as they don't cross a branch.

Given a model like this, for a call with a single return value, you can easily decompose it:

call @print_one (call @sin (const 0.5))

->

call @sin (const 0.5)
call @print_one (retval 0)

This would potentially eliminate a lot of mostly-wasted local slots and eliminate the need for some liveness analysis on locals that are used once. That's probably good. It also makes it easier to tell whether a given return value is ever used.

@sunfishcode
Copy link
Member Author

As we discussed on IRC, let's move the discussion of how to efficiently encode multiple return values into a different PR, because it's an interesting but separate topic.

@lukewagner
Copy link
Member

Nice work capturing all of this.


* `void`: no value
Copy link
Member

Choose a reason for hiding this comment

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

I'd like this document to explain why there's no void type. I'd also like to understand what a function does return if it doesn't return anything in source form. Maybe this can be in the "calls" section, when you explain that a function can return 0 elements?

@sunfishcode
Copy link
Member Author

Because Github hides it, this PR is blocked as of @titzer's remark above:

I don't think the PR represents consensus yet, so maybe it's more appropriate to make an issue for discussion first?

#89 already exists as the issue for discussion.

@lukewagner
Copy link
Member

I'd suggest:

  1. Splitting out multiple-return values into a separate PR (although I think in the other issue we had some rough consensus about how to do that).
  2. Having type-specific function-pointer types since for some impl strategies I can see it avoiding dynamic signature-match checks.
  3. Instead of having the index order defined implicitly, having an explicit func-ptr-table declaration (effectively moving the "vtable optimization" from future features into the MVP, since I see little cost and immediate applicability).

@titzer What are the open issues you're thinking about?

@rossberg
Copy link
Member

I agree with Luke's suggestion that function ids ought to be typed. What is the motivation for having runtime signature checks instead?

@jfbastien
Copy link
Member

Can we move discussions to separate issues instead of this PR, as discussed in #282? I think we have a few forks to make :-)

@lukewagner
Copy link
Member

@jfbastien This isn't a diff; it's the main conversation of a PR and thus won't disappear if the branch is updated which was the problem identified by #282.

@lukewagner
Copy link
Member

On further thought, I'm realizing that there is a lot more nuance to func-ptr types:

The big question is: what happens if you funcid.decode some bogus integer (out of range or, if we have typed funcids, type mismatch)? An obvious option would be to fault. However, this would inhibit the C compiler from aggressively representing C function pointers as funcids since sometimes (questionably legal) C code puts random bits into a func ptr, never calls that func ptr, and expects to get those bits back later. With faulting semantics, a C compiler would only be able to generate a funcid.decode when it dominated a call_indirect in which case there's little benefit from having funcids at all over simply having call_indirect take an int32 directly (and relying on the wasm backend to do any hoisting it can).

The other option is to have a non-faulting funcid.decode which has the property that int32.encode of funcid.decode of an int32 x equals x for all x, valid or not. Thus funcids would effectively be opaque int32s so the question is, again, what's the benefit over just having a call_indirect that took an int32. I can imagine a tricky impl strategy that represented funcids as function addresses by generating thunks on demand for invalid integer values (assuming this is a rare case), but I'm not sure if I'd want to do this over simply representing funcid as an int32.

Perhaps this is the same "need more impl experience" situation you were thinking about @titzer?

@titzer
Copy link

titzer commented Aug 14, 2015

On Fri, Aug 14, 2015 at 12:03 PM, Luke Wagner notifications@github.com
wrote:

On further thought, I'm realizing that there is a lot more nuance to
func-ptr types:

The big question is: what happens if you funcid.decode some bogus integer
(out of range or, if we have typed funcids, type mismatch)? An obvious
option would be to fault. However, this would inhibit the C compiler from
aggressively representing C function pointers as funcids since sometimes
(questionably legal) C code puts random bits into a func ptr, never calls
that func ptr, and expects to get those bits back later. With faulting
semantics, a C compiler would only be able to generate a funcid.decode
when it dominated a call_indirect in which case there's little benefit
from having funcids at all over simply having call_indirect take an int32
directly (and relying on the wasm backend to do any hoisting it can).

The other option is to have a non-faulting funcid.decode which has the
property that int32.encode of funcid.decode of an int32 x equals x for
all x, valid or not. Thus funcids would effectively be opaque int32s so
the question is, again, what's the benefit over just having a
call_indirect that took an int32. I can imagine a tricky impl strategy
that represented funcids as function addresses by generating thunks on
demand for invalid integer values (assuming this is a rare case), but I'm
not sure if I'd want to do this over simply representing funcid as an
int32.

Perhaps this is the same "need more impl experience" situation you were
thinking about @titzer https://github.com/titzer?

Yeah, I think we can move quickly to get some experience if we take one of
a number of shortcuts here. E.g. we could:

a.) punt: have the producer generate their own dispatcher routines, just to
make progress. we'll probably try this first.
b.) per-signature function tables: something similar to asm.js, but since
it would be part of wasm, could be compiled into a single
boundscheck+indexedload+indirect branch (i.e. about 4-6 instructions).
c.) dynamic check: add a single funcptr type and try to make signature
checks cheap.

Those are in rough order of personal palatability. The check in c.) could
be made efficient if we require exact signature matching. That check could
then just be to load a signature id from the word immediately before the
machine code of the funcptr, compare against the callsite's expected id,
branching far away to trapping land if it fails (i.e. about 3-4
instructions).

The advantage of fully typed funcptrs I see here is that dispatch is 1-2
instructions.

Obviously, any of the strategies above could be sped up with an IC
mechanism that caches the last target and does a direct call with code
patching.


Reply to this email directly or view it on GitHub
#278 (comment).

@titzer
Copy link

titzer commented Aug 14, 2015

On Fri, Aug 14, 2015 at 12:41 PM, Ben L. Titzer titzer@google.com wrote:

On Fri, Aug 14, 2015 at 12:03 PM, Luke Wagner notifications@github.com
wrote:

On further thought, I'm realizing that there is a lot more nuance to
func-ptr types:

The big question is: what happens if you funcid.decode some bogus
integer (out of range or, if we have typed funcids, type mismatch)? An
obvious option would be to fault. However, this would inhibit the C
compiler from aggressively representing C function pointers as funcids
since sometimes (questionably legal) C code puts random bits into a func
ptr, never calls that func ptr, and expects to get those bits back later.
With faulting semantics, a C compiler would only be able to generate a
funcid.decode when it dominated a call_indirect in which case there's
little benefit from having funcids at all over simply having
call_indirect take an int32 directly (and relying on the wasm backend to
do any hoisting it can).

The other option is to have a non-faulting funcid.decode which has the
property that int32.encode of funcid.decode of an int32 x equals x for
all x, valid or not. Thus funcids would effectively be opaque int32s so
the question is, again, what's the benefit over just having a
call_indirect that took an int32. I can imagine a tricky impl strategy
that represented funcids as function addresses by generating thunks on
demand for invalid integer values (assuming this is a rare case), but I'm
not sure if I'd want to do this over simply representing funcid as an
int32.

Perhaps this is the same "need more impl experience" situation you were
thinking about @titzer https://github.com/titzer?

Yeah, I think we can move quickly to get some experience if we take one of
a number of shortcuts here. E.g. we could:

a.) punt: have the producer generate their own dispatcher routines, just
to make progress. we'll probably try this first.
b.) per-signature function tables: something similar to asm.js, but since
it would be part of wasm, could be compiled into a single
boundscheck+indexedload+indirect branch (i.e. about 4-6 instructions).
c.) dynamic check: add a single funcptr type and try to make signature
checks cheap.

Those are in rough order of personal palatability. The check in c.) could
be made efficient if we require exact signature matching. That check could
then just be to load a signature id from the word immediately before the
machine code of the funcptr, compare against the callsite's expected id,
branching far away to trapping land if it fails (i.e. about 3-4
instructions).

The advantage of fully typed funcptrs I see here is that dispatch is 1-2
instructions.

Obviously, any of the strategies above could be sped up with an IC
mechanism that caches the last target and does a direct call with code
patching.

Also, long-term I think we will want to have typed function pointers that
fit in with a GC'd object model so that users can build vtables and such.
The question right now is if we need to solve this for MVP.

Reply to this email directly or view it on GitHub
#278 (comment).

@qwertie
Copy link

qwertie commented Aug 14, 2015

this would inhibit the C compiler from aggressively representing C function pointers as funcids

True, but couldn't C++ vtables still benefit, as well as dispatch mechanisms in many other languages? (the overall workflow of how wasm code will use funcptrs isn't clear to me.)

The C compiler might be able to help too. At least as a global optimizaton, it could use knowledge about whether "garbage" was casted to a function pointer type or not, and generate slower code if yes.

@sunfishcode
Copy link
Member Author

@lukewagner Imho, that's not questionably legal, that's seriously illegal. I don't want to penalize everyone just to optimize for that case. C compilers wishing to support that can always just be conservative.

If we agree that signature-parameterized types are something we'll be ok with long-term anyway, what are the downsides to just doing that in the MVP? We can figure out the vtable/etc. optimization parts after the MVP.

@lukewagner
Copy link
Member

@sunfishcode It doesn't matter if it's illegal if many codebases do it and expects it to work. (E.g., SpiderMonkey does this all over the place via JS_DATA_TO_FUNC_PTR.) Also, I was trying to read more about this and it looks like it is valid in C99 and conditionally legal in C++11.

@titzer
I think it's possible to specify (c) but have an impl that does (b). That is, even if there is a single specified index space, the impl could generate per-signature tables (selected statically by callsite signature) that contain throw stubs for all but the valid callees. The downside is potentially a lot more memory; I had been thinking this would be an optional optimization that fell back to signature checks if the memory overhead would be prohibitive.

Interestingly, even if (b) is spec'd, real code depends on functions having unique addresses across different signatures. This was enough of a problem in practice that Emscripten has a workaround flag that manually pads the tables in the same manner described above. The difference is that this padding (and memory usage) gets baked into the semantics, instead of being an internal optimization.

Agreed on eventually wanting func-ptr types as GC object field types for implementing vtables; maybe that's reason enough to add func-ptr types now?

@sunfishcode
Copy link
Member Author

@lukewagner JS_DATA_TO_FUNC_PTR etc. for function pointers in object*, not the other way around. You're right though that while C/C++ present several obstacles to storing arbitrary bits in a function pointer, reinterpret_cast semantics ultimately make it possible. But in any case, this is just a minor variant of the broader problem of function pointers being storable in linear memory. If wasm has func_id types, producers from languages like C will need to do escape analysis to optimize language function pointers into wasm func_ids anyway, and that'll also handle the various type conversion cases.

@lukewagner
Copy link
Member

There is also JS_FUNC_TO_DATA_PTR which is often used in conjunction to go the other way.

The alias analysis necessary to use funcid instead of an int in the heap would be the same intra-procedural analysis necessary to put anything into a local instead of the heap stack. However, to use funcid, e.g., as the parameter type when the C++ code has a function-pointer parameter type, you need interprocedural analysis to see all the sources of values and/or uses. I think the ideal here is that translation to funcids could be trivial and predictable: any time you have an unaliased C++ function-pointer in a stack variable, you get a funcid.

@sunfishcode
Copy link
Member Author

(JS_FUNC_TO_DATA_PTR is for putting a function pointer in an Object*, and JS_DATA_TO_FUNC_PTR is for getting it out again. From a quick scan through SpiderMonkey, I don't think they're ever used in the reverse to put arbitrary bits into a function pointer type.)

Making placement of funcptr decode operations predictable from the C++ level where translation to funcid happens will be tricky; optimizing compilers aggressively discard variable lifetimes in the original code. Also, since translation from an index into a funcid will incur a cost, some cleverness may be desirable to place those operations to minimize the costs. I think we can do a reasonable job with this though.

Overall, what is the overall feeling here? Are people interested in having signature-parameterized funcid types in wasm, to allow type checks to be factored out in addition to range checks? Or are people leaning towards just having bare integer indices everywhere to start with?

@lukewagner
Copy link
Member

@sunfishcode Assuming the heap is much larger than the func-ptr table, casting an Object* to a func-ptr might as well be an arbitrary integer: both are invalid func-ptr indices and will spuriously fault if we have faulting funcid.decode semantics.

Overall, what is the overall feeling here? Are people interested in having signature-parameterized > funcid types in wasm, to allow type checks to be factored out in addition to range checks? Or are
people leaning towards just having bare integer indices everywhere to start with?

This question seems to boil down to: is there any perf win from funcid over bare integers. Once we have working prototypes, we should be able to do a quick-and-dirty experiment to test this. As @titzer said above, we do eventually want fully-typed funcids for use in GC types, but that use care may be sufficiently different from C++ function pointers (in particular, it might not even need int32.encode/funcid.decode) that we could get away with something much simpler. Thus, we shouldn't preemptively add funcid now just based on the expected need in the future, but only with some clear perf win.

The function pointer type and its operations (conversions, comparisons)
were previously only implied. This patch documents them like regular
types and operations.

It also lays the groundwork for a future with multiple-return-value
functions. This is done by eliminating the `void` type and defining
return types as a sequence. In the MVP, the sequence can have at most
one value.
sunfishcode added a commit that referenced this pull request Aug 20, 2015
This replaces the `void` type with the concept of a return type sequence
which may be empty, and it defines *signature*.

This brings in the parts of #278 not connected to function pointers.
@sunfishcode
Copy link
Member Author

Closing this PR; the parts unrelated to function pointers are now merged in #307. The original issue #89 is still open.

@sunfishcode sunfishcode deleted the funcptr branch October 31, 2015 17:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants