Open lukewagner opened 4 years ago
It's not just simd instructions though -- we also allow the v128
type everywhere, as block types, global values, parameter and result types, etc. What does the compiler do to allow for engines that don't support SIMD? Does it have to avoid using these features?
The nuance to the approach I was describing is that an engine can "support" SIMD by compiling all SIMD instructions to traps, which makes it easy for all hosts to support SIMD, regardless of their platform. (An engine still has to validate v128
and SIMD instructions (according to the SIMD validation rules), but this is also easy.) Thus, there shouldn't be any long-term need to run wasm on hosts that don't support SIMD, because they should all support it.
I mean, if the engine "supports" simd by compiling all simd instructions into traps, it still has to deal with the v128
type wherever else it shows up too. I suppose that's not too hard, though? Would be good to get input from an engine that's not planning to implement simd.
Thanks for raising this suggestion, @lukewagner! I think it has a lot going for it, and it avoids almost all of the complexity of the conditional sections proposal. That being said, there are some more abstract advantages to having a conditional sections proposal, and as a community we should consider these advantages and decide whether they are worth the complexity of conditional sections.
Generality. Conditional sections offers a general mechanism by which any proposal can be made optional with no additional work or complication for specific proposals. This is effectively a separation of concerns that makes optionality orthogonal to the design of every other proposal.
Compatibility. The core feature of conditional sections is that they allow decoding and validation to skip sections entirely. That means that new features can be used in disabled conditional sections even on old engines and engines that have not started implementing the new features. Having optionality at the level of execution rather than decoding means that new features cannot be included, even when disabled, until the engine does some work to support them. Conditional sections improve the portability and usability of all new features.
Configurability. Conditional sections provide a mechanism by which modules can be configured at instantiation time. This mechanism could be used to tune the isolation of a module depending on what context it's used in, for example by importing its memory and exporting raw functions when running as a C++-style shared object, but declaring its memory and exporting a more abstract, untrusting interface when instantiated in an untrusted context. Another use of configurability would be to conditionally enable module functionality based on the underlying capabilities of the system, for example corresponding to different WASI profiles. That way misconfigurations become instantiation-time errors rather than runtime errors.
@binji Good point; to further ease the impl burden, I was thinking that perhaps a trapping SIMD implementation would also trap at any point where a v128
value could pass into or out of the host (so func_invoke or a call to a func_alloc'd hostfunc
). Thus, noone could observe the value of a v128
and it could be represented however.
@tlively Thanks for the consideration! Yes, I agree we should study more of the use cases we imagine for conditional-sections and see how they would concretely play out (especially considering how a single wasm module is run both inside and outside of a Web VM) before making any decisions.
It's nice that any function that takes a v128 (or other "unsupported" type) would trivially be known to be unreachable, except by calls directly from the host, which could trap.
I totally agree that the current design brings a lot of complexity for little (current) gain. It may make us ready for future similar features, but if those don't come, we would have been better of with a SIMD specific feature like what @lukewagner suggests.
I'd go even further. There may be 4 classes of engines:
I have a hard time picturing a world where 1 & 2 don't cover pretty much all use cases. For those super-constrained embedded devices (or blockchains?) that require 3, I would imagine 3B to make more sense, since modules compiled like this are unlikely to be shared as-is with browsers or servers or whatever, so this conditionality is not that useful. Requiring super specialized use cases to use a special module build doesn't seem that onerous to me.
I'm not too fond of starting to invent ad-hoc feature detection instructions. And implementing the whole SIMD surface (which will probably keep growing fast), even if stubbed out as traps, strikes me as much more implementation work than conditional sections (which is solely a frontend matter).
We discussed compartmentalising the spec a number of times. SIMD (along with threads and GC) always was on the list of features that we expect to be an obvious candidate for making optional, since it's a large feature set with potentially no benefit to some Wasm ecosystems. In that light, do we care about the ability to construct code that's portable and optimal across different ecosystems?
I'm not too fond of starting to invent ad-hoc feature detection instructions.
Agreed! But it would be even worse to start inventing ad-hoc non-standard feature flags that non-JS wasm engines end up having to implicitly know about and implement.
And implementing the whole SIMD surface (which will probably keep growing fast), even if stubbed out as traps, strikes me as much more implementation work than conditional sections (which is solely a frontend matter).
I'd like to hear if there's any concrete evidence that validating SIMD and compiling to traps takes any significant time or generates any significant engine code size. It seems pretty easy to me, definitely less than supporting conditional compilation. (Hence the question if there's any more real non-SIMD use cases.)
In that light, do we care about the ability to construct code that's portable and optimal across different ecosystems?
For threads and GC, I agree that it's hard to imagine compiling wasm modules that work across hosts that do and don't support the features (hence I don't they count as use cases for conditional-sections). But for the specific case of SIMD it does seem entirely practical to generate portable wasm modules.
All things considered, if SIMD is the only motivating use case here for generating portable wasm modules based on conditionally-present instructions, I think the wasm ecosystem will be net more-portable and less-complex if we make this conditionality a SIMD-local detail rather than a cross-cutting wasm feature that opens the door to many more ad hoc feature flags.
One consideration that hasn't been mentioned yet is determinism. Technically, optional trapping makes the semantics of all SIMD instructions non-deterministic. Is that desirable, given the stated goals of Wasm?
@lukewagner:
I'd like to hear if there's any concrete evidence
SIMD about doubles the size of the instruction set. Even if most of a trapping implementation is mechanical, that seems wasted effort for something unusable. Plus, an implementor would have to invest indefinitely into tracking all future extensions to the feature, which may be a rather fast-moving target.
if SIMD is the only motivating use case
I could imagine threads for local parallelism to be another example.
And of course, there is the open set of use cases of conditional configuration. One example I could imagine is adapting to resource constraints of the host, or other system parameters that are not available in-language or require static configuration (such as memory/table limits).
As a data point about the burden of supporting SIMD validation: I'm implementing SIMD in SpiderMonkey. The patches labeled Part 2 and Part 3 here represent roughly the burden of adding V128 and validation for the current instruction set. The code generators would additionally need to decode the bytecodes correctly and then generate traps, though there are few instruction patterns so that's not a large burden. It's less clear to me precisely how we would have to insert traps elsewhere in the the system eg at host/wasm boundaries, or in other situations where v128 might be used.
@rossberg
One consideration that hasn't been mentioned yet is determinism. Technically, optional trapping makes the semantics of all SIMD instructions non-deterministic. Is that desirable, given the stated goals of Wasm?
I think the way we would specify the SIMD test instruction would be to bubble it up to be a host-supplied parameter of the module, making it just as deterministic as conditional-sections and imports.
SIMD about doubles the size of the instruction set. Even if most of a trapping implementation is mechanical, that seems wasted effort for something unusable. Plus, an implementor would have to invest indefinitely into tracking all future extensions to the feature, which may be a rather fast-moving target.
Any wasm implementation that wants to stay up-to-date already has to track a stream of changes; that's why hosts would want to reuse a common implementation. I think the question to ask is analogous to Amdahl's law: is the work required to track (validate and compile to traps) SIMD changes a significant % of the work to track the wasm spec overall?
I think the way we would specify the SIMD test instruction would be to bubble it up to be a host-supplied parameter of the module, making it just as deterministic as conditional-sections and imports.
Not sure I understand, are you talking about spec machinery? You can certainly parameterise the specification in some way, but observably that still results in non-determinism a.k.a. implementation-dependent behaviour.
I think the question to ask is analogous to Amdahl's law: is the work required to track (validate and compile to traps) SIMD changes a significant % of the work to track the wasm spec overall?
Fair enough. If you e.g. consider a minimal implementation that also decides to eschew threads and GC then I would estimate the answer to be yes. I expect that the other parts of the core spec will only see a moderate amount of changes after the current round of proposals has landed.
You can certainly parameterise the specification in some way, but observably that still results in non-determinism a.k.a. implementation-dependent behaviour.
Sure, but in the same sense, all calls to imports are non-deterministic. But on a particular host (or for the entire Web platform, or a whole blockchain platform), if the import values are deterministic, then the wasm is deterministic.
I expect that the other parts of the core spec will only see a moderate amount of changes after the current round of proposals has landed.
Even setting aside the big-ticket items of threads/GC, that would suggest a break in the "evergreen" model of wasm that we've been assuming as a basis for aggressively kicking things out of the MVP. I think we have at least another 5-10 years of (slow, careful) evolution to attempt to claim we've completed even the (non-GC, non-thread) core of wasm. That means regularly updating engines that want to take advantage of the emerging common, portable wasm ecosystem, where I still believe the % of work specific to SIMD would be low.
@sunfishcode, could you chime in about how you see conditional sections being used to support multiple WASI profiles and for isolation configuration in the WASI ecosystem? It would also be helpful if you could assess how likely it would be that WASI tools actually implement these use cases if conditional sections were available.
You can certainly parameterise the specification in some way, but observably that still results in non-determinism a.k.a. implementation-dependent behaviour.
Sure, but in the same sense, all calls to imports are non-deterministic. But on a particular host (or for the entire Web platform, or a whole blockchain platform), if the import values are deterministic, then the wasm is deterministic.
Er, that argument could be used to define away all implementation-dependent behaviour as deterministic. ;)
Imports are not in the same class. They are explicit, under control of and picked by the user. An internal semantics "parameter" isn't.
I think we have at least another 5-10 years of (slow, careful) evolution to attempt to claim we've completed even the (non-GC, non-thread) core of wasm. That means regularly updating engines that want to take advantage of the emerging common, portable wasm ecosystem, where I still believe the % of work specific to SIMD would be low.
I agree that there will be constant change, but I expect it to slow down outside big feature extensions. I suspect that there will be more changes in SIMD alone than in the entire kernel language at some point.
Er, that argument could be used to define away all implementation-dependent behaviour as deterministic. ;)
Yep! For example, NaN payload nondeterminism could also be turned into a parameter, allowing determinism-desiring hosts to clearly specify what they want to happen. Especially considering the whole topic of "host built-in" imports, I don't think this is a black-and-white situation.
I suspect that there will be more changes in SIMD alone than in the entire kernel language at some point.
Even if that were to be the case, it would still seem unfortunate to add conditional-sections only to address that one problem (hence my root question in this issue).
Yep! For example, NaN payload nondeterminism could also be turned into a parameter
Sure, but hosts are not the issue. It doesn't make it any less bad from the point of view of clients that can't predict what host they will be running on. Do you really want Wasm to have implementation-dependent behaviour, in some 100+ instructions?
it would still seem unfortunate to add conditional-sections only to address that one problem (hence my root question in this issue).
Agreed, but the proposal already changed into a more generic mechanism. I mentioned a couple of other potential use cases above, like static configuration.
Sure, but hosts are not the issue. It doesn't make it any less bad from the point of view of clients that can't predict what host they will be running on.
Yes, but does conditional-sections make it any better when I can't predict which host I'm running on? At least, with a boolean-valued host parameter that is testable and has a precisely-defined connection to which instructions will trap, I can definitively know and handle both cases in pure wasm.
Agreed, but the proposal already changed into a more generic mechanism. I mentioned a couple of other potential use cases above, like static configuration.
Yes, that's precisely what I'm questioning in this issue: are these actually concrete, motivating use cases that can't be solved by a less-severe mechanism? For example, any API-related use case (like the local thread-based optimizations you mentioned) could be handled by WASI optional imports.
Yes, but does conditional-sections make it any better [...]?
Yes, in so far that it doesn't infect the execution semantics of all programs. You still have static invariants. For example, reasoning about quasi-non-deterministic dynamic behaviour is significantly harder, with control-flow-dependency and all, even if it all reduces to a single state parameter.
could be handled by WASI optional imports.
The notion of optional import does not scale beyond functions, so is rather limited (and pretty gross, if you ask me :) ). It might already fall short of handling the threads use case, because the memory must be defined differently, which, if it's im/exported, affects the interface as well.
I think it would be good to refocus this issue on the various use cases and other ecosystem benefits of the conditional sections proposal. I think there is an interesting discussion to be had about how easy or difficult it is for tools to reason about condition sections and alternative solutions, but let's move that discussion to a separate issue.
Ping @sunfishcode on https://github.com/WebAssembly/conditional-sections/issues/20#issuecomment-603997732. Do you have WASI use cases you could share?
It turns out that WASI's design principles, which are separately motivated, happen to discourage conditional sections in many cases. For example, discouraging libraries from using string arguments which require hard-coded methods for turning strings into handles (eg. through a filesystem open or URL fetch), and encouraging them to just take handle arguments instead, allow libraries to not care as much where the handles come from.
We'll also prefer per-handle checks rather than per-instance checks when we can. "Is this directory case sensitive" is better than "Are directories on this platform case sensitive" or even "Am I on Windows?". With virtualization being applied in so many ways across the computing landscape these days, broad "what kind of platform am I on" assumptions are often unreliable.
Beyond that, we have optional imports, which, yes, one can debate the merits of, but seem to be a closer fit for WASI's purposes. Note that part of their ugliness is fixable: if we were to put optional imports in the core language, we could easily generalize them to handle tables, memories, and globals, and even make the syntax prettier. Optional imports don't pose new questions for eg. tooling. And they don't have any question of whether WASI needs to standardize a set of conditions that non-Web wasm code can be conditional on.
Thanks for that input, @sunfishcode! If WASI is not interested in the proposed conditional sections for broad module configuration and we have no other interested ecosystems, it may make sense to explore a less generally powerful mechanism.
I agree that the SIMD use cases that this proposal addresses are compelling and should be addressed. However, I haven't seen any really compelling use cases outside of SIMD. All the other features I can think of (GC, threads, exception handling, ...) seem like cases where conditional sections wouldn't be enough -- you'd want to compile wholly different modules.
If our only real motivation is SIMD (and other instructions that have the same "instructions that optimize what you can already do" character), it seems like there is a simpler design that avoids much of the complication and compatibility hazards of conditional-sections.
In particular, with SIMD (and similar kinds of instructions), validation is very easy to implement; the hard thing is compiling SIMD when you don't have good hardware support. Thus, we could say that:
bool
(i32
)-producing constant-valued testing instruction was added returning whether SIMD instructions were, in this instance, compiled to trapsThus the burden is on the compiler to guard SIMD instructions with suitable branches and providing a fallback to non-SIMD. Since the tests are constant-valued, an optimizing compiler should have no problem eliminating the dead branch and so there should be no appreciable difference in performance vs. the more
#ifdef
-y conditional-sections proposal. In fact, by not requiring the branch to happen at function boundaries, this approach could enable SIMD to be used more liberally "in line" without incurring function call costs (or depending on wasm engines performing function inlining, which they generally avoid doing).What's nice about this approach is:
One thing I realized during the discussion at the last CG f2f that I didn't realize in #9, is that, even with the move toward explicit parameters, in the absence of a JS host, a pure-wasm implementation has to define the set of parameter values itself making the choice of parameters actually part of the "standard" that any wasm host must implement to portably run content. If there is a standard, it seems better to be explicit and intentional about it.