diff --git a/core/src/main/java/de/mirkosertic/bytecoder/core/backend/sequencer/Sequencer.java b/core/src/main/java/de/mirkosertic/bytecoder/core/backend/sequencer/Sequencer.java index 2391adc593..162279a499 100644 --- a/core/src/main/java/de/mirkosertic/bytecoder/core/backend/sequencer/Sequencer.java +++ b/core/src/main/java/de/mirkosertic/bytecoder/core/backend/sequencer/Sequencer.java @@ -216,8 +216,10 @@ private void visitDominationTreeOf(final ControlTokenConsumer startNode, final S } } catch (final IllegalStateException e) { throw e; + } catch (final SequencerException e) { + throw e; } catch (final RuntimeException e) { - throw new RuntimeException("Error processing node #" + graph.nodes().indexOf(current) + " " + current.nodeType + current.additionalDebugInfo(), e); + throw new SequencerException("Error processing node #" + graph.nodes().indexOf(current) + " " + current.nodeType + current.additionalDebugInfo(), e); } } diff --git a/core/src/main/java/de/mirkosertic/bytecoder/core/backend/sequencer/SequencerException.java b/core/src/main/java/de/mirkosertic/bytecoder/core/backend/sequencer/SequencerException.java new file mode 100644 index 0000000000..2384ab99df --- /dev/null +++ b/core/src/main/java/de/mirkosertic/bytecoder/core/backend/sequencer/SequencerException.java @@ -0,0 +1,23 @@ +/* + * Copyright 2024 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.backend.sequencer; + +public class SequencerException extends RuntimeException{ + + public SequencerException(final String message, final Exception cause) { + super(message, cause); + } +} diff --git a/core/src/main/java/de/mirkosertic/bytecoder/core/ir/Goto.java b/core/src/main/java/de/mirkosertic/bytecoder/core/ir/Goto.java index 40757afda9..a3998ce822 100644 --- a/core/src/main/java/de/mirkosertic/bytecoder/core/ir/Goto.java +++ b/core/src/main/java/de/mirkosertic/bytecoder/core/ir/Goto.java @@ -25,4 +25,8 @@ public class Goto extends ControlTokenConsumer { public Goto stampInto(final Graph target) { return target.newGoto(); } + + public void deleteFromControlFlow() { + owner.deleteFromControlFlowInternally(this); + } } diff --git a/core/src/main/java/de/mirkosertic/bytecoder/core/optimizer/DropDebugData.java b/core/src/main/java/de/mirkosertic/bytecoder/core/optimizer/DropDebugData.java index 37288254a4..7e61263bdf 100644 --- a/core/src/main/java/de/mirkosertic/bytecoder/core/optimizer/DropDebugData.java +++ b/core/src/main/java/de/mirkosertic/bytecoder/core/optimizer/DropDebugData.java @@ -19,6 +19,7 @@ import de.mirkosertic.bytecoder.core.ir.ControlTokenConsumer; import de.mirkosertic.bytecoder.core.ir.EdgeType; import de.mirkosertic.bytecoder.core.ir.FrameDebugInfo; +import de.mirkosertic.bytecoder.core.ir.Goto; import de.mirkosertic.bytecoder.core.ir.Graph; import de.mirkosertic.bytecoder.core.ir.LineNumberDebugInfo; import de.mirkosertic.bytecoder.core.ir.NodeType; @@ -33,7 +34,7 @@ public class DropDebugData implements Optimizer { public boolean optimize(final BackendType backendType, final CompileUnit compileUnit, final ResolvedMethod method) { final Graph g = method.methodBody; boolean changed = false; - for (final ControlTokenConsumer t : g.nodes().stream().filter(t -> t.nodeType == NodeType.FrameDebugInfo || t.nodeType == NodeType.LineNumberDebugInfo).map(t -> (ControlTokenConsumer) t).collect(Collectors.toList())) { + for (final ControlTokenConsumer t : g.nodes().stream().filter(t -> t.nodeType == NodeType.FrameDebugInfo || t.nodeType == NodeType.LineNumberDebugInfo || t.nodeType == NodeType.Goto).map(t -> (ControlTokenConsumer) t).collect(Collectors.toList())) { if (t.controlComingFrom.size() == 1 && t.controlFlowsTo.size() == 1 && t.controlFlowsTo.keySet().iterator().next().edgeType() == EdgeType.FORWARD) { if (t.nodeType == NodeType.FrameDebugInfo) { ((FrameDebugInfo) t).deleteFromControlFlow(); @@ -43,6 +44,10 @@ public boolean optimize(final BackendType backendType, final CompileUnit compile ((LineNumberDebugInfo) t).deleteFromControlFlow(); changed = true; } + if (t.nodeType == NodeType.Goto) { + ((Goto) t).deleteFromControlFlow(); + changed = true; + } } } diff --git a/core/src/main/java/de/mirkosertic/bytecoder/core/optimizer/GraphPatternMatcher.java b/core/src/main/java/de/mirkosertic/bytecoder/core/optimizer/GraphPatternMatcher.java new file mode 100644 index 0000000000..09f8955e65 --- /dev/null +++ b/core/src/main/java/de/mirkosertic/bytecoder/core/optimizer/GraphPatternMatcher.java @@ -0,0 +1,263 @@ +/* + * Copyright 2024 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.optimizer; + +import de.mirkosertic.bytecoder.core.ir.ControlTokenConsumer; +import de.mirkosertic.bytecoder.core.ir.Graph; +import de.mirkosertic.bytecoder.core.ir.Node; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Stack; +import java.util.StringTokenizer; +import java.util.stream.Collectors; + +public class GraphPatternMatcher { + + private interface Context { + Node[] outgoingDataFlowsFor(final Node node); + } + + private static class Path { + + private final String path; + + public Path() { + this("R"); + } + + private Path(final String path) { + this.path = path; + } + + public Path addIncoming(final int i) { + return new Path(path + ".i[" + i + "]"); + } + + public Path addOutgoing(final int i) { + return new Path(path + ".o[" + i + "]"); + } + + public Path controlComingFrom(final int nodeIndex) { + return new Path(path + ".cin[" + nodeIndex + "]"); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final Path path1 = (Path) o; + return Objects.equals(path, path1.path); + } + + @Override + public int hashCode() { + return Objects.hashCode(path); + } + + public Node resolve(final Node root, final Context context) { + Node node = null; + final StringTokenizer st = new StringTokenizer(path, "."); + while (st.hasMoreTokens()) { + final String token = st.nextToken(); + if ("R".equals(token)) { + node = root; + } else if (token.startsWith("i[")) { + int index = Integer.parseInt(token.substring(2, token.length() - 1)); + final Node[] incoming = node.incomingDataFlows; + if (incoming.length > index) { + node = incoming[index]; + } else { + return null; + } + } else if (token.startsWith("o[")) { + int index = Integer.parseInt(token.substring(2, token.length() - 1)); + final Node[] outgoing = context.outgoingDataFlowsFor(node); + if (outgoing.length > index) { + node = outgoing[index]; + } else { + return null; + } + } else { + throw new IllegalStateException("what do do with " + token); + } + } + return node; + } + } + + private static class CompiledPattern { + + private final Map> nodeToPaths = new HashMap<>(); + private final List nodeIndex = new ArrayList<>(); + + private boolean registerToIndex(final Node n) { + if (!nodeIndex.contains(n)) { + nodeIndex.add(n); + return true; + } + return false; + } + + public int nodeIndexOf(final Node n) { + return nodeIndex.indexOf(n); + } + + public boolean matchesTo(final Node analysisPoint, final Context c) { + // Check for all nodes and paths if they resolve according to their path + for (final Map.Entry> entry : nodeToPaths.entrySet()) { + Node singularValue = null; + for (final Path p : entry.getValue()) { + final Node r = p.resolve(analysisPoint, c); + if (r != null) { + if (singularValue == null) { + singularValue = r; + } else { + if (singularValue != r) { + return false; + } + } + } else { + return false; + } + } + // todo: check index of singular value in template and at analysispoint?? + } + return true; + } + } + + private final Node pattern; + private final CompiledPattern compiledPattern; + + public GraphPatternMatcher(final Node patternToCompile) { + this.pattern = patternToCompile; + this.compiledPattern = compile(patternToCompile); + } + + private static class PathAnalysisState { + private final Node node; + private final Path path; + + public PathAnalysisState(final Node node, final Path path) { + this.node = node; + this.path = path; + } + } + + private void registerPaths(final Node pivot, final CompiledPattern compiledPattern) { + + final Path root = new Path(); + + final Stack workingQueue = new Stack<>(); + workingQueue.add(new PathAnalysisState(pivot, root)); + + compiledPattern.registerToIndex(pivot); + + while (!workingQueue.isEmpty()) { + final PathAnalysisState workingItem = workingQueue.pop(); + final Path workingItemPath = workingItem.path; + + final List workingItemPaths = compiledPattern.nodeToPaths.computeIfAbsent(workingItem.node, key -> new ArrayList<>()); + if (!workingItemPaths.contains(workingItemPath)) { + workingItemPaths.add(workingItemPath); + } + + final Node[] incomingDataFlow = workingItem.node.incomingDataFlows; + for (int i = 0; i < incomingDataFlow.length; i++) { + final Node n = incomingDataFlow[i]; + final boolean registered = compiledPattern.registerToIndex(n); + final Path newPath = workingItemPath.addIncoming(i); + + final List paths = compiledPattern.nodeToPaths.computeIfAbsent(n, key -> new ArrayList<>()); + if (!paths.contains(newPath)) { + paths.add(newPath); + } + + if (registered) { + workingQueue.add(new PathAnalysisState(n, newPath)); + } + } + final Node[] outgoing = workingItem.node.outgoingDataFlows(); + for (int i = 0; i < outgoing.length; i++) { + final Node n = outgoing[i]; + final boolean registered = compiledPattern.registerToIndex(n); + final Path newPath = workingItemPath.addOutgoing(i); + + final List paths = compiledPattern.nodeToPaths.computeIfAbsent(n, key -> new ArrayList<>()); + if (!paths.contains(newPath)) { + paths.add(newPath); + } + + if (registered) { + workingQueue.add(new PathAnalysisState(n, newPath)); + } + } + + if (workingItem.node instanceof ControlTokenConsumer) { + final ControlTokenConsumer control = (ControlTokenConsumer) workingItem.node; + for (final ControlTokenConsumer inc : control.controlComingFrom) { + final boolean registered = compiledPattern.registerToIndex(inc); + final Path newPath = workingItemPath.controlComingFrom(compiledPattern.nodeIndexOf(inc)); + + final List paths = compiledPattern.nodeToPaths.computeIfAbsent(inc, key -> new ArrayList<>()); + if (!paths.contains(newPath)) { + paths.add(newPath); + } + + if (registered) { + workingQueue.add(new PathAnalysisState(inc, newPath)); + } + } + } + } + } + + private CompiledPattern compile(final Node patternRoot) { + final CompiledPattern pattern = new CompiledPattern(); + + registerPaths(patternRoot, pattern); + + return pattern; + } + + public List findMatches(final Graph source) { + final List result = new ArrayList<>(); + + final Context c = new Context() { + + private final Map outgoingFlows = new HashMap<>(); + + @Override + public Node[] outgoingDataFlowsFor(final Node node) { + return outgoingFlows.computeIfAbsent(node, Node::outgoingDataFlows); + } + }; + + // We search for all analysis candidates in source with the same nodetype as pivot + for (final Node analysisPoint : source.nodes().stream().filter(t -> t.nodeType == pattern.nodeType).collect(Collectors.toList())) { + + if (compiledPattern.matchesTo(analysisPoint, c)) { + result.add(analysisPoint); + } + } + + return result; + } +} diff --git a/core/src/main/java/de/mirkosertic/bytecoder/core/optimizer/InefficientSetFieldOrArray.java b/core/src/main/java/de/mirkosertic/bytecoder/core/optimizer/InefficientSetFieldOrArray.java new file mode 100644 index 0000000000..2fdb1fba16 --- /dev/null +++ b/core/src/main/java/de/mirkosertic/bytecoder/core/optimizer/InefficientSetFieldOrArray.java @@ -0,0 +1,69 @@ +/* + * Copyright 2024 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.optimizer; + +import de.mirkosertic.bytecoder.core.backend.BackendType; +import de.mirkosertic.bytecoder.core.ir.ControlTokenConsumer; +import de.mirkosertic.bytecoder.core.ir.Copy; +import de.mirkosertic.bytecoder.core.ir.EdgeType; +import de.mirkosertic.bytecoder.core.ir.Graph; +import de.mirkosertic.bytecoder.core.ir.Node; +import de.mirkosertic.bytecoder.core.ir.NodeType; +import de.mirkosertic.bytecoder.core.ir.ResolvedMethod; +import de.mirkosertic.bytecoder.core.ir.Variable; +import de.mirkosertic.bytecoder.core.parser.CompileUnit; + +import java.util.stream.Collectors; + +public class InefficientSetFieldOrArray implements Optimizer { + + @Override + public boolean optimize(final BackendType backendType, final CompileUnit compileUnit, final ResolvedMethod method) { + final Graph g = method.methodBody; + boolean changed = false; + for (final Copy copy : g.nodes().stream().filter(t -> t.nodeType == NodeType.Copy).map(t -> (Copy) t).collect(Collectors.toList())) { + final Node[] outgoing = copy.outgoingDataFlows(); + if (outgoing.length == 1 && outgoing[0].nodeType == NodeType.Variable && copy.controlFlowsTo.size() == 1 && copy.controlFlowsTo.keySet().stream().noneMatch(t -> t.edgeType() == EdgeType.BACK)) { + final Variable copyTarget = (Variable) outgoing[0]; + // We are copying to a variable, and there is only one following successor control token + final ControlTokenConsumer successor = copy.controlFlowsTo.values().iterator().next(); + if (successor.nodeType == NodeType.SetClassField || successor.nodeType == NodeType.SetInstanceField || successor.nodeType == NodeType.ArrayStore) { + final Node source = copy.incomingDataFlows[0]; + // Case one : there is something written to the copyTarget + final Node[] succOutgoing = successor.outgoingDataFlows(); + if (source.nodeType == NodeType.Variable && copyTarget.incomingDataFlows.length == 2 && copyTarget.incomingDataFlows[0] == copy && copyTarget.incomingDataFlows[1] == successor && succOutgoing.length == 1 && succOutgoing[0] == copyTarget) { + + copyTarget.removeFromIncomingData(copy); + + copyTarget.removeFromIncomingData(successor); + source.addIncomingData(successor); + + copy.deleteFromControlFlow(); + + if (copyTarget.outgoingDataFlows().length == 0) { + g.deleteNode(copyTarget); + } + + changed = true; + } + } + } + } + + return changed; + + } +} diff --git a/core/src/main/java/de/mirkosertic/bytecoder/core/optimizer/Optimizations.java b/core/src/main/java/de/mirkosertic/bytecoder/core/optimizer/Optimizations.java index d1cf679ff3..9bbd51e2fd 100644 --- a/core/src/main/java/de/mirkosertic/bytecoder/core/optimizer/Optimizations.java +++ b/core/src/main/java/de/mirkosertic/bytecoder/core/optimizer/Optimizations.java @@ -37,6 +37,7 @@ public enum Optimizations implements Optimizer { new DropUnusedValues(), new CopyToUnusedPHIOrVariable(), new SingularPHIOrVariable(), + //new InefficientSetFieldOrArray(), }), ALL(new Optimizer[] { new DropDebugData(), @@ -52,6 +53,7 @@ public enum Optimizations implements Optimizer { new DropUnusedValues(), new CopyToUnusedPHIOrVariable(), new SingularPHIOrVariable(), + //new InefficientSetFieldOrArray(), }), ; diff --git a/core/src/main/java/de/mirkosertic/bytecoder/core/optimizer/STATS.md b/core/src/main/java/de/mirkosertic/bytecoder/core/optimizer/STATS.md index 22c91623b9..04e4045c33 100644 --- a/core/src/main/java/de/mirkosertic/bytecoder/core/optimizer/STATS.md +++ b/core/src/main/java/de/mirkosertic/bytecoder/core/optimizer/STATS.md @@ -74,3 +74,8 @@ JBox2D JS Opt3 1729499 bytes LUA Wasm Opt3 445050 bytes LUA JS Opt3 1566128 bytes + + JBox2D Wasm Opt4 440125 bytes + JBox2D JS Opt4 1699546 bytes + LUA Wasm Opt4 443106 bytes + LUA JS Opt4 1519864 bytes diff --git a/core/src/test/java/de/mirkosertic/bytecoder/IRExport.java b/core/src/test/java/de/mirkosertic/bytecoder/IRExport.java index 27b6334c37..b1fa46b1e7 100644 --- a/core/src/test/java/de/mirkosertic/bytecoder/IRExport.java +++ b/core/src/test/java/de/mirkosertic/bytecoder/IRExport.java @@ -21,6 +21,9 @@ public class IRExport { + int target; + int target2; + public static void doit(final String[] args) { } @@ -33,6 +36,9 @@ public int dosomething(final int value) { for (int i = 0; i < 100; i++) { x = x + i + value; } + IRExport t = new IRExport(); + t.target = x; + t.target2 = x + 1; return x; } @@ -45,12 +51,12 @@ public static void main(final String[] args) throws IOException, ClassNotFoundEx final CompileUnit compileUnit = new CompileUnit(loader, new Slf4JLogger(), new WasmIntrinsics()); final Type invokedType = Type.getType(javaClass); - //final ResolvedMethod method = compileUnit.resolveMainMethod(invokedType, "dosomething", Type.getMethodType(Type.INT_TYPE, Type.INT_TYPE)); + final ResolvedMethod method = compileUnit.resolveMainMethod(invokedType, "dosomething", Type.getMethodType(Type.INT_TYPE, Type.INT_TYPE)); //final ResolvedMethod method = compileUnit.resolveMainMethod(invokedType, "need_value", Type.getMethodType(Type.BOOLEAN_TYPE, Type.INT_TYPE)); //final ResolvedMethod method = compileUnit.resolveMainMethod(Type.getType(Buffer.class), "", Type.getMethodType(Type.VOID_TYPE)); //final ResolvedMethod method = compileUnit.resolveMainMethod(Type.getType("Ljdk/internal/util/ArraysSupport;"), "", Type.getMethodType(Type.VOID_TYPE)); - final ResolvedMethod method = compileUnit.resolveMainMethod(Type.getType("Ljava/lang/Object;"), "equals", Type.getMethodType(Type.BOOLEAN_TYPE, Type.getType(Object.class))); + //final ResolvedMethod method = compileUnit.resolveMainMethod(Type.getType("Ljava/lang/Object;"), "equals", Type.getMethodType(Type.BOOLEAN_TYPE, Type.getType(Object.class))); compileUnit.finalizeLinkingHierarchy(); diff --git a/core/src/test/java/de/mirkosertic/bytecoder/core/optimizer/GraphPatternMatcherTest.java b/core/src/test/java/de/mirkosertic/bytecoder/core/optimizer/GraphPatternMatcherTest.java new file mode 100644 index 0000000000..103acb90eb --- /dev/null +++ b/core/src/test/java/de/mirkosertic/bytecoder/core/optimizer/GraphPatternMatcherTest.java @@ -0,0 +1,61 @@ +/* + * Copyright 2024 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.optimizer; + +import de.mirkosertic.bytecoder.api.Logger; +import de.mirkosertic.bytecoder.core.Slf4JLogger; +import de.mirkosertic.bytecoder.core.ir.Graph; +import de.mirkosertic.bytecoder.core.ir.Node; +import de.mirkosertic.bytecoder.core.ir.Variable; +import org.junit.Test; +import org.objectweb.asm.Type; + +import static org.junit.Assert.assertTrue; + +public class GraphPatternMatcherTest { + + @Test + public void testEmpty() { + final Logger logger = new Slf4JLogger(); + final Graph source = new Graph(logger); + final Graph pattern = new Graph(logger); + final Node pivot = pattern.newRegion("Test"); + + final GraphPatternMatcher matcher = new GraphPatternMatcher(pivot); + assertTrue(matcher.findMatches(source).isEmpty()); + } + + @Test + public void testSearchForCopy() { + final Logger logger = new Slf4JLogger(); + final Graph source = new Graph(logger); + final Variable st = source.newVariable(Type.INT_TYPE); + final Node sc = source.newCopy(); + final Node sq = source.newInt(10); + st.addIncomingData(sc); + sc.addIncomingData(sq); + + final Graph pattern = new Graph(logger); + final Variable pt = pattern.newVariable(Type.INT_TYPE); + final Node pc = pattern.newCopy(); + final Node pq = pattern.newInt(10); + pt.addIncomingData(pc); + pc.addIncomingData(pq); + + final GraphPatternMatcher matcher = new GraphPatternMatcher(pc); + assertTrue(matcher.findMatches(source).size() == 1); + } +}