Skip to content

Commit

Permalink
#376 Implement more efficient Class Hierarchy Analysis for method de-…
Browse files Browse the repository at this point in the history
…virtualization (#377)

* #376 Implement more efficient Class Hierarchy Analysis for method de-virtualization
  • Loading branch information
mirkosertic authored Apr 14, 2020
1 parent bd9d1ff commit a6ba5bf
Show file tree
Hide file tree
Showing 11 changed files with 236 additions and 50 deletions.
11 changes: 9 additions & 2 deletions core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,12 @@
<version>${kotlin.version}</version>
<scope>test</scope>
</dependency>
<!-- <dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>1.7.28</version>
<scope>test</scope>
</dependency> -->
</dependency>
<dependency>
<groupId>org.apache.xmlgraphics</groupId>
<artifactId>batik-svggen</artifactId>
Expand Down Expand Up @@ -146,6 +146,13 @@
</sourceDirs>
</configuration>
</execution>
<execution>
<id>compile</id>
<phase>process-sources</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,9 @@ public CompileResult compile(

final CompileResult theResult = backend.generateCodeFor(aOptions, theLinkerContext, aClass, aMethodName, aSignature);

// Write some statistics
theLinkerContext.getStatistics().writeTo(aOptions.getLogger());

// Include all required resources from included modules
final List<String> resourcesToInclude = new ArrayList<>();
for (final ClassLibProvider provider : ClassLibProvider.availableProviders()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1132,6 +1132,20 @@ public String methodHandleDelegateFor(final MethodHandleExpression e) {
theWriter.tab().text("};").newLine();
}

if (aEntry.getProvidingClass().getClassName().equals(BytecodeObjectTypeRef.fromRuntimeClass(Class.class))
&& theMethod.getName().stringValue().equals("getClassLoader")
&& theMethod.getSignature().matchesExactlyTo(BytecodeLinkedClass.GET_CLASSLOADER_SIGNATURE)) {

// Special method: we resolve a runtime class by name here
theWriter.tab().text("C.");

final String theJSMethodName = theMinifier.toMethodName(theMethod.getName().stringValue(), theCurrentMethodSignature);

theWriter.text(theJSMethodName).assign().text("function()").space().text("{").newLine();
theWriter.tab(2).text("return null;").newLine();
theWriter.tab().text("};").newLine();
}

return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ public class BytecodeLinkedClass extends Node<Node, EdgeType> {
private Boolean opaque;
private Boolean callback;
private Boolean event;
private Set<BytecodeVirtualMethodIdentifier> implementedIdentifiersCache;
private final BytecodeLinkedClass superClass;

public BytecodeLinkedClass(final BytecodeLinkedClass aSuperclass, final int aUniqueId, final BytecodeLinkerContext aLinkerContext, final BytecodeObjectTypeRef aClassName, final BytecodeClass aBytecodeClass) {
Expand Down Expand Up @@ -504,20 +503,6 @@ public void resolveInheritedOverriddenMethods() {
}
}

public boolean implementsMethod(final BytecodeVirtualMethodIdentifier aIdentifier) {
// Do we already have a link?
if (implementedIdentifiersCache == null) {
implementedIdentifiersCache = outgoingEdges(
BytecodeProvidesMethodEdgeType.filter())
.map(t -> (BytecodeMethod) t.targetNode())
.filter(t -> !t.getAccessFlags().isAbstract())
.map(t -> linkerContext.getMethodCollection().identifierFor(t))
.collect(Collectors.toSet());
}

return implementedIdentifiersCache.contains(aIdentifier);
}

@Override
public String toString() {
return className.name();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,19 @@ public class BytecodeLinkerContext {
private final BytecodeMethodCollection methodCollection;
private final Logger logger;
private int classIdCounter;
private final Statistics statistics;

public BytecodeLinkerContext(final BytecodeLoader aLoader, final Logger aLogger) {
rootNode = new RootNode();
loader = aLoader;
methodCollection = new BytecodeMethodCollection();
logger = aLogger;
classIdCounter = 0;
statistics = new Statistics();
}

public Statistics getStatistics() {
return statistics;
}

public Logger getLogger() {
Expand Down Expand Up @@ -114,6 +120,8 @@ public BytecodeLinkedClass resolveClass(final BytecodeObjectTypeRef aTypeRef) {

logger.info("Linked {}" ,theLinkedClass.getClassName().name());

statistics.context("Linker context").counter("Loaded classes").increment();

return theLinkedClass;
} catch (final Exception e) {
throw new RuntimeException("Error linking class " + aTypeRef.name(), e);
Expand Down Expand Up @@ -150,11 +158,4 @@ public void resolveAbstractMethodsInSubclasses() {
resolveAbstractMethodsInSubclasses();
}
}

public List<BytecodeLinkedClass> getClassesImplementingVirtualMethod(final BytecodeVirtualMethodIdentifier aIdentifier) {
return linkedClasses()
.map(Edge::targetNode)
.filter(t -> t.implementsMethod(aIdentifier))
.collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ public boolean isImplementedBy(final BytecodeMethod aMethod, final BytecodeLinke
}

public MethodEntry implementingClassOf(final String aMethodName, final BytecodeMethodSignature aSignature) {
for (final MethodEntry theEntry : entries) {
for (int i = entries.size() - 1; i >= 0; i--) {
final MethodEntry theEntry = entries.get(i);
final BytecodeMethod theMethod = theEntry.getValue();
if (theMethod.getName().stringValue().equals(aMethodName) && !theMethod.getAccessFlags().isAbstract() &&
theMethod.getSignature().matchesExactlyTo(aSignature)) {
Expand Down
63 changes: 63 additions & 0 deletions core/src/main/java/de/mirkosertic/bytecoder/core/Statistics.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright 2018 Mirko Sertic
*
* 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 de.mirkosertic.bytecoder.core;

import de.mirkosertic.bytecoder.api.Logger;

import java.util.HashMap;
import java.util.Map;

public class Statistics {

public static class Counter {
private int value;

public void increment() {
value++;
}
}

public static class Context {
private final Map<String, Counter> counter;

Context() {
counter = new HashMap<>();
}

public Counter counter(final String name) {
return counter.computeIfAbsent(name, t -> new Counter());
}

}

private final Map<String, Context> contexts;

Statistics() {
contexts = new HashMap<>();
}

public Context context(final String name) {
return contexts.computeIfAbsent(name, t -> new Context());
}

public void writeTo(final Logger logger) {
for (final Map.Entry<String, Context> ctx : contexts.entrySet()) {
for (final Map.Entry<String, Counter> ctn : ctx.getValue().counter.entrySet()) {
logger.info("[Statistics] {}::{} = {}", ctx.getKey(), ctn.getKey(), ctn.getValue().value);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
import de.mirkosertic.bytecoder.core.BytecodeMethod;
import de.mirkosertic.bytecoder.core.BytecodeMethodSignature;
import de.mirkosertic.bytecoder.core.BytecodeObjectTypeRef;
import de.mirkosertic.bytecoder.core.BytecodeVirtualMethodIdentifier;
import de.mirkosertic.bytecoder.core.Statistics;
import de.mirkosertic.bytecoder.ssa.ClassHierarchyAnalysis;
import de.mirkosertic.bytecoder.ssa.ControlFlowGraph;
import de.mirkosertic.bytecoder.ssa.DirectInvokeMethodExpression;
import de.mirkosertic.bytecoder.ssa.Expression;
Expand All @@ -30,7 +31,6 @@
import de.mirkosertic.bytecoder.ssa.Value;
import de.mirkosertic.bytecoder.ssa.VariableAssignmentExpression;

import java.util.List;
import java.util.Optional;

public class InvokeVirtualOptimizerStage implements OptimizerStage {
Expand Down Expand Up @@ -76,27 +76,37 @@ private Optional<DirectInvokeMethodExpression> visit(final InvokeVirtualMethodEx
return Optional.empty();
}

final Statistics.Context context = aLinkerContext.getStatistics().context("InvokeVirtualOptimizer");

context.counter("Total number of invocations").increment();

final String theMethodName = aExpression.getMethodName();
final BytecodeMethodSignature theSignature = aExpression.getSignature();

final BytecodeVirtualMethodIdentifier theIdentifier = aLinkerContext.getMethodCollection().toIdentifier(theMethodName, theSignature);
final List<BytecodeLinkedClass> theLinkedClasses = aLinkerContext.getClassesImplementingVirtualMethod(theIdentifier);
if (theLinkedClasses.size() == 1) {
// There is only one class implementing this method, so we can make a direct call
final BytecodeLinkedClass theLinked = theLinkedClasses.get(0);
if (!theLinked.emulatedByRuntime() && !theLinked.getClassName().equals(BytecodeObjectTypeRef.fromRuntimeClass(Class.class))) {
final ClassHierarchyAnalysis theAnalysis = new ClassHierarchyAnalysis(aLinkerContext);
final Optional<BytecodeLinkedClass> theClassToInvoke = theAnalysis.classProvidingInvokableMethod(theMethodName, theSignature, aExpression.getInvokedClass(),
aExpression.incomingDataFlows().get(0),
aClass -> !aClass.emulatedByRuntime() && !aClass.getClassName().name().equals(Class.class.getName()),
aMethod -> !aMethod.getAccessFlags().isAbstract() && !aMethod.getAccessFlags().isStatic());

if (theClassToInvoke.isPresent()) {

// There is only one class implementing this method and reachable in the hierarchy, so we can make a direct call
final BytecodeLinkedClass theLinked = theClassToInvoke.get();
final BytecodeObjectTypeRef theClazz = theLinked.getClassName();

// Due to method substitution in the JDK emulation layer we might get another method implementation
// as seen by the wait vs. waitInternal thing in TObject. This is strange, but has to be this way
// as wait is final and cannot be overwritten anyhow.
final BytecodeMethod theMethodToInvoke = theLinked.getBytecodeClass().methodByNameAndSignatureOrNull(theMethodName, theSignature);

final BytecodeMethod theMethod = theLinked.getBytecodeClass().methodByNameAndSignatureOrNull(theMethodName, theSignature);
if (!theMethod.getAccessFlags().isAbstract()) {
final BytecodeObjectTypeRef theClazz = theLinked.getClassName();
final DirectInvokeMethodExpression theNewExpression = new DirectInvokeMethodExpression(aExpression.getProgram(), aExpression.getAddress(), theClazz, theMethodToInvoke.getName().stringValue(),
theSignature);
aExpression.routeIncomingDataFlowsTo(theNewExpression);

final DirectInvokeMethodExpression theNewExpression = new DirectInvokeMethodExpression(aExpression.getProgram(), aExpression.getAddress(), theClazz, theMethodName,
theSignature);
aExpression.routeIncomingDataFlowsTo(theNewExpression);
context.counter("Optimized invocations").increment();

return Optional.of(theNewExpression);
}
}
return Optional.of(theNewExpression);
}
return Optional.empty();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Copyright 2020 Mirko Sertic
*
* 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 de.mirkosertic.bytecoder.ssa;

import de.mirkosertic.bytecoder.classlib.Array;
import de.mirkosertic.bytecoder.core.BytecodeLinkedClass;
import de.mirkosertic.bytecoder.core.BytecodeLinkerContext;
import de.mirkosertic.bytecoder.core.BytecodeMethod;
import de.mirkosertic.bytecoder.core.BytecodeMethodSignature;
import de.mirkosertic.bytecoder.core.BytecodeObjectTypeRef;
import de.mirkosertic.bytecoder.core.BytecodeTypeRef;
import de.mirkosertic.bytecoder.graph.Edge;

import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;

public class ClassHierarchyAnalysis {

private final BytecodeLinkerContext linkerContext;

public ClassHierarchyAnalysis(final BytecodeLinkerContext linkerContext) {
this.linkerContext = linkerContext;
}

public Optional<BytecodeLinkedClass> classProvidingInvokableMethod(final String aMethodName,
final BytecodeMethodSignature aSignature,
final BytecodeTypeRef aInvocationTarget,
final Value aReceiver,
final Predicate<BytecodeLinkedClass> aClassFilter,
final Predicate<BytecodeMethod> aMethodFilter) {
final BytecodeLinkedClass theInvocationTarget;
if (aInvocationTarget.isArray()) {
theInvocationTarget = linkerContext.resolveClass(BytecodeObjectTypeRef.fromRuntimeClass(Array.class));
} else {
theInvocationTarget = linkerContext.resolveClass((BytecodeObjectTypeRef) aInvocationTarget);
}

if (aClassFilter.test(theInvocationTarget)) {
// We don't have to check the type hierarchy for final classes
// Finding the implementation type can be done faster in this case
if (theInvocationTarget.getBytecodeClass().getAccessFlags().isFinal()) {
BytecodeLinkedClass theCurrent = theInvocationTarget;
while (theCurrent != null) {
final BytecodeMethod m = theCurrent.getBytecodeClass().methodByNameAndSignatureOrNull(aMethodName, aSignature);
if (m != null && aMethodFilter.test(m)) {
return Optional.of(theCurrent);
}
theCurrent = theCurrent.getSuperClass();
}
return Optional.empty();
}

// We have to check the whole type hierarchy
final Set<BytecodeLinkedClass> theResult = new HashSet<>();
linkerContext.linkedClasses()
.map(Edge::targetNode)
.filter(aClassFilter)
.filter(t -> {
final Set<BytecodeLinkedClass> theImplementingTypes = t.getImplementingTypes();
return theImplementingTypes.contains(theInvocationTarget);
})
.forEach(clz -> {
BytecodeLinkedClass theCurrent = clz;
test:
while (theCurrent != null) {
final BytecodeMethod m = theCurrent.getBytecodeClass().methodByNameAndSignatureOrNull(aMethodName, aSignature);
if (m != null && aMethodFilter.test(m)) {
theResult.add(theCurrent);
break test;
}
theCurrent = theCurrent.getSuperClass();
}
});

if (theResult.size() == 1) {
return Optional.of(theResult.iterator().next());
} else if (theResult.size() > 1) {
// We might check the type of the receiver here,
// but i haven't found a stable solution for this problem
// This happens when calling abstract methods in base types and there
// are multiple implementation types available.
// Bytecoder Dataflow Analysis cannot do this
}
}
return Optional.empty();
}
}
Loading

0 comments on commit a6ba5bf

Please sign in to comment.