Conversation
niloc132
left a comment
There was a problem hiding this comment.
I like the patch - it seems to go a little beyond what is discussed in the description though. Can you describe a bit more how the JS is being generated that causes this - is it from Java that isn't handled correctly, or is this JSNI (or jsinterop-base's JSNI) emitting these extra !!s? Might make sense to have an issue to generally track the topic too, in case there are more examples of it.
|
|
||
| private enum EvalMode { | ||
| BOOL, | ||
| VOID |
There was a problem hiding this comment.
would you document the meaning of these, and what it means for evalContext to not have a key at all?
My understanding from (heh) context is that VOID stands for "value will be thrown away", and BOOL means "this will be coerced to boolean", while absent means we don't know, so retain everything
| } | ||
|
|
||
| @Override | ||
| public boolean isBooleanFalse() { |
There was a problem hiding this comment.
I don't think this is the right place for this - it should be part of the JsStaticEval check. the comments below indicate that the converse is "already simplified", but that's only true post-JsStaticEval.
The same code that does the shortCircuitOr()/etc check should also be looking for this case - you added logic to shortCircuitAnd, does it not work for OR too?
If we need this here, we probably also need to test for , operations... those are messier since they imply side effects on the LHS (JsStaticEval.trySimplifyComma should remove those though).
DeadCodeElimination/DCE and Simplifier do another transformation that doesnt appear to happen in JsStaticEval - expressions like (a,b)?c:d are rewritten to (a, b?c:d) so that b can be statically eval'd by the Conditional. If you're running into a situation where JsStaticEval can't work out the value of a sub-expr, it could be from something like this?
DCE at least has some checks in tests to assert that it only runs once and always converges - JsStaticEval is written in a similar style, but doesn't have such a check.
There was a problem hiding this comment.
I don't think this is the right place for this
I wasn't sure, the JsPrefixOperation has this, so having it in binary operations seemed consistent. But I didn't want an implementation that checks the whole tree and you're right that what I cam up was only correct under the assumption that some simplifications are already done, which is too fragile. So I now went with your approach of replacing && and || with , and adding a check for , in conditionals.
The same code that does the shortCircuitOr()/etc check should also be looking for this case
I fixed the code to be symmetric now.
|
Using #10263 to bring this up to date, I show that this brings samples down from 10821121 to 10800491, some 20kb, or about 0.2% - not bad for some light logic improvements! |
|
Taking a little more time to go over this, unless this is exclusively a JSNI problem I think you really want this to be moved/copied to DeadCodeElimination. Doing an optimization on constants like this earlier is extremely likely to benefit the Java optimization loop more than the JS loop (where JsStaticEval lives), as removing flow control is quite likely to also improve inlinability and enhance type tightening or pruning - something that the JS passes are unable to reason about as effectively. I'm nearly certain the two samples at the end of the description are Java rather than JSNI:
Harder to be sure about anything here, but I'd bet that Tqe and Pqe are Java instance methods on Keeping the change also in the JS loop isn't bad per se, as it will still apply to JSNI or any other work that the Java loop can't optimize out. I could easily imagine that the (apparently uninlinable) pruned instance methods could have other impacts on code size that the JS loop can't necessarily recognize. |
|
So there are two issues with the boolean expressions
|
|
Might there be another reason to use !!, like to convert to boolean or something? |
|
I think the patch mostly looks good, just trying to understand the root issue, be sure we can't do less and get more from it. Nothing here seems like it could increase build times, and for how much we're saving already, it seems obviously worth it. I think the Concrete example: gwt/user/super/com/google/gwt/emul/java/util/TreeMap.java Lines 828 to 834 in b7117c1 This is inlined into removeWithState: https://github.com/gwtproject/gwt/blob/b7117c157bbb3bcfcc022433e307722a8e12fb33/user/super/com/google/gwt/emul/java/util/TreeMap.java#L882C12-L882C17 Resulting in roughly if (!(!!node && node.isRed) && !$isRed(node.child[dir])) {That gwt/dev/core/src/com/google/gwt/dev/js/JsStaticEval.java Lines 399 to 418 in b7117c1 So I have no doubts about the patch being right, I'm sorry if I've implied that. Adding the Java side to go after I have another review pass about half done, just trying to more deeply understand what's going on here. No draft comments so far on the actual contents of the patch, just the tests and one comment I think could be cleaner. |
Exactly - but in cases where we know it will already be coerced to boolean, the extra For example will either print the string, or if "falsey", the value (this is dumb, since it should just be On the other hand Here we don't need the |
niloc132
left a comment
There was a problem hiding this comment.
Sorry, taking longer than I had anticipated with the release going on.
Oddly, I'm seeing zero impact from this patch in the Java AST of the Showcase sample - I'm still trying to understand why. The original objective of cleaning up !!s is going to have to happen only in the JS AST, but the nested &&+?: examples still appear to be from Java, unless this is again just a case of JSNI method inlining. I'll keep poking, trying to find examples where we could move the work earlier.
gwt/user/src/com/google/gwt/user/cellview/client/CellTreeNodeView.java
Lines 150 to 169 in b7117c1
This code is improved in JS by this patch, in ways that don't seem obvious right away - the COMMA operators are getting cleaned up due to lack of side effects on the right side.
isOpen = $containsKey(openNodes.map, key);
outerClasses = new StringBuilder(itemStyle);
- isOpen && (outerClasses.string += openStyle , outerClasses);
- isRootNode && (outerClasses.string += topStyle , outerClasses);
+ isOpen && (outerClasses.string += openStyle);
+ isRootNode && (outerClasses.string += topStyle);
isSelected = !!selectionModel && selectionModel.isSelected(value);
ariaSelected = '' + isSelected;
- isSelected && (outerClasses.string += selectedStyle , outerClasses);
+ isSelected && (outerClasses.string += selectedStyle);
innerClasses = new StringBuilder(itemStyle);
innerClasses.string += itemImageValueStyle;
- isRootNode && (innerClasses.string += topImageValueStyle , innerClasses);
+ isRootNode && (innerClasses.string += topImageValueStyle);
isOpen?(image = openImage):model.isLeaf(value)?(image = ($clinit_CellTreeNodeView() , LEAF_IMAGE)):(image = closedImage);
cellBuilder = new SafeHtmlBuilder;
Not all cases of the "return this" are impacted by this, but these improvement sure seem like obvious wins, and occur in many other places in the showcase.
Another example here, where we get a small win, but due to apparent side effects in the clinit, we can't outright remove the whole line:
gwt/user/src/com/google/gwt/user/client/ui/RootPanel.java
Lines 191 to 201 in b7117c1
if (!!rp) {
return rp;
}
- $size(rootPanels) == 0 && ($clinit_LocaleInfo() , false);
+ $size(rootPanels) == 0 && $clinit_LocaleInfo();
rp = new RootPanel$DefaultRootPanel;
$put(rootPanels, null, rp);
Here's a case where we probably should be removing the !! on the LHS, but instead leave it in place, possibly because this is a VOID context rather than BOOLEAN? Oddly, other cases like this are cleaned up. Maybe the code is too simple here, and being optimized in the Java ast so that we still see this pattern?
gwt/user/src/com/google/gwt/cell/client/ButtonCell.java
Lines 68 to 74 in b7117c1
_.render = function render(context, data, sb){
$append(sb.sb, '<button type="button" tabindex="-1">');
- !!data && ($append(sb.sb, data.html) , sb);
+ !!data && $append(sb.sb, data.html);
$append(sb.sb, '<\/button>');
}
This next one is probably as optimized as this patch can get it, and shows how the old code was still getting some benefits of removing the dead code, just not as complete as what you worked out
gwt/user/src/com/google/gwt/user/datepicker/client/DatePicker.java
Lines 713 to 723 in b7117c1
function $refreshAll(this$static){
$refresh(this$static.view);
$refresh(this$static.monthAndYearSelector);
- $isAttached(this$static) && this$static.view.lastDisplayed;
+ $isAttached(this$static);
$setAriaSelectedCell(this$static.view, this$static.value);
}
It is possible that this patch will open up other avenues for #10242 to benefit the codebase like this one.
This makes me mildly suspicious - is it correct to drop the || 0 here? This is a "number" context, right, since we're |ing the desired event bits against the existing ones?
gwt/user/src/com/google/gwt/user/client/ui/Widget.java
Lines 239 to 245 in b7117c1
_.sinkEvents = function sinkEvents(eventBitsToAdd){
- this.eventsToSink == -1?sinkEvents(($clinit_DOM() , this.element), eventBitsToAdd | ($clinit_DOM() , ($clinit_DOM() , this.element).__eventBits || 0)):(this.eventsToSink |= eventBitsToAdd);
+ this.eventsToSink == -1?sinkEvents(($clinit_DOM() , this.element), eventBitsToAdd | ($clinit_DOM() , ($clinit_DOM() , this.element).__eventBits)):(this.eventsToSink |= eventBitsToAdd);
}
My read is that if you simplify it, this looks like
sinkEvents(this.element), eventBitsToAdd | (this.element.__eventBits || 0));
and so we want the ||0 as "coerce to number".
| // if side effect, allow rewriting a() && false && b() -> (a(), false) && b() | ||
| // -> (a(), false && b()) -> (a(), false) |
There was a problem hiding this comment.
I'm wondering if this comment is useful here, since left-associativity means that we would view the whole
a() && false && b()
as
(a() && false) && b()
and so we would be relying on the JMultiExpr instanceof above for step two, and the lhs is the JBooleanLiteral on step three? I think a comment in the test makes sense to spell out that we're going to get all three different optimizations in a single pass, but probably just simplify here to something like
| // if side effect, allow rewriting a() && false && b() -> (a(), false) && b() | |
| // -> (a(), false && b()) -> (a(), false) | |
| // if side effect, allow rewriting a() && false -> (a(), false) |
Also technically (a(),false) is a character longer than a()&&false - perhaps we shouldn't touch it unless we know the current binaryoperation is itself within a binaryoperation?
There was a problem hiding this comment.
It may be tricky to know in advance if the outer bracket is needed, if it's not, the , expression is shorter.
alert(foo()&&false)
alert(foo(),false)
There was a problem hiding this comment.
Agreed - my "technically" responses are mostly to mean "this is possible, but probably not easy, and doesnt save us many bytes".
There was a problem hiding this comment.
I can reproduce the issue with ||0 in a test, checking...
| + "static int f1;" | ||
| + "static A createA() { A.f1 = 1; return new A(); } " | ||
| + "static boolean booleanWithSideEffects() { createA(); return true;}" | ||
| + "static boolean randomBooleanWithSideEffects() { createA(); return random() > .5;}" |
There was a problem hiding this comment.
We don't usually need to add something like random() to trick the compiler in these tests, since very little of the compiler is run here, just DCE and MethodInliner (requested by this method, line 295) - specializer is still left disabled. It won't even notice that random() fails to return a value...
Given the tests you're adding, you might just prefer to make a new test method and leave the inliner off, so the compiler just lets you call a method and not try to inline it away? Then you could write a() && false && b() and ensure you only get (EntryPoint$A.a(),false) out, rather than "expect" that the random() and stuff gets inlined in?
Alternatively, JSNI can never be inlined in the Java ast, so you could also just write randomBooleanWithSideEffects() itself as jsni, and it will be assumed to have side effects, not be inlined away.
To be clear, what you have now isn't bad, but it does look like you may have been fighting the test setup a bit to get the test you wanted.
| optimizeExpressions(false, "int", "A.randomBooleanWithSideEffects() && false && A.randomBooleanWithSideEffects() ? 1 : 2") | ||
| .intoString("return (EntryPoint$A.f1 = 1, new EntryPoint$A(), (EntryPoint$A.random() > 0.5, 2));"); |
There was a problem hiding this comment.
Note that it isn't obvious if the first or second randomBooleanWithSideEffects() was inlined here - the code could be wrong and inlining the wrong one, but the test output is the same.
| assertEquals("alert(a||true);", optimize("alert(a || true)")); | ||
| assertEquals("alert(a||false);", optimize("alert(a || false)")); | ||
| assertEquals("alert(!!a||!!b);", optimize("alert(!!a || !!b)")); |
There was a problem hiding this comment.
This last one technically can be simplified further right? !!(a||b)
Might also be nice to get a test like the first two, but with !!:
assertEquals("alert(!!a);", optimize("alert(!!a || false)"));
assertEquals("alert(true);", optimize("alert(!!a || true)"));
| } | ||
|
|
||
| public void testShortCircuitOrWithSideEffects() throws Exception { | ||
| assertEquals("a||b();", optimize("!!a || !!b()")); |
There was a problem hiding this comment.
Question, is this passing because it is in a void context, the value is being thrown away? Is that the goal of the test, or should this be wrapped in something (like alert()) - or should the a also be optimized out?
| evalContext.put(x.getArg2(), evalContext.get(x)); | ||
| } else if (x.getOperator() == JsBinaryOperator.COMMA) { | ||
| evalContext.put(x.getArg1(), EvalMode.VOID); | ||
| evalContext.put(x.getArg2(), evalContext.get(x)); |
There was a problem hiding this comment.
This doesn't look right - evalContext.containsKey(x) may or may not be false, so we could be putting null for the RHS's context, which means that if we read evalContext.containsKey(RHS) later, we might get "true" when we want "false".
|
I worked backwards from one of the First, the method that everything is inlined into - ignore everything except the super call at the end gwt/user/src/com/google/gwt/user/client/ui/DialogBox.java Lines 517 to 529 in b7117c1 That super call is PopupPanel.onPreviewNativeEvent: gwt/user/src/com/google/gwt/user/client/ui/PopupPanel.java Lines 1046 to 1052 in b7117c1 which in turn calls onEventPreview (as noted, this is deprecated, so just has a default impl): gwt/user/src/com/google/gwt/user/client/ui/PopupPanel.java Lines 704 to 707 in b7117c1 The !onEventPreview naturally will be inlined as just !true and at the end of the Java optimization loop, these are fully inlined, though we keep the event.nativeEvent accessor for some reason, but !true is now false
First iteration optimization loop with JS (before this patch's changes): The JS pass got the gwt/dev/core/src/com/google/gwt/dev/jjs/ast/JCastOperation.java Lines 53 to 63 in b7117c1 At the same time, Event.as() is marked as "this is always safe" gwt/user/src/com/google/gwt/user/client/Event.java Lines 464 to 471 in b7117c1 It turns out that the compiler will make the cast be free... because both Event and NativeEvent are JSOs. Event is a subclass of NativeEvent, so TypeTightener doesnt believe it is automatically safe, but really, since it is a JSO, it shouldn't check, it should just make it a free operation. As for why it is a free cast once we're in JS: gwt/dev/core/src/com/google/gwt/dev/jjs/impl/ImplementCastsAndTypeChecks.java Lines 100 to 104 in b7117c1 Then, when we translate the Java AST to JS, we just ignore that leftover cast: gwt/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java Lines 605 to 608 in b7117c1 So for this single case, my conclusion is that it is a bug for GWT to ignore the case during JS AST generation, when it could just as easily have tightened the cast away earlier and made all of this unnecessary. So at least for this specific example, better casting logic would have prevented this |
In the output I noticed statements like
which can be simplified to
In general, if we know that we ignore the result of an evaluation or we know the result is coerced to a boolean, we can apply more simplifications.
Other examples from compiler output this should simplify: