Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

kdashg
Copy link
Contributor

@kdashg kdashg commented Mar 25, 2020

This leaves unless unused, so it was also removed here.

@kdashg kdashg added the wgsl WebGPU Shading Language Issues label Mar 25, 2020
@kdashg
Copy link
Contributor Author

kdashg commented Mar 25, 2020

These seem easy to identify, and if so we should consider having One True Way to do things.

@dneto0
Copy link
Contributor

dneto0 commented Mar 27, 2020

I strongly object.

The if with brace-enclosed-statements is the introduction of a new level of divergence in the control flow.
The "break if condition" construct is a resolution of a prior divergence.

These are different concepts that the developer will have to deal with, in order to know when where uniformity holds.

@kvark
Copy link
Contributor

kvark commented Mar 27, 2020

@dneto0 wait, you are saying these two constructs are not semantically equivalent? 😨

@dneto0
Copy link
Contributor

dneto0 commented Mar 27, 2020

Here's a fun puzzle, in C:

   while (cond) {
      if (cond2) {
         stmtA();
         break;
      }
   }

Question: Is stmtA in the loop or not?

It sure looks like it is. But look at the control flow graph and it's not; it's a common view among compiler writers (not just me) to recognize that stmtA is not in the loop. For a visual illustration, see @johnkslang's avatar.

@dneto0
Copy link
Contributor

dneto0 commented Mar 27, 2020

@dneto0 wait, you are saying these two constructs are not semantically equivalent? 😨

For a single threaded execution, there is no difference.

If you force coders to write if (cond) { ..... ; break; } then you get two bad effects:

  • adding dead code after the break, but before the end brace. That should be an error.
  • they'll get confused about the uniformity / non-uniformity of the code in the braces before the break. We should encourage the use of "break if" and "break unless" to make it crystal clear.

@kvark
Copy link
Contributor

kvark commented Mar 27, 2020

The way I (amateurishly) see it is that one could represent this with either a CFG where the stmt is in a block within the main body of the loop, or a CFG where the stmt is in a block outside of the body loop, but in the end it doesn't matter since the two CFGs are isomorphic. So semantically these constructs should be equivalent.

@dneto0
Copy link
Contributor

dneto0 commented Mar 27, 2020

It doesn't matter until you try to do a collective operation like a derivative or a subgroup operation (broadcast, reduction, ballot, etc.) Then you need to be a lot more careful. C doesn't need to tell you. SPIR-V (for Shader) tells you with the placement of the structured construct's merge block and the uniformity reconvergence rule.

@kvark
Copy link
Contributor

kvark commented Mar 27, 2020

I understand the point you brought about uniform control flow of the stmtA example. I don't quite see why it would apply to our problem at hand though. We aren't trying to figure out what to do with this construct, we are trying to figure out what to do with if (xxx) { break } one.

@litherum
Copy link
Contributor

adding dead code after the break, but before the end brace. That should be an error.

I don’t think break if solves this problem, because it will still be possible to write if (...) {break;}, so there’s going to have to be an error either way.

The "break if condition" construct is a resolution of a prior divergence.

I agree with @kvark that this makes sense, but because the stmtA example wouldn’t actually use break if, we need another illustrative example to justify it.

@damyanp
Copy link

damyanp commented Mar 30, 2020

@dneto0 - would the two constructs generate different SPIR-V? Would the SPIR-V -> WGSL converter generate break if?

@kdashg
Copy link
Contributor Author

kdashg commented Mar 31, 2020

So it sounds like the objection is that while if (foo) break; is the same as break if (foo);, this only holds for a break-only block. So while if (foo) break; ~= break if (foo);, if (foo) { bar(); break; } would have to be const x = foo; if (x) { bar(); } break if (x);, which has surprising (foot-gun-y?) uniformity implications.
Am I getting that right? (I was not)

@dneto0
Copy link
Contributor

dneto0 commented Mar 31, 2020

@dneto0 - would the two constructs generate different SPIR-V? Would the SPIR-V -> WGSL converter generate break if?

A simple SPIR-V --> WGSL converter and simple WGSL --> SPIR-V converter would both generate different code.
A simple WGSL--> SPIR-V converter for if (x) { break; } will generate whole new selection construct with a merge instruction (at the condition) to point to the (new, distinct) merge block. So you get a cluster of at least 2 new blocks.
Meanwhile, break if (x) translates to a single OpBranchConditional where the true target is the merge block for the construct you're already in, and the false branch is a fall-through.

@grorg
Copy link
Contributor

grorg commented Mar 31, 2020

Discussed at the 2020-03-31 meeting.

Resolution from that meeting was that we'd spend a week thinking about this and re-visit it.

@kainino0x
Copy link
Contributor

To serialize the thought I had from the meeting. If I understood correctly, if (cond) { ... } should always generate concern about whether the ... code inside the block is still uniform, while break if cond; is not an if block and can avoid generating that concern.

I don't think the mental overhead is significant to determine that there's no uniformity concern for if { break; }.

However I'd also be fine with keeping this, or with reordering it to if (cond) break; (where normal ifs always require braces but this one does not).

@kvark
Copy link
Contributor

kvark commented Mar 31, 2020

I'm not entirely sure where we get the motivation to keep break if.

Is the semantics equivalent to if (xx) break? Yes.
Is this new syntax opening any new doors to developers? No.

Finally, from the practical standpoint, is it trivial for the shader compiler to turn one into another? Pretty much yes.

So why are we still considering break if?

WebGPU still has to perform uniform control flow analysis and error in case anything is assuming uniformity (e.g. derivatives) when there is none, so it's not like if (xx) will introduce any hidden issues - the errors of this class would be triggered as early as at createShaderModule stage, as I indicated in #646 (comment).

@kainino0x
Copy link
Contributor

kainino0x commented Mar 31, 2020

While having separate break if and if () { break; } constructs allows us to express more SPIR-V programs, if I understand correctly (not sure), there's no semantic difference at all between those programs, in which case I don't think I want to be able to express the difference. (edit: I'm agreeing)

@kdashg
Copy link
Contributor Author

kdashg commented Mar 31, 2020

For something like (Example 1)...

loop {
   if (cond2()) {
      stmtA();
      break;
   }
   work()
}

...we naively get something like (Example 2)...

loop top:top_of_loop merge:end_of_loop

top_of_loop:
if (cond2()) branch cond2_true
work()
branch top_of_loop

cond2_true:
stmtA()
branch end_of_loop

end_of_loop:

Because cond2_true doesn't branch back into top_of_loop, arguably stmtA() is outside the loop.


The author could choose to use the semantic equivalent to make this more obvious: (Example 3)

loop {
   break if (cond2());
   work()
}
stmtA();

FWIW I think the expressiveness isn't much worse as: (Example 4)

loop {
   if (cond2()) { break; }
   work()
}
stmtA();

These show what one would actually want the original code to do, generating: (Example 5)

loop top:top_of_loop merge:end_of_loop

top_of_loop:
if (cond2()) branch end_of_loop
branch top_of_loop

end_of_loop:
stmtA()

Unsurprisingly now stmtA is now clearly outside the loop.

If uniform control flow enters the loop header, the loop's merge block will also be uniform.
Since it's in-or-after the merge block, stmtA is in UCF (unless there's a kill in the loop?) regardless of the UCF status of the rest of the loop.
Contrast with the original code, where it's not readily apparant that stmtA should be back in UCF, since it's lexically within the loop.


However, for something like... (Example 6)

loop {
   break if (cond());
   if (cond2()) {
      stmtA();
      break;
   }
   work()
}

...the author couldn't simply sink stmtA outside of the loop, as there are two exit paths from the loop, so we get almost exactly Example 2 instead of Example 5: (Example 7)

 loop top:top_of_loop merge:end_of_loop
 
 top_of_loop:
+if (cond()) branch end_of_loop
 if (cond2()) branch cond2_true
 work()
 branch top_of_loop
 
 cond2_true:
 stmtA()
 branch end_of_loop
 
 end_of_loop:

Given that as my understanding, I still think that requiring if () { break; } instead of break if (); is a reasonable simplification, without major loss of expressiveness.

I can see how a clever compiler would, for Example 1, emit Example 5 instead of Example 2.
I don't think break-if makes or breaks (har har) that usecase.

FWIW, I think we should consider requiring Example 3 or 4 in order to get UCF guaranteed for stmtA as in Example 5.

@litherum
Copy link
Contributor

litherum commented Apr 7, 2020

I found @dneto0's argument during the last call made sense to me. It seems valuable to allow people to clearly express their intention, even if there are other less-clear ways to express themselves. I'm now on the fence about this proposal: neither for it nor against it.

The reason he didn't persuade me entirely is that it's not clear to me that authors are clamoring a way to express the idea presented above in this thread. I'm not sure how valuable it is to be able to express this particular concept, especially without also being able to somehow attach stmtA() to the keyword.

@othermaciej
Copy link

Do we have a limited budget for syntactic sugar? If yes, this doesn't seem like a good way to spend it. if (foo) { break; } and break if (foo); (setting aside any additional statements) ought to generate identical code and don't particularly seem to express different intent. It would arguably be a bug if WGSL with these two different constructs compiled to different code.

If our budget for syntactic sugar is not particularly limited, then the question naturally arises wether any statement BAR ought to have an BAR if (FOO) form. To programmers writing in this language, it would seem non-orthogonal that the conditional can only be appended to break and continue statements in particular. Perl and Ruby have "suffix if" for all statements, for instance. Appealing to the shape of the control flow graph is probably not going to make this limitation much more understandable. It's unlikely most programmers would be thinking about the shape of the control flow graph as a reason to choose the shortcut syntax.

@dneto0
Copy link
Contributor

dneto0 commented Apr 7, 2020

Thanks everyone for the thoughtful considerations, given my dramatic pushback. :-)

"Do we have a limited budget for syntactic sugar? "
Whether you view it as sugar depends on where you're starting from.

Is there ever a case where you want to do a break or continue as completely unconditional? (Setting aside for the moment that there may be other code that is also guarded by the same condition.) If a break is completely unconditional then it seems to me you would/should always rewrite the code in a simpler form in the first place.

One suggestion I've heard is that you can avoid the stmtA confusion by rule: that break must appear alone in the braces: { break; } is the only way to utter "break", as if it's a 4-token sequence that is required any time "break" is present.
That feels a rather long way to go to keep with C tradition.

Why not have conditional-anything? E.g. Perl has $a = $b if $cond.
But I think that's different. break and continue are intrinsically about modifying control flow, which is why it goes with control-flow-graph considerations. Similarly, if itself is intrinsically about control flow.

I've been thinking about how I would describe "if" and "break if" to newcomers, keeping in mind uniformity consdierations

Eg.

Normally statements are executed in the order presented in the source text: top line to bottom line, and from left to right within a line.
Several constructs change the default execution order.
An if statement splits control flow depending on the value of a boolean condition ... If the condition is dynamically non-uniform, and the "if" is executed in uniform control flow, then the "if" statement introduces a divergence which may be resolved after its associated end-brace. [ Ok there's a bunch of awkwardness there.]

A "break" statement is used to transfer control to just after the end-brace of the nearest enclosing switch or loop. If the break statement has an "if" clause then control is transfered when the condition is true. If the break statement has an "unless" clause then control is transfered when the condition is false. A "break" without any conditional clause always transfers control.
In uniform control flow terms, the transfer of control of a taken break may cause a reconvergence with other threads. [see blah blah blah].

@dneto0
Copy link
Contributor

dneto0 commented Apr 7, 2020

One more thought: This decision is not entirely independent. Having "break unless" makes for very idiomatic use of loop.

Where in C you'd have

while (keep_going()) {
}

You can have

loop {
    ...
    const keep_going : bool = ...evaluate condition
    break unless (keep_going);
     ...
}

@kvark
Copy link
Contributor

kvark commented Apr 7, 2020

@litherum @dneto0
What are the risks associated with us misinterpreting the user intention (e.g. by seeing if(xx) {break;}) here?

@dneto0
Copy link
Contributor

dneto0 commented Apr 14, 2020

There is no ambiguity about if(xx) {break;} itself.

The confusion/trouble/trap begins because the syntax is highly suggestive that you can put other statements in with the break.

@grorg
Copy link
Contributor

grorg commented Apr 14, 2020

Discussed at the 2020-04-14 meeting.

@dneto0
Copy link
Contributor

dneto0 commented Apr 15, 2020

During the meeting @litherum asked (more or less) how I would prefer to express the "stmtA" scenario.

Let's say the C code fragment is:

  if (i+j >= limit) {
         cleanup_action();
         break;
  }

Then my preference is for the developer to choose consciously where to put the cleanup code. First option:

   const escape : bool = i+j >= limit;
   if (escape) {
        cleanup_action();
   }
   break if (escape);   // I'd really like to eliminate those parentheses, but Dan won't let me

Second option:

loop {...
   break if (i+j>=limit); 
}
cleanup_action();  // consciously outside the loop

I like the style of the second option better. It clearly shows that the cleanup action is not part of the loop and should not be considered as taking up internal register space for the purposes of scheduling.

@dneto0
Copy link
Contributor

dneto0 commented Apr 15, 2020

The @jdashg proposal in #705 is a useful middle ground:

  • ban unconditional break and continue
  • allow syntax like if (xx) break; and if (xx) continue;

Yes, this can always be circumvented, e.g. if (xx) { stmtA(); if(true) break; } but then the programmer is really going out of their way to avoid the strong nudge.

@litherum
Copy link
Contributor

litherum commented Apr 15, 2020

  if (i+j >= limit) {
         cleanup_action();
         break if (true);
  }
  if (i+j >= limit) {
         cleanup_action();
         if (true) break;
  }

@kdashg
Copy link
Contributor Author

kdashg commented Apr 15, 2020

The @jdashg proposal in #705 is a useful middle ground:

* ban unconditional break and continue

* allow syntax like `if (xx) break;` and `if (xx) continue;`

Yes, this can always be circumvented, e.g. if (xx) { stmtA(); if(true) break; } but then the programmer is really going out of their way to avoid the strong nudge.

Oh, oops, I didn't mean to ban unconditional break and continue, but you're right that I did! I can leave that one as is if you'd like, but my intent for that PR was purely additive.

@kvark
Copy link
Contributor

kvark commented Apr 21, 2020

Just wanted to re-iterate in writing something I was saying on the calls.

The confusion/trouble/trap begins because the syntax is highly suggestive that you can put other statements in with the break.

There is no danger here that is immediately obvious to me. If they put something illegal, their shaders will stop building. This is true with and without "break if".

@grorg
Copy link
Contributor

grorg commented May 12, 2020

This was discussed at the 2020-05-12 meeting.

@grorg
Copy link
Contributor

grorg commented May 18, 2020

To be discussed at 2020-05-19 meeting.

  • @jdashg was going to revise the proposal
  • explicit request for feedback from @dj2

OpReturn
OpReturnValue
| if_stmt
| unless_stmt
Copy link
Member

Choose a reason for hiding this comment

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

This seems like a separate change?

const a : i32 = 2;
var i : i32 = 0; <1>
loop {
break if (i >= 4);
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 lean towards the #705 solution here and we drop the {}s in the limited cases.

@grorg
Copy link
Contributor

grorg commented May 19, 2020

Discussed at the 2020-05-19 meeting.

@litherum
Copy link
Contributor

litherum commented May 19, 2020

It's already been stated that, if you write the "bad" code at #643 (comment) and stmtA() does derivatives, you'll get a compiler error due to uniformity rules. So, neither break if nor if break; (no braces) help authors in this situation.

It's worth considering the reverse: where stmtA() is benign with respect to uniformity rules.

Consider:

for (...; condition(); ...) {
    if (...) {
        stmtA();
        break;
    }
    ...
}

Now that we have multiple loop types, and I expect for loops to be idiomatic (hopefully this is a non-controversial statement...) then the above seems like a more common way to write the "bad" code described in #643 (comment).

In this formulation, let's pretend that stmtA() is benign with respect to uniformity rules. It's tricky to rewrite this to move the stmtA() after the loop, because stmtA() should only execute if the break statement caused the control flow to exit the loop, but not if the condition() caused it. Therefore, if we encourage authors to move stmtA() after the loop, they will need a dummy variable to determine which loop exit point was used:

var exitPoint : i32 = 0;
for (...; condition(); ...) {
    if (...) {
        exitPoint = 1;
        break;
    }
    ...
}
if (exitPoint == 1) {
    stmtA();
}

This new code is worse than the original, for multiple reasons:

  • There's this extra unnecessary variable
  • It doesn't actually get rid of the code between the if and the break
  • It moves related code farther apart from each other

It's also worth noting that this argument holds even if there were no for loops; all it needs is multiple breaks inside a loop.

Therefore, neither break if or if break; (no braces) help authors if they break uniformity rules, and they actually hinder authors if they abide by uniformity rules.

@kdashg
Copy link
Contributor Author

kdashg commented May 20, 2020

Adding shorthand doesn't hinder authors.
However, there are authors in this group who have already expressed interest in these shorthands, who do see value in them. Don't mistake personal style guidelines for truth.

@othermaciej
Copy link

Adding shorthand doesn't hinder authors, but removing longhand (as some have suggested in this very PR thread) does. The shorthand also seems redundant, and of lesser value than other syntactic sugar that has been rejected by the group (such as while loops) as well as being non-orthogonal.

I'm all for generously adding shorthand and syntactic sugar, but this should be applied consistently. I'm against removing the ability to do something between the if and the break, because, as @litherum pointed out, this makes it hard to write some otherwise valid code.

@dj2
Copy link
Member

dj2 commented May 27, 2020

We've talked about this internally and if we update this CL to put the unless_stmt back in place we'd be fine with this change to remove the conditionals on break and continue. (There is already issue #577 to discuss removing unless)

We would then be depending on the uniformity analysis to make sure everything is correct which we think is fine.

I'd suggest opening a new issue to make {}s optional in general to untangle it from this change.

@grorg
Copy link
Contributor

grorg commented Jun 2, 2020

Discussed at the 2020-06-02 meeting.

@kdashg kdashg force-pushed the if-break-not-break-if branch from 772a1bc to 1c1f1f8 Compare June 2, 2020 22:12
@kdashg kdashg force-pushed the if-break-not-break-if branch from 1c1f1f8 to 46ae574 Compare June 2, 2020 22:19
@dj2 dj2 merged commit 5ddf3a6 into gpuweb:master Jun 3, 2020
ben-clayton pushed a commit to ben-clayton/gpuweb that referenced this pull request Sep 6, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
wgsl WebGPU Shading Language Issues
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants