Skip to content

Commit

Permalink
Add runtime optimizations for math operations
Browse files Browse the repository at this point in the history
This adds a number of special-purpose linkers for invoke dynamic instructions that let us take a shorter code path for many math operations and a few other things, including:

* "==", "===", and "+" operations
* Other math operations like conversions
* Access to the "prototype" field of functions
* Access to the "length" field of native arrays
  • Loading branch information
gbrail authored Dec 3, 2024
1 parent 75e9c9f commit 822b5c7
Show file tree
Hide file tree
Showing 11 changed files with 932 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,7 @@ protected boolean hasPrototypeProperty() {
return prototypeProperty != null || this instanceof NativeFunction;
}

protected Object getPrototypeProperty() {
public Object getPrototypeProperty() {
Object result = prototypeProperty;
if (result == null) {
// only create default prototype on native JavaScript functions,
Expand Down
22 changes: 11 additions & 11 deletions rhino/src/main/java/org/mozilla/javascript/ScriptRuntime.java
Original file line number Diff line number Diff line change
Expand Up @@ -3270,30 +3270,30 @@ public static Number remainder(Number val1, Number val2) {
// Integer-optimized methods.

public static Object add(Integer i1, Integer i2) {
// Try to add integers for efficiency, but account for overflow
long r = (long) i1.intValue() + (long) i2.intValue();
// Do 64-bit addition to account for overflow
long r = i1.longValue() + i2.longValue();
if ((r >= Integer.MIN_VALUE) && (r <= Integer.MAX_VALUE)) {
return Integer.valueOf((int) r);
return (int) r;
}
return Double.valueOf((double) r);
return (double) r;
}

public static Number subtract(Integer i1, Integer i2) {
// Account for overflow
long r = (long) i1.intValue() - (long) i2.intValue();
long r = i1.longValue() - i2.longValue();
if ((r >= Integer.MIN_VALUE) && (r <= Integer.MAX_VALUE)) {
return Integer.valueOf((int) r);
return (int) r;
}
return Double.valueOf((double) r);
return (double) r;
}

public static Number multiply(Integer i1, Integer i2) {
// Aunt for overflow
long r = (long) i1.intValue() * (long) i2.intValue();
// Account for overflow
long r = i1.longValue() * i2.longValue();
if ((r >= Integer.MIN_VALUE) && (r <= Integer.MAX_VALUE)) {
return Integer.valueOf((int) r);
return (int) r;
}
return Double.valueOf((double) r);
return (double) r;
}

@SuppressWarnings("AndroidJdkLibsChecker")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package org.mozilla.javascript.optimizer;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import jdk.dynalink.StandardNamespace;
import jdk.dynalink.StandardOperation;
import jdk.dynalink.linker.GuardedInvocation;
import jdk.dynalink.linker.LinkRequest;
import jdk.dynalink.linker.LinkerServices;
import jdk.dynalink.linker.TypeBasedGuardingDynamicLinker;
import jdk.dynalink.linker.support.Guards;
import org.mozilla.javascript.BaseFunction;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Scriptable;

/**
* This linker optimizes accesses to the "prototype" property of any standard Rhino function so that
* it calls the native function rather than going through' a property name match.
*/
@SuppressWarnings("AndroidJdkLibsChecker")
class BaseFunctionLinker implements TypeBasedGuardingDynamicLinker {
@Override
public boolean canLinkType(Class<?> type) {
return BaseFunction.class.isAssignableFrom(type);
}

@Override
public GuardedInvocation getGuardedInvocation(LinkRequest req, LinkerServices svc)
throws Exception {
if (req.isCallSiteUnstable()) {
return null;
}

ParsedOperation op = new ParsedOperation(req.getCallSiteDescriptor().getOperation());
MethodHandle mh = null;
MethodHandle guard = null;

if (op.isNamespace(StandardNamespace.PROPERTY)) {
if (op.isOperation(StandardOperation.GET, RhinoOperation.GETNOWARN)
&& "prototype".equals(op.getName())) {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType mType = req.getCallSiteDescriptor().getMethodType();
mh = lookup.findStatic(BaseFunctionLinker.class, "getPrototype", mType);
guard = Guards.getInstanceOfGuard(BaseFunction.class);
}
}

if (mh != null) {
assert guard != null;
if (DefaultLinker.DEBUG) {
System.out.println(op + " base function operation");
}
return new GuardedInvocation(mh, guard);
}

return null;
}

@SuppressWarnings("unused")
private static Object getPrototype(Object o, Context cx, Scriptable scope) {
return ((BaseFunction) o).getPrototypeProperty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package org.mozilla.javascript.optimizer;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.Objects;
import jdk.dynalink.linker.GuardedInvocation;
import jdk.dynalink.linker.LinkRequest;
import jdk.dynalink.linker.LinkerServices;
import jdk.dynalink.linker.TypeBasedGuardingDynamicLinker;
import jdk.dynalink.linker.support.Guards;

/**
* This linker short-circuits invocations of "==", "===", and "toBoolean" operations when the
* argument is already a boolean.
*/
@SuppressWarnings("AndroidJdkLibsChecker")
class BooleanLinker implements TypeBasedGuardingDynamicLinker {
@Override
public boolean canLinkType(Class<?> type) {
return Boolean.class.equals(type);
}

@Override
public GuardedInvocation getGuardedInvocation(LinkRequest req, LinkerServices svc)
throws Exception {
if (req.isCallSiteUnstable()) {
return null;
}

ParsedOperation op = new ParsedOperation(req.getCallSiteDescriptor().getOperation());
MethodHandle mh = null;
MethodHandle guard = null;

if (op.isNamespace(RhinoNamespace.MATH)) {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType mType = req.getCallSiteDescriptor().getMethodType();

if (op.isOperation(RhinoOperation.EQ, RhinoOperation.SHALLOWEQ)
&& req.getArguments()[1] instanceof Boolean) {
mh = lookup.findStatic(BooleanLinker.class, "eq", mType);
guard = lookup.findStatic(BooleanLinker.class, "testEq", mType);
} else if (op.isOperation(RhinoOperation.TOBOOLEAN)) {
mh = lookup.findStatic(BooleanLinker.class, "toBoolean", mType);
guard = Guards.getInstanceOfGuard(Boolean.class);
}
}

if (mh != null) {
assert guard != null;
if (DefaultLinker.DEBUG) {
System.out.println(op + " boolean operation");
}
return new GuardedInvocation(mh, guard);
}

return null;
}

@SuppressWarnings("unused")
private static boolean testEq(Object lval, Object rval) {
return lval instanceof Boolean && rval instanceof Boolean;
}

@SuppressWarnings("unused")
private static boolean eq(Object lval, Object rval) {
return Objects.equals(lval, rval);
}

@SuppressWarnings("unused")
private static boolean toBoolean(Object raw) {
return ((Boolean) raw);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,37 @@
import java.lang.invoke.CallSite;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.Arrays;
import java.util.regex.Pattern;
import jdk.dynalink.CallSiteDescriptor;
import jdk.dynalink.DynamicLinker;
import jdk.dynalink.DynamicLinkerFactory;
import jdk.dynalink.Operation;
import jdk.dynalink.StandardNamespace;
import jdk.dynalink.StandardOperation;
import jdk.dynalink.linker.support.CompositeTypeBasedGuardingDynamicLinker;
import jdk.dynalink.support.ChainedCallSite;
import org.mozilla.classfile.ByteCode;
import org.mozilla.classfile.ClassFileWriter;

/**
* The Bootstrapper contains the method that is called by invokedynamic instructions in the bytecode
* to map a call site to a method. We should never go down this entire code path on Android.
* to map a call site to a method. The "bootstrap" method here is called the first time the runtime
* encounters a particular "invokedynamic" call site, and it is responsible for setting up method
* handles that may be used to invoke code. To learn more about this entire sequence, read up on the
* "jdk.dynalink" package.
*
* <p>We will never go down this entire code path on Android because we do not support bytecode
* generation there.
*/
@SuppressWarnings("AndroidJdkLibsChecker")
public class Bootstrapper {
private static final Pattern SEPARATOR = Pattern.compile(":");

/**
* This is the method handle that's wired in to the bytecode for every dynamic call site in the
* bytecode.
*/
public static final ClassFileWriter.MHandle BOOTSTRAP_HANDLE =
new ClassFileWriter.MHandle(
ByteCode.MH_INVOKESTATIC,
Expand All @@ -33,27 +45,50 @@ public class Bootstrapper {
private static final DynamicLinker linker;

static {
// Set up the linkers
// Set up the linkers that will map each call site to a method handle.
DynamicLinkerFactory factory = new DynamicLinkerFactory();
// The const-aware-linker will only bind a few operations, and everything
// else will fall back to the default linker, which will always bind.
factory.setPrioritizedLinkers(new ConstAwareLinker(), new DefaultLinker());
// Set up a linker that will delegate to other linkers based on the class
// of the first argument to each dynamic invocation. (That's why the method
// signatures in "Signatures" sometimes have different orders than their
// counterparts in "ScriptRuntime".)
// The linker caches the results so that it can efficiently only delegate to
// compatible linkers. It will still go in order, so we put the linkers
// likely to have the biggest impact on performance at the top of the list.
CompositeTypeBasedGuardingDynamicLinker typeLinker =
new CompositeTypeBasedGuardingDynamicLinker(
Arrays.asList(
new ConstAwareLinker(),
new BooleanLinker(),
new IntegerLinker(),
new DoubleLinker(),
new StringLinker(),
new ConsStringLinker(),
new NativeArrayLinker(),
new BaseFunctionLinker()));
// Add the default linker, which can link anything no matter what.
factory.setPrioritizedLinkers(typeLinker, new DefaultLinker());
linker = factory.createLinker();
}

/** This is the method called by every call site in the bytecode to map it to a function. */
/**
* This is the method called by every call site in the bytecode to map it to a method handle.
*/
@SuppressWarnings("unused")
public static CallSite bootstrap(MethodHandles.Lookup lookup, String name, MethodType mType)
throws NoSuchMethodException {
Operation op = parseOperation(name);
// ChainedCallSite lets a call site have a few options for complex situations
// ChainedCallSite lets a call site have a few options for complex situations.
// It caches up to eight invocations, so that we can quickly select the best
// implementation in situations where the same call site is invoked in different
// contexts.
return linker.link(new ChainedCallSite(new CallSiteDescriptor(lookup, op, mType)));
}

/**
* Operation names in the bytecode are names like "PROP:GET:<NAME> and "NAME:BIND:<NAME>". This
* translates them the first time a call site is seen to an object that can be easily consumed
* by the various types of linkers.
* Operation names in the bytecode are names like "PROP:GET:<NAME> and "NAME:BIND:<NAME>". (See
* the "Signatures" interface for a description of these.) This method translates them the first
* time a call site is seen to an object that can be easily consumed by the various types of
* linkers.
*/
private static Operation parseOperation(String name) throws NoSuchMethodException {
String[] tokens = SEPARATOR.split(name, -1);
Expand Down Expand Up @@ -169,20 +204,23 @@ private static Operation parseOperation(String name) throws NoSuchMethodExceptio
}
}

// Fall through to no match. This should only happen if the name in the bytecode
// does not match the pattern that this method understands.
// Fall through to no match. This will only happen if the name in the bytecode
// does not match the pattern that this method understands, which means that
// there is a mismatch between the bytecode and the runtime.
throw new NoSuchMethodException(name);
}

// Given a list of name segments and a position, return the interned name at the
// specified position.
// specified position. This allows us, to pull a name like "foo" from an operation
// named, for example, "NAME:GET:foo".
private static String getNameSegment(String[] segments, String name, int pos) {
if (pos >= segments.length) {
return "";
}
// Because segments of operation names, especially property names, are essentially
// wired in to the bootstrapping result, interning works and has a big impact on
// performance.
// The "slot maps" in ScriptableObject-based classes can shortcut when property names
// are "==", so interning strings improves performance in a measurable way, because
// the property names that we pull from the INDY operation descriptors are essentially
// constants.
return segments[pos].intern();
}
}
Loading

0 comments on commit 822b5c7

Please sign in to comment.