Simplify boolean expressions in JavaScript AST#10261
Simplify boolean expressions in JavaScript AST#10261zbynek wants to merge 3 commits intogwtproject:mainfrom
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.
| optimize("alert((a && b) || ( c && d));")); | ||
| assertEquals("a(),b&&c();", optimize("a(), b && c()")); | ||
| assertEquals("a()&&b,c();", optimize("a() && b, c()")); | ||
| assertEquals("a()&&b(),c();", optimize("a() && b(), c()")); |
There was a problem hiding this comment.
If the expr is in a statement like this, the output should be a(),b(),c(). Doesn't necessarily have to be in this patch, but I think it could be fixed by passing the context as void for the whole exprstatement?
There was a problem hiding this comment.
I don't think that's generally valid -- if a() evaluates to false we don't want to evaluate b().
There was a problem hiding this comment.
No you're right of course, my reply is nonsense.
|
|
||
| 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
|
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...
There was a problem hiding this comment.
This was fixed by not putting nulls into the map.
dev/core/test/com/google/gwt/dev/jjs/impl/DeadCodeEliminationTest.java
Outdated
Show resolved
Hide resolved
| 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.
| } | ||
|
|
||
| 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?
There was a problem hiding this comment.
Yes, I wanted to test that in void context !! is removed even with side effects.
Here we can't optimize a out because it decides if b() is or is not called.
We can add some tests with alert and side effects.
|
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 |
|
From GGB codebase which comes from inlining the two methods The |
bda5a4a to
cdec294
Compare
|
Picking the inner-most ternary's condition to preserve my sanity, from the expression, I'm guessing that we really do have side effects, and this is the "(a(),false)" vs "a()&&false" difference, still preserving side effects - the former is easier to potentially optimize out, but the latter is shorter. As it happens, Simplifier.java explicitly calls this case out: That above is embedded in a conditional if I'm reading it right, applying the multiexpr transformation we would have (what your patch now does): Now we're in Simplifier.simplifyConditional(), and this case should cover it: so that we end up with When simplifyConditional does that, it recursively calls itself, so would collapse that Does that work with the current version of the patch? At a glance, these should apply in the correct order to take place in a single pass of DCE? As an aside, its too bad that the optimization doesn't work on assignment - it would be an easy win for your code (and lots of other code) to add a case like |
|
I wrote a quick test that seems to confirm that the last step at least works as expected: |
cdec294 to
52f4726
Compare
|
#9731 use case potentially is also improved by this - effectively we have The |
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:
Fixes #10280