Skip to content

Commit

Permalink
Replace some substring() and substr() calls with charAt()
Browse files Browse the repository at this point in the history
Replace cases like foo.substring(0, 1) with foo.charAt(0). According to https://jsperf.com/substr-or-charat, charAt is about 20x faster on Chrome 54. This also saves a bit on code size.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=138557701
  • Loading branch information
Dominator008 authored and blickly committed Nov 9, 2016
1 parent 13f48ca commit 39184a6
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 88 deletions.
11 changes: 6 additions & 5 deletions src/com/google/javascript/jscomp/DefaultPassConfig.java
Expand Up @@ -1541,12 +1541,12 @@ protected CompilerPass create(AbstractCompiler compiler) {
/** Various peephole optimizations. */ /** Various peephole optimizations. */
private static CompilerPass createPeepholeOptimizationsPass(AbstractCompiler compiler) { private static CompilerPass createPeepholeOptimizationsPass(AbstractCompiler compiler) {
final boolean late = false; final boolean late = false;
final boolean useTypesForOptimization = compiler.getOptions().useTypesForLocalOptimization; final boolean useTypesForOptimization = compiler.getOptions().useTypesForLocalOptimization;
return new PeepholeOptimizationsPass(compiler, return new PeepholeOptimizationsPass(compiler,
new MinimizeExitPoints(compiler), new MinimizeExitPoints(compiler),
new PeepholeMinimizeConditions(late, useTypesForOptimization), new PeepholeMinimizeConditions(late, useTypesForOptimization),
new PeepholeSubstituteAlternateSyntax(late), new PeepholeSubstituteAlternateSyntax(late),
new PeepholeReplaceKnownMethods(late), new PeepholeReplaceKnownMethods(late, useTypesForOptimization),
new PeepholeRemoveDeadCode(), new PeepholeRemoveDeadCode(),
new PeepholeFoldConstants(late, useTypesForOptimization), new PeepholeFoldConstants(late, useTypesForOptimization),
new PeepholeCollectPropertyAssignments()); new PeepholeCollectPropertyAssignments());
Expand Down Expand Up @@ -1576,13 +1576,14 @@ protected CompilerPass create(AbstractCompiler compiler) {
@Override @Override
protected CompilerPass create(AbstractCompiler compiler) { protected CompilerPass create(AbstractCompiler compiler) {
final boolean late = true; final boolean late = true;
final boolean useTypesForOptimization = options.useTypesForLocalOptimization;
return new PeepholeOptimizationsPass(compiler, return new PeepholeOptimizationsPass(compiler,
new StatementFusion(options.aggressiveFusion), new StatementFusion(options.aggressiveFusion),
new PeepholeRemoveDeadCode(), new PeepholeRemoveDeadCode(),
new PeepholeMinimizeConditions(late, options.useTypesForLocalOptimization), new PeepholeMinimizeConditions(late, useTypesForOptimization),
new PeepholeSubstituteAlternateSyntax(late), new PeepholeSubstituteAlternateSyntax(late),
new PeepholeReplaceKnownMethods(late), new PeepholeReplaceKnownMethods(late, useTypesForOptimization),
new PeepholeFoldConstants(late, options.useTypesForLocalOptimization), new PeepholeFoldConstants(late, useTypesForOptimization),
new ReorderConstantExpression()); new ReorderConstantExpression());
} }
}; };
Expand Down
23 changes: 13 additions & 10 deletions src/com/google/javascript/jscomp/ExpandJqueryAliases.java
Expand Up @@ -24,7 +24,6 @@
import com.google.javascript.rhino.IR; import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node; import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token; import com.google.javascript.rhino.Token;

import java.util.ArrayList; import java.util.ArrayList;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
Expand Down Expand Up @@ -88,7 +87,7 @@ class ExpandJqueryAliases extends AbstractPostOrderCallback
this.peepholePasses = new PeepholeOptimizationsPass(compiler, this.peepholePasses = new PeepholeOptimizationsPass(compiler,
new PeepholeMinimizeConditions(late, useTypesForOptimization), new PeepholeMinimizeConditions(late, useTypesForOptimization),
new PeepholeSubstituteAlternateSyntax(late), new PeepholeSubstituteAlternateSyntax(late),
new PeepholeReplaceKnownMethods(late), new PeepholeReplaceKnownMethods(late, useTypesForOptimization),
new PeepholeRemoveDeadCode(), new PeepholeRemoveDeadCode(),
new PeepholeFoldConstants(late, useTypesForOptimization), new PeepholeFoldConstants(late, useTypesForOptimization),
new PeepholeCollectPropertyAssignments()); new PeepholeCollectPropertyAssignments());
Expand All @@ -109,11 +108,13 @@ public static boolean isJqueryExtendCall(Node n, String qname,
} }


Node secondArgument = firstArgument.getNext(); Node secondArgument = firstArgument.getNext();
if ((firstArgument.isObjectLit() && secondArgument == null) || if ((firstArgument.isObjectLit() && secondArgument == null)
(firstArgument.isName() || NodeUtil.isGet(firstArgument) && || (firstArgument.isName()
!NodeUtil.mayHaveSideEffects(firstArgument, compiler) && || (NodeUtil.isGet(firstArgument)
secondArgument != null && secondArgument.isObjectLit() && && !NodeUtil.mayHaveSideEffects(firstArgument, compiler)
secondArgument.getNext() == null)) { && secondArgument != null
&& secondArgument.isObjectLit()
&& secondArgument.getNext() == null))) {
return true; return true;
} }
} }
Expand Down Expand Up @@ -355,7 +356,8 @@ private Node tryExpandJqueryEachCall(NodeTraversal t, Node n,
boolean isValidExpansion = true; boolean isValidExpansion = true;


// Expand the jQuery.expandedEach call // Expand the jQuery.expandedEach call
Node key = objectToLoopOver.getFirstChild(), val = null; Node key = objectToLoopOver.getFirstChild();
Node val = null;
for (int i = 0; key != null; key = key.getNext(), i++) { for (int i = 0; key != null; key = key.getNext(), i++) {
if (key != null) { if (key != null) {
if (objectToLoopOver.isArrayLit()) { if (objectToLoopOver.isArrayLit()) {
Expand Down Expand Up @@ -535,7 +537,8 @@ static class FindCallbackArgumentReferences extends AbstractPostOrderCallback
List<Node> valueReferences, boolean useArrayMode) { List<Node> valueReferences, boolean useArrayMode) {
Preconditions.checkState(functionRoot.isFunction()); Preconditions.checkState(functionRoot.isFunction());


String keyString = null, valueString = null; String keyString = null;
String valueString = null;
Node callbackParams = NodeUtil.getFunctionParameters(functionRoot); Node callbackParams = NodeUtil.getFunctionParameters(functionRoot);
Node param = callbackParams.getFirstChild(); Node param = callbackParams.getFirstChild();
if (param != null) { if (param != null) {
Expand Down Expand Up @@ -579,7 +582,7 @@ public void visit(NodeTraversal t, Node n, Node parent) {
isThis = n.isThis(); isThis = n.isThis();
} }


if (isThis || n.isName() && !isShadowed(n.getString(), t.getScope())) { if (isThis || (n.isName() && !isShadowed(n.getString(), t.getScope()))) {
String nodeValue = isThis ? null : n.getString(); String nodeValue = isThis ? null : n.getString();
if (!isThis && keyName != null && nodeValue.equals(keyName)) { if (!isThis && keyName != null && nodeValue.equals(keyName)) {
keyReferences.add(n); keyReferences.add(n);
Expand Down
171 changes: 110 additions & 61 deletions src/com/google/javascript/jscomp/PeepholeReplaceKnownMethods.java
Expand Up @@ -21,7 +21,6 @@
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.javascript.rhino.IR; import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node; import com.google.javascript.rhino.Node;

import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
Expand All @@ -34,15 +33,17 @@
class PeepholeReplaceKnownMethods extends AbstractPeepholeOptimization{ class PeepholeReplaceKnownMethods extends AbstractPeepholeOptimization{


private final boolean late; private final boolean late;
private final boolean useTypes;


/** /**
* @param late When late is true, this mean we are currently running after * @param late When late is true, this mean we are currently running after
* most of the other optimizations. In this case we avoid changes that make * most of the other optimizations. In this case we avoid changes that make
* the code larger (but otherwise easier to analyze - such as using string * the code larger (but otherwise easier to analyze - such as using string
* splitting). * splitting).
*/ */
PeepholeReplaceKnownMethods(boolean late) { PeepholeReplaceKnownMethods(boolean late, boolean useTypes) {
this.late = late; this.late = late;
this.useTypes = useTypes;
} }


@Override @Override
Expand All @@ -67,9 +68,9 @@ private Node tryFoldKnownMethods(Node subtree) {
} }


if (NodeUtil.isGet(callTarget)) { if (NodeUtil.isGet(callTarget)) {
subtree = tryFoldKnownStringMethods(subtree); subtree = tryFoldKnownStringMethods(subtree, callTarget);
} else { } else if (callTarget.isName()) {
subtree = tryFoldKnownNumericMethods(subtree); subtree = tryFoldKnownNumericMethods(subtree, callTarget);
} }
} }


Expand All @@ -80,74 +81,70 @@ private Node tryFoldKnownMethods(Node subtree) {
* Try to evaluate known String methods * Try to evaluate known String methods
* .indexOf(), .substr(), .substring() * .indexOf(), .substr(), .substring()
*/ */
private Node tryFoldKnownStringMethods(Node subtree) { private Node tryFoldKnownStringMethods(Node subtree, Node callTarget) {
Preconditions.checkArgument(subtree.isCall()); Preconditions.checkArgument(subtree.isCall());


// check if this is a call on a string method // check if this is a call on a string method
// then dispatch to specific folding method. // then dispatch to specific folding method.
Node callTarget = subtree.getFirstChild();
if (callTarget == null) {
return subtree;
}

if (!NodeUtil.isGet(callTarget)) {
return subtree;
}

Node stringNode = callTarget.getFirstChild(); Node stringNode = callTarget.getFirstChild();
Node functionName = stringNode.getNext(); Node functionName = callTarget.getLastChild();


if ((!stringNode.isString()) || if (!functionName.isString()) {
(!functionName.isString())) {
return subtree; return subtree;
} }


boolean isStringLiteral = stringNode.isString();
String functionNameString = functionName.getString(); String functionNameString = functionName.getString();
Node firstArg = callTarget.getNext(); Node firstArg = callTarget.getNext();
if (functionNameString.equals("split")) { if (isStringLiteral) {
subtree = tryFoldStringSplit(subtree, stringNode, firstArg); if (functionNameString.equals("split")) {
} else if (firstArg == null) { return tryFoldStringSplit(subtree, stringNode, firstArg);
if (functionNameString.equals("toLowerCase")) { } else if (firstArg == null) {
subtree = tryFoldStringToLowerCase(subtree, stringNode); switch (functionNameString) {
} else if (functionNameString.equals("toUpperCase")) { case "toLowerCase":
subtree = tryFoldStringToUpperCase(subtree, stringNode); return tryFoldStringToLowerCase(subtree, stringNode);
case "toUpperCase":
return tryFoldStringToUpperCase(subtree, stringNode);
}
} else {
if (NodeUtil.isImmutableValue(firstArg)) {
switch (functionNameString) {
case "indexOf":
case "lastIndexOf":
return tryFoldStringIndexOf(subtree, functionNameString, stringNode, firstArg);
case "substr":
return tryFoldStringSubstr(subtree, stringNode, firstArg);
case "substring":
return tryFoldStringSubstring(subtree, stringNode, firstArg);
case "charAt":
return tryFoldStringCharAt(subtree, stringNode, firstArg);
case "charCodeAt":
return tryFoldStringCharCodeAt(subtree, stringNode, firstArg);
}
}
} }
return subtree; } else if (useTypes && firstArg != null
} else if (NodeUtil.isImmutableValue(firstArg)) { && stringNode.getJSType() != null && stringNode.getJSType().isStringValueType()) {
if (functionNameString.equals("indexOf") || switch (functionNameString) {
functionNameString.equals("lastIndexOf")) { case "substr":
subtree = tryFoldStringIndexOf(subtree, functionNameString, return tryReplaceSubstrWithCharAt(subtree, callTarget, firstArg);
stringNode, firstArg); case "substring":
} else if (functionNameString.equals("substr")) { return tryReplaceSubstringWithCharAt(subtree, callTarget, firstArg);
subtree = tryFoldStringSubstr(subtree, stringNode, firstArg);
} else if (functionNameString.equals("substring")) {
subtree = tryFoldStringSubstring(subtree, stringNode, firstArg);
} else if (functionNameString.equals("charAt")) {
subtree = tryFoldStringCharAt(subtree, stringNode, firstArg);
} else if (functionNameString.equals("charCodeAt")) {
subtree = tryFoldStringCharCodeAt(subtree, stringNode, firstArg);
} }
} }

return subtree; return subtree;
} }


/** /**
* Try to evaluate known Numeric methods * Try to evaluate known Numeric methods
* parseInt(), parseFloat() * parseInt(), parseFloat()
*/ */
private Node tryFoldKnownNumericMethods(Node subtree) { private Node tryFoldKnownNumericMethods(Node subtree, Node callTarget) {
Preconditions.checkArgument(subtree.isCall()); Preconditions.checkArgument(subtree.isCall());


if (isASTNormalized()) { if (isASTNormalized()) {
// check if this is a call on a string method // check if this is a call on a string method
// then dispatch to specific folding method. // then dispatch to specific folding method.
Node callTarget = subtree.getFirstChild();

if (!callTarget.isName()) {
return subtree;
}

String functionNameString = callTarget.getString(); String functionNameString = callTarget.getString();
Node firstArgument = callTarget.getNext(); Node firstArgument = callTarget.getNext();
if ((firstArgument != null) && (firstArgument.isString() || firstArgument.isNumber()) if ((firstArgument != null) && (firstArgument.isString() || firstArgument.isNumber())
Expand Down Expand Up @@ -191,7 +188,8 @@ private static String normalizeNumericString(String input) {
return input; return input;
} }


int startIndex = 0, endIndex = input.length() - 1; int startIndex = 0;
int endIndex = input.length() - 1;


// Remove leading zeros // Remove leading zeros
while (startIndex < input.length() && input.charAt(startIndex) == '0' && while (startIndex < input.length() && input.charAt(startIndex) == '0' &&
Expand Down Expand Up @@ -509,22 +507,24 @@ private Node tryFoldArrayJoin(Node n) {
private Node tryFoldStringSubstr(Node n, Node stringNode, Node arg1) { private Node tryFoldStringSubstr(Node n, Node stringNode, Node arg1) {
Preconditions.checkArgument(n.isCall()); Preconditions.checkArgument(n.isCall());
Preconditions.checkArgument(stringNode.isString()); Preconditions.checkArgument(stringNode.isString());
Preconditions.checkArgument(arg1 != null);


int start, length; int start;
int length;
String stringAsString = stringNode.getString(); String stringAsString = stringNode.getString();


// TODO(nicksantos): We really need a NodeUtil.getNumberValue Double maybeStart = NodeUtil.getNumberValue(arg1, useTypes);
// function. if (maybeStart != null) {
if (arg1 != null && arg1.isNumber()) { start = maybeStart.intValue();
start = (int) arg1.getDouble();
} else { } else {
return n; return n;
} }


Node arg2 = arg1.getNext(); Node arg2 = arg1.getNext();
if (arg2 != null) { if (arg2 != null) {
if (arg2.isNumber()) { Double maybeLength = NodeUtil.getNumberValue(arg2, useTypes);
length = (int) arg2.getDouble(); if (maybeLength != null) {
length = maybeLength.intValue();
} else { } else {
return n; return n;
} }
Expand Down Expand Up @@ -563,20 +563,24 @@ private Node tryFoldStringSubstr(Node n, Node stringNode, Node arg1) {
private Node tryFoldStringSubstring(Node n, Node stringNode, Node arg1) { private Node tryFoldStringSubstring(Node n, Node stringNode, Node arg1) {
Preconditions.checkArgument(n.isCall()); Preconditions.checkArgument(n.isCall());
Preconditions.checkArgument(stringNode.isString()); Preconditions.checkArgument(stringNode.isString());
Preconditions.checkArgument(arg1 != null);


int start, end; int start;
int end;
String stringAsString = stringNode.getString(); String stringAsString = stringNode.getString();


if (arg1 != null && arg1.isNumber()) { Double maybeStart = NodeUtil.getNumberValue(arg1, useTypes);
start = (int) arg1.getDouble(); if (maybeStart != null) {
start = maybeStart.intValue();
} else { } else {
return n; return n;
} }


Node arg2 = arg1.getNext(); Node arg2 = arg1.getNext();
if (arg2 != null) { if (arg2 != null) {
if (arg2.isNumber()) { Double maybeEnd = NodeUtil.getNumberValue(arg2, useTypes);
end = (int) arg2.getDouble(); if (maybeEnd != null) {
end = maybeEnd.intValue();
} else { } else {
return n; return n;
} }
Expand Down Expand Up @@ -610,6 +614,50 @@ private Node tryFoldStringSubstring(Node n, Node stringNode, Node arg1) {
return resultNode; return resultNode;
} }


private Node tryReplaceSubstrWithCharAt(Node n, Node callTarget, Node firstArg) {
if (n.getChildCount() == 3) {
Double maybeLength = NodeUtil.getNumberValue(firstArg.getNext(), useTypes);
if (maybeLength != null) {
int length = maybeLength.intValue();
if (length == 1) {
return replaceWithCharAt(n, callTarget, firstArg);
}
}
}
return n;
}

private Node tryReplaceSubstringWithCharAt(Node n, Node callTarget, Node firstArg) {
if (n.getChildCount() != 3) {
return n;
}
Double maybeStart = NodeUtil.getNumberValue(firstArg, useTypes);
if (maybeStart == null) {
return n;
}
Node secondArg = firstArg.getNext();
Double maybeEnd = NodeUtil.getNumberValue(secondArg, useTypes);
if (maybeEnd == null) {
return n;
}
int start = maybeStart.intValue();
int end = maybeEnd.intValue();
if (Math.abs(start - end) == 1) {
return replaceWithCharAt(n, callTarget, start < end ? firstArg : secondArg);
}
return n;
}

private Node replaceWithCharAt(Node n, Node callTarget, Node firstArg) {
// TODO(moz): Maybe correct the arity of the function type here.
n.detachChildren();
callTarget.getLastChild().setString("charAt");
n.addChildToFront(callTarget);
n.addChildToBack(firstArg);
reportCodeChange();
return n;
}

/** /**
* Try to fold .charAt() calls on strings * Try to fold .charAt() calls on strings
*/ */
Expand Down Expand Up @@ -718,7 +766,8 @@ private String[] jsSplit(String stringValue, String separator, int limit) {
splitStrings.add(stringValue.substring(i, i + 1)); splitStrings.add(stringValue.substring(i, i + 1));
} }
} else { } else {
int startIndex = 0, matchIndex; int startIndex = 0;
int matchIndex;
while ((matchIndex = while ((matchIndex =
jsSplitMatch(stringValue, startIndex, separator)) >= 0 && jsSplitMatch(stringValue, startIndex, separator)) >= 0 &&
splitStrings.size() < limit) { splitStrings.size() < limit) {
Expand Down
Expand Up @@ -380,6 +380,7 @@ public static void addNativeProperties(JSTypeRegistry registry) {
addMethod(registry, stringPrototype, "search", numberType); addMethod(registry, stringPrototype, "search", numberType);
addMethod(registry, stringPrototype, "slice", stringType); addMethod(registry, stringPrototype, "slice", stringType);
addMethod(registry, stringPrototype, "split", arrayType); addMethod(registry, stringPrototype, "split", arrayType);
addMethod(registry, stringPrototype, "substr", stringType);
addMethod(registry, stringPrototype, "substring", stringType); addMethod(registry, stringPrototype, "substring", stringType);
addMethod(registry, stringPrototype, "toLowerCase", stringType); addMethod(registry, stringPrototype, "toLowerCase", stringType);
addMethod(registry, stringPrototype, "toLocaleLowerCase", stringType); addMethod(registry, stringPrototype, "toLocaleLowerCase", stringType);
Expand Down

6 comments on commit 39184a6

@phistuck
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not string[1] instead of string.charAt(1)? Shorter and marginally more performant in Chrome, significantly more performant in Internet Explorer 11 -
https://jsperf.com/substr-or-charat/2

(In Firefox, everything has more or less the same performance characteristics)

@MatrixFrog
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe string[1] doesn't work in some older IEs. If we had a flag to indicate whether you need to support those, we could probably do that.

@phistuck
Copy link
Contributor

@phistuck phistuck commented on 39184a6 Nov 13, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does not work in the ancient Internet Explorer 7 and older. Are those really still supported?
http://netmarketshare.com/ desktop by version shows those as lower than a percent (combined).
http://gs.statcounter.com/#browser_version_partially_combined-ww-monthly-201610-201611-bar shows them (export the data to see) as lower than 0.12% (combined).

Anyway, you can use the ECMAScript 5 language output to control it, because it is part of that, according to https://kangax.github.io/compat-table/es5

@ChadKillingsworth
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does not work in the ancient Internet Explorer 7 and older. Are those really still supported?

Yes. Google Search and others.

use the ECMAScript 5 language output

That's my suggestion as well

@MatrixFrog
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or should we just have a separate pass that explicitly converts charAt calls (whether added by this pass, or in the original code) to bracket notation?

@ChadKillingsworth
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems very suited as a peephole optimization.

Please sign in to comment.