com.google.gwt.dev.jjs.impl.MethodInliner Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2007 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.gwt.dev.jjs.impl;
import com.google.gwt.dev.jjs.InternalCompilerException;
import com.google.gwt.dev.jjs.ast.Context;
import com.google.gwt.dev.jjs.ast.JBinaryOperation;
import com.google.gwt.dev.jjs.ast.JBinaryOperator;
import com.google.gwt.dev.jjs.ast.JCastOperation;
import com.google.gwt.dev.jjs.ast.JDeclarationStatement;
import com.google.gwt.dev.jjs.ast.JDeclaredType;
import com.google.gwt.dev.jjs.ast.JExpression;
import com.google.gwt.dev.jjs.ast.JExpressionStatement;
import com.google.gwt.dev.jjs.ast.JLocal;
import com.google.gwt.dev.jjs.ast.JLocalRef;
import com.google.gwt.dev.jjs.ast.JMethod;
import com.google.gwt.dev.jjs.ast.JMethodBody;
import com.google.gwt.dev.jjs.ast.JMethodCall;
import com.google.gwt.dev.jjs.ast.JModVisitor;
import com.google.gwt.dev.jjs.ast.JNewInstance;
import com.google.gwt.dev.jjs.ast.JParameter;
import com.google.gwt.dev.jjs.ast.JParameterRef;
import com.google.gwt.dev.jjs.ast.JProgram;
import com.google.gwt.dev.jjs.ast.JReferenceType;
import com.google.gwt.dev.jjs.ast.JReturnStatement;
import com.google.gwt.dev.jjs.ast.JStatement;
import com.google.gwt.dev.jjs.ast.JThisRef;
import com.google.gwt.dev.jjs.ast.JType;
import com.google.gwt.dev.jjs.ast.JVisitor;
import com.google.gwt.dev.jjs.ast.js.JMultiExpression;
import com.google.gwt.dev.util.collect.Stack;
import com.google.gwt.dev.util.log.speedtracer.CompilerEventType;
import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger;
import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger.Event;
import com.google.gwt.thirdparty.guava.common.collect.Lists;
import com.google.gwt.thirdparty.guava.common.collect.Maps;
import com.google.gwt.thirdparty.guava.common.collect.Sets;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Inline methods that can be inlined. The current implementation limits the
* methods that can be inlined to those that are composed of at most two
* top-level expressions.
*
* Future improvements will allow more complex methods to be inlined based on
* the number of call sites, as well as adding support for more complex target
* method expressions.
*/
public class MethodInliner {
/**
* Clones an expression, ensuring no local or this refs.
*/
private static class CloneCalleeExpressionVisitor extends CloneExpressionVisitor {
@Override
public boolean visit(JThisRef x, Context ctx) {
throw new InternalCompilerException("Should not encounter a JThisRef "
+ "within a static method");
}
}
/**
* Method inlining visitor.
*/
private class InliningVisitor extends JChangeTrackingVisitor {
public InliningVisitor(OptimizerContext optimizerCtx) {
super(optimizerCtx);
}
/**
* Resets with each new visitor, which is good since things that couldn't be
* inlined before might become inlinable.
*/
private final Set cannotInline = Sets.newHashSet();
private final Stack expressionsWhoseValuesAreIgnored = Stack.create();
@Override
public void endVisit(JExpressionStatement x, Context ctx) {
expressionsWhoseValuesAreIgnored.pop();
}
@Override
public void endVisit(JMethodCall x, Context ctx) {
JMethod method = x.getTarget();
if (getCurrentMethod() == method) {
// Never try to inline a recursive call!
return;
}
if (cannotInline.contains(method)) {
return;
}
if (tryInlineMethodCall(x, ctx) == InlineResult.BLACKLIST) {
// Do not try to inline this method again
cannotInline.add(method);
}
}
@Override
public void endVisit(JMultiExpression x, Context ctx) {
for (int i = 0; i < x.getExpressions().size() - 1; i++) {
expressionsWhoseValuesAreIgnored.pop();
}
}
private InlineResult tryInlineMethodCall(JMethodCall x, Context ctx) {
JMethod method = x.getTarget();
if (!method.isStatic() || method.isJsniMethod() || method.canBeImplementedExternally()) {
// Only inline static methods that are not native.
return InlineResult.BLACKLIST;
}
if (!method.isInliningAllowed()) {
return InlineResult.BLACKLIST;
}
JMethodBody body = (JMethodBody) method.getBody();
List stmts = body.getStatements();
if (method.getEnclosingType() != null
&& method.getEnclosingType().getClinitMethod() == method && !stmts.isEmpty()) {
// clinit() calls cannot be inlined unless they are empty
return InlineResult.BLACKLIST;
}
// try to inline
List expressions = extractExpressionsFromBody(body);
if (expressions == null) {
// If it will never be possible to inline the method, add it to a
// blacklist
return InlineResult.BLACKLIST;
}
return tryInlineBody(x, ctx, expressions, expressionsWhoseValuesAreIgnored.contains(x));
}
@Override
public void endVisit(JNewInstance x, Context ctx) {
// Do not inline new operations.
}
@Override
public boolean visit(JExpressionStatement x, Context ctx) {
expressionsWhoseValuesAreIgnored.push(x.getExpr());
return true;
}
@Override
public boolean enter(JMethod x, Context ctx) {
if (program.getStaticImpl(x) != null) {
/*
* Never inline a static impl into the calling instance method. We used
* to allow this, and it required all kinds of special logic in the
* optimizers to keep the AST sane. This was because it was possible to
* tighten an instance call to its static impl after the static impl had
* already been inlined, this meant any "flow" type optimizer would have
* to fake artificial flow from the instance method to the static impl.
*
* TODO: allow the inlining if we are the last remaining call site, and
* prune the static impl? But it might tend to generate more code.
*/
return false;
}
return true;
}
@Override
public boolean visit(JMultiExpression x, Context ctx) {
for (int i = 0; i < x.getExpressions().size() - 1; i++) {
expressionsWhoseValuesAreIgnored.push(x.getExpression(i));
}
return true;
}
private JMethodCall createClinitCall(JMethodCall x) {
JDeclaredType targetType = x.getTarget().getEnclosingType().getClinitTarget();
if (!getCurrentMethod().getEnclosingType().checkClinitTo(targetType)) {
// Access from this class to the target class won't trigger a clinit
return null;
}
if (program.isStaticImpl(x.getTarget()) &&
!x.getTarget().getEnclosingType().isJsoType()) {
// No clinit needed; target is really a non-jso instance method.
return null;
}
if (JProgram.isClinit(x.getTarget())) {
// This is a clinit call, doesn't need another clinit
return null;
}
JMethod clinit = targetType.getClinitMethod();
// If the clinit is a non-native, empty body we can optimize it out here
if (!clinit.isJsniMethod() && (((JMethodBody) clinit.getBody())).getStatements().size() == 0) {
return null;
}
return new JMethodCall(x.getSourceInfo(), null, clinit);
}
/**
* Creates a JMultiExpression from a set of JExpressionStatements,
* optionally terminated by a JReturnStatement. If the method doesn't match
* this pattern, it returns null
.
*
* If a method has a non-void return statement and can be represented as a
* multi-expression, the output of the multi-expression will be the return
* expression of the method. If the method is void, the output of the
* multi-expression should be considered undefined.
*/
private List extractExpressionsFromBody(JMethodBody body) {
List expressions = Lists.newArrayList();
CloneCalleeExpressionVisitor cloner = new CloneCalleeExpressionVisitor();
for (JStatement stmt : body.getStatements()) {
if (stmt instanceof JDeclarationStatement) {
JDeclarationStatement declStatement = (JDeclarationStatement) stmt;
if (!(declStatement.getVariableRef() instanceof JLocalRef)) {
return null;
}
JExpression initializer = declStatement.getInitializer();
if (initializer == null) {
continue;
}
JLocal local = (JLocal) declStatement.getVariableRef().getTarget();
JExpression clone = new JBinaryOperation(stmt.getSourceInfo(), local.getType(),
JBinaryOperator.ASG,
local.makeRef(declStatement.getVariableRef().getSourceInfo()),
cloner.cloneExpression(initializer));
expressions.add(clone);
} else if (stmt instanceof JExpressionStatement) {
JExpressionStatement exprStmt = (JExpressionStatement) stmt;
JExpression expr = exprStmt.getExpr();
JExpression clone = cloner.cloneExpression(expr);
expressions.add(clone);
} else if (stmt instanceof JReturnStatement) {
JReturnStatement returnStatement = (JReturnStatement) stmt;
JExpression expr = returnStatement.getExpr();
if (expr != null) {
JExpression clone = cloner.cloneExpression(expr);
clone = maybeCast(clone, body.getMethod().getType());
expressions.add(clone);
}
// We hit an unconditional return; no need to evaluate anything else.
break;
} else {
// Any other kind of statement won't be inlinable.
return null;
}
}
return expressions;
}
/**
* Creates a lists of expression for evaluating a method call instance,
* possible clinit, and all arguments. This is a precursor for inlining the
* remainder of a method that does not reference any parameters.
*/
private List expressionsIncludingArgs(JMethodCall x) {
List expressions = Lists.newArrayListWithCapacity(x.getArgs().size() + 2);
expressions.add(x.getInstance());
expressions.add(createClinitCall(x));
for (int i = 0, c = x.getArgs().size(); i < c; ++i) {
JExpression arg = x.getArgs().get(i);
ExpressionAnalyzer analyzer = new ExpressionAnalyzer();
analyzer.accept(arg);
if (analyzer.hasAssignment() || analyzer.canThrowException()) {
expressions.add(arg);
}
}
return expressions;
}
/**
* Inline a call to an expression. Returns {@code InlineResult.BLACKLIST} if the method is
* deemed not inlineable regardless of call site; {@code InlineResult.DO_NOT_BLACKLIST}
* otherwise.
*/
private InlineResult tryInlineBody(JMethodCall x, Context ctx,
List bodyAsExpressionList, boolean ignoringReturn) {
if (isTooComplexToInline(bodyAsExpressionList, ignoringReturn)) {
return InlineResult.BLACKLIST;
}
// Do not inline anything that modifies one of its params.
ExpressionAnalyzer targetAnalyzer = new ExpressionAnalyzer();
targetAnalyzer.accept(bodyAsExpressionList);
if (targetAnalyzer.hasAssignmentToParameter()) {
return InlineResult.BLACKLIST;
}
// Make sure the expression we're about to inline doesn't include a call
// to the target method!
RecursionCheckVisitor recursionCheckVisitor = new RecursionCheckVisitor(x.getTarget());
recursionCheckVisitor.accept(bodyAsExpressionList);
if (recursionCheckVisitor.isRecursive()) {
return InlineResult.BLACKLIST;
}
/*
* After this point, it's possible that the method might be inlinable at
* some call sites, depending on its arguments. From here on return 'true'
* as the method might be inlinable elsewhere.
*/
/*
* There are a different number of parameters than args - this is likely a
* result of parameter pruning. Don't consider this call site a candidate.
*
* TODO: would this be possible in the trivial delegation case?
*/
if (x.getTarget().getParams().size() != x.getArgs().size()) {
// Could not inline this call but the method might be inlineable at a different call site.
return InlineResult.DO_NOT_BLACKLIST;
}
// Run the order check. This verifies that all the parameters are
// referenced once and only once, not within a conditionally-executing
// expression and before any tricky target expressions, such as:
// - assignments to any variable
// - expressions that throw exceptions
// - field references
/*
* Ensure correct evaluation order or params relative to each other and to
* other expressions.
*/
OrderVisitor orderVisitor = new OrderVisitor(x.getTarget().getParams());
orderVisitor.accept(bodyAsExpressionList);
switch (orderVisitor.checkResults()) {
case NO_REFERENCES:
/*
* A method that doesn't touch any parameters is trivially inlinable (this
* covers the empty method case)
*/
if (!x.hasSideEffects()) {
markCallsAsSideEffectFree(bodyAsExpressionList);
}
new LocalVariableExtruder(getCurrentMethod()).accept(bodyAsExpressionList);
List expressions = expressionsIncludingArgs(x);
expressions.addAll(bodyAsExpressionList);
ctx.replaceMe(JjsUtils.createOptimizedMultiExpression(ignoringReturn, expressions));
return InlineResult.DO_NOT_BLACKLIST;
case FAILS:
/*
* We can still inline in the case where all of the actual arguments are
* "safe". They must have no side effects, and also have values which
* could not be affected by the execution of any code within the callee.
*/
for (JExpression arg : x.getArgs()) {
ExpressionAnalyzer argAnalyzer = new ExpressionAnalyzer();
argAnalyzer.accept(arg);
if (argAnalyzer.hasAssignment() || argAnalyzer.accessesField()
|| argAnalyzer.createsObject() || argAnalyzer.canThrowException()) {
/*
* This argument evaluation could affect or be affected by the
* callee so we cannot inline here.
*/
// Could not inline this call but the method is potentially inlineable.
return InlineResult.DO_NOT_BLACKLIST;
}
}
// Fall through!
case CORRECT_ORDER:
default:
if (!x.hasSideEffects()) {
markCallsAsSideEffectFree(bodyAsExpressionList);
}
new LocalVariableExtruder(getCurrentMethod()).accept(bodyAsExpressionList);
// Replace all params in the target expression with the actual arguments.
ParameterReplacer replacer = new ParameterReplacer(x);
replacer.accept(bodyAsExpressionList);
bodyAsExpressionList.add(0, x.getInstance());
bodyAsExpressionList.add(1, createClinitCall(x));
ctx.replaceMe(JjsUtils.createOptimizedMultiExpression(ignoringReturn,
bodyAsExpressionList));
return InlineResult.DO_NOT_BLACKLIST;
}
}
}
private static void markCallsAsSideEffectFree(List expressions) {
// Propagate side effect information to the inlined body due to @HasNoSideEffects annotation
// in the method.
new JModVisitor() {
@Override
public void endVisit(JMethodCall x, Context ctx) {
x.markSideEffectFree();
}
}.accept(expressions);
}
private static boolean isTooComplexToInline(List bodyAsExpressionList,
boolean ignoringReturn) {
/*
* Limit inlined methods to multiexpressions of length 2 for now. This
* handles the simple { return JVariableRef; } or { expression; return
* something; } cases.
*
* TODO: add an expression complexity analyzer.
*/
if (bodyAsExpressionList.size() > 3) {
return true;
}
if (bodyAsExpressionList.size() == 3
&& (!ignoringReturn || bodyAsExpressionList.get(2).hasSideEffects())) {
return true;
}
// The expression is effectively of size 2, hence not too complex to inline.
return false;
}
/**
* Verifies that all the parameters are referenced once and only once, not
* within a conditionally-executing expression, and any before trouble some
* expressions evaluate. Examples of troublesome expressions include:
*
*
* - assignments to any variable
* - expressions that throw exceptions
* - field references
*
*/
private static class OrderVisitor extends ExpressionAnalyzer {
private int currentIndex = 0;
private final List parameters;
private boolean succeeded = true;
public OrderVisitor(List parameters) {
this.parameters = parameters;
}
public SideEffectCheck checkResults() {
if (succeeded && currentIndex == parameters.size()) {
return SideEffectCheck.CORRECT_ORDER;
}
if (succeeded && currentIndex == 0) {
return SideEffectCheck.NO_REFERENCES;
}
return SideEffectCheck.FAILS;
}
@Override
public void endVisit(JParameterRef x, Context ctx) {
JParameter param = x.getParameter();
// If the expression has side-effects before a parameter reference, fail
if (hasAssignment() || accessesField() || canThrowException()) {
succeeded = false;
}
// If this parameter reference won't always execute, fail
if (isInConditional()) {
succeeded = false;
}
// Ensure this parameter is evaluated in the correct order relative to
// other parameters.
if (parameters.indexOf(param) == currentIndex) {
currentIndex++;
} else {
succeeded = false;
}
super.endVisit(x, ctx);
}
}
/**
* Replace parameters inside an inlined expression with arguments to the
* inlined method.
*/
private class ParameterReplacer extends JModVisitor {
private final JMethodCall methodCall;
public ParameterReplacer(JMethodCall methodCall) {
this.methodCall = methodCall;
}
@Override
public void endVisit(JParameterRef x, Context ctx) {
int paramIndex = methodCall.getTarget().getParams().indexOf(x.getParameter());
assert paramIndex != -1;
// Replace with a cloned call argument.
CloneExpressionVisitor cloner = new CloneExpressionVisitor();
JExpression arg = methodCall.getArgs().get(paramIndex);
JExpression clone = cloner.cloneExpression(arg);
clone = maybeCast(clone, x.getType());
ctx.replaceMe(clone);
}
}
/**
* Extrudes local variables from the body into the currect method.
*/
private class LocalVariableExtruder extends JModVisitor {
private final Map newLocalsByOriginalLocal = Maps.newLinkedHashMap();
private final JMethodBody methodBody;
public LocalVariableExtruder(JMethod method) {
methodBody = (JMethodBody) method.getBody();
}
@Override
public void endVisit(JLocalRef x, Context ctx) {
JLocal originalLocal = x.getLocal();
JLocal newLocal = newLocalsByOriginalLocal.get(originalLocal);
if (newLocal == null) {
newLocal = JProgram.createLocal(originalLocal.getSourceInfo(), originalLocal.getName(), originalLocal.getType(), originalLocal.isFinal(), methodBody);
newLocalsByOriginalLocal.put(originalLocal, newLocal);
}
ctx.replaceMe(newLocal.makeRef(x.getSourceInfo()));
}
}
private static class RecursionCheckVisitor extends JVisitor {
private boolean isRecursive = false;
private final JMethod method;
public RecursionCheckVisitor(JMethod method) {
this.method = method;
}
@Override
public void endVisit(JMethodCall x, Context ctx) {
if (x.getTarget() == method) {
isRecursive = true;
}
}
public boolean isRecursive() {
return isRecursive;
}
}
/**
* Results of a side-effect and order check.
*/
private enum SideEffectCheck {
CORRECT_ORDER, FAILS, NO_REFERENCES
}
public static String NAME = MethodInliner.class.getSimpleName();
public static OptimizerStats exec(JProgram program, OptimizerContext optimizerCtx) {
Event optimizeEvent = SpeedTracerLogger.start(CompilerEventType.OPTIMIZE, "optimizer", NAME);
OptimizerStats stats = new MethodInliner(program).execImpl(optimizerCtx);
optimizeEvent.end("didChange", "" + stats.didChange());
return stats;
}
public static OptimizerStats exec(JProgram program) {
return exec(program, new FullOptimizerContext(program));
}
private final JProgram program;
private MethodInliner(JProgram program) {
this.program = program;
}
private OptimizerStats execImpl(OptimizerContext optimizerCtx) {
OptimizerStats stats = new OptimizerStats(NAME);
while (true) {
InliningVisitor inliner = new InliningVisitor(optimizerCtx);
Set modifiedMethods =
optimizerCtx.getModifiedMethodsSince(optimizerCtx.getLastStepFor(NAME));
Set affectedMethods = affectedMethods(modifiedMethods, optimizerCtx);
optimizerCtx.traverse(inliner, affectedMethods);
stats.recordModified(inliner.getNumMods());
optimizerCtx.setLastStepFor(NAME, optimizerCtx.getOptimizationStep());
optimizerCtx.incOptimizationStep();
if (!inliner.didChange()) {
break;
}
// Run a cleanup on the methods we just modified
OptimizerStats dceStats = DeadCodeElimination.exec(program, optimizerCtx);
stats.recordModified(dceStats.getNumMods());
}
JavaAstVerifier.assertProgramIsConsistent(program);
return stats;
}
/**
* Return the set of methods affected (because they are or callers of) by the modifications to the
* given set functions.
*/
private Set affectedMethods(Set modifiedMethods,
OptimizerContext optimizerCtx) {
assert (modifiedMethods != null);
Set affectedMethods = Sets.newLinkedHashSet();
affectedMethods.addAll(modifiedMethods);
affectedMethods.addAll(optimizerCtx.getCallers(modifiedMethods));
return affectedMethods;
}
/**
* Insert an implicit cast if the types differ; it might get optimized out
* later, but in some cases it will force correct math evaluation.
*/
private JExpression maybeCast(JExpression exp, JType targetType) {
if (targetType instanceof JReferenceType) {
assert exp.getType() instanceof JReferenceType;
targetType = merge((JReferenceType) exp.getType(), (JReferenceType) targetType);
}
if (!program.typeOracle.castSucceedsTrivially(exp.getType(), targetType)) {
exp = new JCastOperation(exp.getSourceInfo(), targetType, exp);
}
return exp;
}
private JReferenceType merge(JReferenceType source, JReferenceType target) {
JReferenceType result;
if (program.typeOracle.castSucceedsTrivially(
source.getUnderlyingType(), target.getUnderlyingType())) {
result = source;
} else {
result = target;
}
return result;
}
private enum InlineResult { BLACKLIST, DO_NOT_BLACKLIST}
}