Skip to content

Commit

Permalink
Add a linker that understands consts
Browse files Browse the repository at this point in the history
The ConstAwareLinker turns invocations to look up object properties and
variables that are truly constant into constants in the JVM. This makes
constants massively faster -- when people use them.
  • Loading branch information
gbrail committed Oct 7, 2024
1 parent 53e49ac commit 75ac904
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import jdk.dynalink.Operation;
import jdk.dynalink.StandardNamespace;
import jdk.dynalink.StandardOperation;
import jdk.dynalink.support.SimpleRelinkableCallSite;
import jdk.dynalink.support.ChainedCallSite;
import org.mozilla.classfile.ByteCode;
import org.mozilla.classfile.ClassFileWriter;

Expand All @@ -35,7 +35,7 @@ public class Bootstrapper {
static {
// Set up the linkers that will be invoked whenever a call site needs to be resolved.
DynamicLinkerFactory factory = new DynamicLinkerFactory();
factory.setPrioritizedLinkers(new DefaultLinker());
factory.setPrioritizedLinkers(new ConstAwareLinker(), new DefaultLinker());
linker = factory.createLinker();
}

Expand All @@ -45,8 +45,7 @@ public static CallSite bootstrap(MethodHandles.Lookup lookup, String name, Metho
throws NoSuchMethodException, IllegalAccessException {
Operation op = parseOperation(name);
// For now, use a very simple call site.
// When we change to add more linkers, we will likely switch to ChainedCallSite.
return linker.link(new SimpleRelinkableCallSite(new CallSiteDescriptor(lookup, op, mType)));
return linker.link(new ChainedCallSite(new CallSiteDescriptor(lookup, op, mType)));
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package org.mozilla.javascript.optimizer;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import jdk.dynalink.NamedOperation;
import jdk.dynalink.NamespaceOperation;
import jdk.dynalink.Operation;
import jdk.dynalink.StandardNamespace;
import jdk.dynalink.StandardOperation;
import jdk.dynalink.linker.GuardedInvocation;
import jdk.dynalink.linker.GuardingDynamicLinker;
import jdk.dynalink.linker.LinkRequest;
import jdk.dynalink.linker.LinkerServices;
import jdk.dynalink.linker.support.Guards;
import org.mozilla.javascript.NativeWith;
import org.mozilla.javascript.RhinoException;
import org.mozilla.javascript.ScriptableObject;

@SuppressWarnings("AndroidJdkLibsChecker")
class ConstAwareLinker implements GuardingDynamicLinker {
@Override
public GuardedInvocation getGuardedInvocation(LinkRequest req, LinkerServices svc)
throws Exception {
if (req.isCallSiteUnstable()) {
if (DefaultLinker.DEBUG) {
System.out.println(
req.getCallSiteDescriptor().getOperation() + ": unstable call site");
}
return null;
}

MethodHandles.Lookup lookup = MethodHandles.lookup();
Operation rootOp = req.getCallSiteDescriptor().getOperation();
MethodType mType = req.getCallSiteDescriptor().getMethodType();
String name = DefaultLinker.getName(rootOp);
Operation op = NamedOperation.getBaseOperation(rootOp);
Object target = req.getReceiver();

if (NamespaceOperation.contains(op, StandardOperation.GET, RhinoNamespace.NAME)
|| NamespaceOperation.contains(
op, StandardOperation.GET, StandardNamespace.PROPERTY)
|| NamespaceOperation.contains(
op, RhinoOperation.GETNOWARN, StandardNamespace.PROPERTY)) {
Object constValue = getConstValue(target, name);
if (constValue != null) {
// The guard returns boolean and compares the first argument to the
// target here. This works because the target is always our first argument.
MethodHandle guard = Guards.asType(Guards.getIdentityGuard(target), mType);
// Replace the actual method invocation with one that just returns a constant.
// Works because we can drop all arguments here.
MethodHandle mh =
MethodHandles.dropArguments(
MethodHandles.constant(Object.class, constValue),
0,
mType.parameterList());
if (DefaultLinker.DEBUG) {
System.out.println(rootOp + " constant");
}
return new GuardedInvocation(mh, guard);
}
}

return null;
}

/**
* Return the value of the specified property, but only if it's found in the root object that we
* search, and only if it's a constant. Return null otherwise, which means that we can't handle
* constants with a value of "null," which should not be a big loss.
*/
private Object getConstValue(Object t, String name) {
if (t instanceof NativeWith) {
// Support constants referenced from inside functions
return getConstValue(((NativeWith) t).getPrototype(), name);
}
if (!(t instanceof ScriptableObject)) {
return null;
}
try {
ScriptableObject target = (ScriptableObject) t;
// Just look in the root of the object -- don't mess around with
// nested scopes and things, to keep this simple and foolproof.
if (target.has(name, target)) {
int attributes = target.getAttributes(name);
if ((attributes & ScriptableObject.READONLY) != 0
&& (attributes & ScriptableObject.PERMANENT) != 0
&& (attributes & ScriptableObject.UNINITIALIZED_CONST) == 0) {
// If we get here then this object's value will not change for the
// lifetime of the target object.
return target.get(name, target);
}
}
} catch (RhinoException re) {
// Some implementations of ScriptableObject will fail on this operation with
// an exception, so treat that as "not found".
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ private GuardedInvocation getNameInvocation(
}

/** If the operation is a named operation, then return the name, */
private static String getName(Operation op) {
static String getName(Operation op) {
Object nameObj = NamedOperation.getName(op);
if (nameObj instanceof String) {
return (String) nameObj;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.mozilla.javascript.tests;

import org.mozilla.javascript.Context;
import org.mozilla.javascript.drivers.LanguageVersion;
import org.mozilla.javascript.drivers.RhinoTest;
import org.mozilla.javascript.drivers.ScriptTestsBase;

@RhinoTest("testsrc/jstests/const-white-box.js")
@LanguageVersion(Context.VERSION_ES6)
public class ConstWhiteBoxTest extends ScriptTestsBase {}
63 changes: 63 additions & 0 deletions tests/testsrc/jstests/const-white-box.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
load("testsrc/assert.js");

// These tests deliberately exercise the optimizations in ConstAwareLinker.

// Verify that things that aren't constant aren't constant
var notConstant = 1;
assertEquals(1, notConstant);
notConstant = 2;
assertEquals(2, notConstant);

// Verify that things that are constant stay constant
const constant = 1;
assertEquals(1, constant);
constant = 2;
assertEquals(1, constant);

// Verify that this works in a loop
function checkConstantness() {
constant++;
assertEquals(1, constant);
}
const ITERATIONS = 10;
for (let i = 0; i < ITERATIONS; i++) {
checkConstantness();
}

// Verify that we can set a local constant in a function
function localConstantness() {
const localConst = 1;
assertEquals(1, localConst);
localConst = 2;
assertEquals(1, localConst);
}
for (let i = 0; i < ITERATIONS; i++) {
localConstantness();
}

// Set up an object with a const field and try it out
const o = {
notConst: 1,
};
Object.defineProperty(o, "const", {
value: 1,
configurable: false,
writable: false,
});
assertEquals(1, o.notConst);
assertEquals(1, o.const);
o.notConst = 2;
o.const = 2;
assertEquals(2, o.notConst);
assertEquals(1, o.const);

// Verify that it works in a function
function objectConstness() {
o.const++;
assertEquals(1, o.const);
}
for (let i = 0; i < ITERATIONS; i++) {
objectConstness();
}

'success';

0 comments on commit 75ac904

Please sign in to comment.