Skip to content
Permalink
Browse files
[JSC] Optimize String#substring
https://bugs.webkit.org/show_bug.cgi?id=244754
<rdar://99770218>

Reviewed by Mark Lam.

This patch optimizes String#substring.

1. We avoid allocating the whole string when just comparing with substring. (JSString::equalSlowCase change).
   We get StringView with unsafeView, and use ensureStillAliveHere to keep both string cells alive.
2. We optimize JSString::equalSlowCase's equal function using StringView.
3. Optimize String#substring runtime function with int32 input fast path.
4. Add DFG StringSubstring node to handle String#substring efficiently in DFG / FTL. We also add StrengthReduction rule
   for String#substring so that we can constant-fold the result if arguments are the constants.

We observed improvements in microbenchmarks.
                                                    ToT                     Patched

    string-substring-constants                30.9691+-0.7392     ^      6.9364+-0.1119        ^ definitely 4.4647x faster
    string-substring-constants-binary         29.5540+-0.0829     ^     12.0035+-0.2692        ^ definitely 2.4621x faster
    string-starts-with-mod-prototype          69.7019+-0.6867     ^     56.8859+-0.4956        ^ definitely 1.2253x faster
    string-substring                          30.9168+-0.2336     ^     18.9088+-0.1950        ^ definitely 1.6350x faster
    string-substring-length-constant          30.6379+-0.2457     ^      6.9260+-0.1237        ^ definitely 4.4236x faster
    string-substring-constants-identity        9.5178+-0.1149     ^      2.9170+-0.0326        ^ definitely 3.2629x faster
    string-starts-with                         2.9473+-0.0220            2.9296+-0.0749
    string-starts-with-mod                    31.1585+-0.1608     ^     12.9144+-0.0646        ^ definitely 2.4127x faster

* JSTests/microbenchmarks/string-starts-with-mod-prototype.js: Added.
(shouldBe):
(String.prototype._startsWith):
(test1):
(test2):
* JSTests/microbenchmarks/string-starts-with-mod.js: Added.
(shouldBe):
(_startsWith):
(test1):
(test2):
* JSTests/microbenchmarks/string-starts-with.js: Added.
(shouldBe):
(test1):
(test2):
* JSTests/microbenchmarks/string-substring-constants-binary.js: Added.
(shouldBe):
(test1):
* JSTests/microbenchmarks/string-substring-constants-identity.js: Added.
(shouldBe):
(test1):
* JSTests/microbenchmarks/string-substring-constants.js: Added.
(shouldBe):
(test1):
* JSTests/microbenchmarks/string-substring-length-constant.js: Added.
(shouldBe):
(test1):
* JSTests/microbenchmarks/string-substring.js: Added.
(shouldBe):
(test1):
* Source/JavaScriptCore/builtins/BuiltinNames.h:
* Source/JavaScriptCore/builtins/RegExpPrototype.js:
(linkTimeConstant.getSubstitution):
(overriddenName.string_appeared_here.replace):
(overriddenName.string_appeared_here.split):
* Source/JavaScriptCore/builtins/StringPrototype.js:
(linkTimeConstant.repeatCharactersSlowPath):
* Source/JavaScriptCore/bytecode/LinkTimeConstant.h:
* Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h:
(JSC::DFG::AbstractInterpreter<AbstractStateType>::executeEffects):
* Source/JavaScriptCore/dfg/DFGBackwardsPropagationPhase.cpp:
(JSC::DFG::BackwardsPropagationPhase::propagate):
* Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp:
(JSC::DFG::ByteCodeParser::handleIntrinsicCall):
* Source/JavaScriptCore/dfg/DFGClobberize.h:
(JSC::DFG::clobberize):
* Source/JavaScriptCore/dfg/DFGDoesGC.cpp:
(JSC::DFG::doesGC):
* Source/JavaScriptCore/dfg/DFGFixupPhase.cpp:
(JSC::DFG::FixupPhase::fixupNode):
* Source/JavaScriptCore/dfg/DFGNodeType.h:
* Source/JavaScriptCore/dfg/DFGOperations.cpp:
(JSC::DFG::JSC_DEFINE_JIT_OPERATION):
* Source/JavaScriptCore/dfg/DFGOperations.h:
* Source/JavaScriptCore/dfg/DFGPredictionPropagationPhase.cpp:
* Source/JavaScriptCore/dfg/DFGSafeToExecute.h:
(JSC::DFG::safeToExecute):
* Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp:
(JSC::DFG::SpeculativeJIT::compileStringSubstring):
* Source/JavaScriptCore/dfg/DFGSpeculativeJIT.h:
* Source/JavaScriptCore/dfg/DFGSpeculativeJIT32_64.cpp:
(JSC::DFG::SpeculativeJIT::compile):
* Source/JavaScriptCore/dfg/DFGSpeculativeJIT64.cpp:
(JSC::DFG::SpeculativeJIT::compile):
* Source/JavaScriptCore/dfg/DFGStrengthReductionPhase.cpp:
(JSC::DFG::StrengthReductionPhase::handleNode):
* Source/JavaScriptCore/ftl/FTLCapabilities.cpp:
(JSC::FTL::canCompile):
* Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp:
(JSC::FTL::DFG::LowerDFGToB3::compileNode):
(JSC::FTL::DFG::LowerDFGToB3::compileCompareStrictEq):
* Source/JavaScriptCore/runtime/Intrinsic.cpp:
(JSC::intrinsicName):
* Source/JavaScriptCore/runtime/Intrinsic.h:
* Source/JavaScriptCore/runtime/JSGlobalObject.cpp:
(JSC::JSGlobalObject::init):
* Source/JavaScriptCore/runtime/JSGlobalObject.h:
* Source/JavaScriptCore/runtime/JSGlobalObjectInlines.h:
(JSC::JSGlobalObject::stringProtoSubstringFunction const):
* Source/JavaScriptCore/runtime/JSString.cpp:
(JSC::JSString::equalSlowCase const):
* Source/JavaScriptCore/runtime/StringPrototype.cpp:
(JSC::StringPrototype::finishCreation):
(JSC::JSC_DEFINE_HOST_FUNCTION):
(JSC::stringSubstringImpl): Deleted.
* Source/JavaScriptCore/runtime/StringPrototype.h:
* Source/JavaScriptCore/runtime/StringPrototypeInlines.h:
(JSC::extractSubstringOffsets):
(JSC::stringSubstring):
* Source/WTF/wtf/text/StringCommon.h:
(WTF::equalCommon):
* Source/WTF/wtf/text/StringView.h:
(WTF::equal):

Canonical link: https://commits.webkit.org/255030@main
  • Loading branch information
Constellation committed Sep 30, 2022
1 parent 2b695e1 commit 3de7e6fba239b0a69f618faa82c7cd642c57726b
Show file tree
Hide file tree
Showing 41 changed files with 371 additions and 52 deletions.
@@ -0,0 +1,27 @@
function shouldBe(actual, expected) {
if (actual !== expected)
throw new Error('bad value: ' + actual);
}

String.prototype._startsWith = function (find) {
return this.substring(0,find.length) === find
}

function test1() {
return "/assets/omfg"._startsWith('/assets/');
}
noInline(test1);

function test2(string) {
return "/assets/omfg"._startsWith(string);
}
noInline(test2);

var count = 0;
for (var i = 0; i < 5e5; ++i) {
if (test1())
++count;
if (test2('/assets/'))
++count;
}
shouldBe(count, 1e6);
@@ -0,0 +1,27 @@
function shouldBe(actual, expected) {
if (actual !== expected)
throw new Error('bad value: ' + actual);
}

function _startsWith(str, find) {
return str.substring(0,find.length) === find
}

function test1() {
return _startsWith("/assets/omfg", '/assets/');
}
noInline(test1);

function test2(string) {
return _startsWith("/assets/omfg", string);
}
noInline(test2);

var count = 0;
for (var i = 0; i < 5e5; ++i) {
if (test1())
++count;
if (test2('/assets/'))
++count;
}
shouldBe(count, 1e6);
@@ -0,0 +1,23 @@
function shouldBe(actual, expected) {
if (actual !== expected)
throw new Error('bad value: ' + actual);
}

function test1() {
return "/assets/omfg".startsWith('/assets/');
}
noInline(test1);

function test2(string) {
return "/assets/omfg".startsWith(string);
}
noInline(test2);

var count = 0;
for (var i = 0; i < 1e5; ++i) {
if (test1())
++count;
if (test2('/assets/'))
++count;
}
shouldBe(count, 2e5);
@@ -0,0 +1,16 @@
function shouldBe(actual, expected) {
if (actual !== expected)
throw new Error('bad value: ' + actual);
}

function test1() {
return "/assets/omfg".substring(1) === 'assets/omfg';
}
noInline(test1);

var count = 0;
for (var i = 0; i < 1e6; ++i) {
if (test1())
++count;
}
shouldBe(count, 1e6);
@@ -0,0 +1,16 @@
function shouldBe(actual, expected) {
if (actual !== expected)
throw new Error('bad value: ' + actual);
}

function test1() {
return "/assets/omfg".substring(0) === '/assets/';
}
noInline(test1);

var count = 0;
for (var i = 0; i < 1e6; ++i) {
if (!test1())
++count;
}
shouldBe(count, 1e6);
@@ -0,0 +1,16 @@
function shouldBe(actual, expected) {
if (actual !== expected)
throw new Error('bad value: ' + actual);
}

function test1() {
return "/assets/omfg".substring(0,8) === '/assets/';
}
noInline(test1);

var count = 0;
for (var i = 0; i < 1e6; ++i) {
if (test1())
++count;
}
shouldBe(count, 1e6);
@@ -0,0 +1,16 @@
function shouldBe(actual, expected) {
if (actual !== expected)
throw new Error('bad value: ' + actual);
}

function test1() {
return "/assets/omfg".substring(0,'/assets/'.length) === '/assets/';
}
noInline(test1);

var count = 0;
for (var i = 0; i < 1e6; ++i) {
if (test1())
++count;
}
shouldBe(count, 1e6);
@@ -0,0 +1,16 @@
function shouldBe(actual, expected) {
if (actual !== expected)
throw new Error('bad value: ' + actual);
}

function test1(start, end) {
return "/assets/omfg".substring(start, end) === '/assets/';
}
noInline(test1);

var count = 0;
for (var i = 0; i < 1e6; ++i) {
if (test1(0, 8))
++count;
}
shouldBe(count, 1e6);
@@ -178,7 +178,7 @@ namespace JSC {
macro(stringIncludesInternal) \
macro(stringIndexOfInternal) \
macro(stringSplitFast) \
macro(stringSubstringInternal) \
macro(stringSubstring) \
macro(makeBoundFunction) \
macro(hasOwnLengthProperty) \
macro(handleProxyGetTrapResult) \
@@ -189,7 +189,7 @@ function getSubstitution(matched, str, position, captures, namedCaptures, replac

for (var start = 0; start = @stringIndexOfInternal.@call(replacement, "$", lastStart), start !== -1; lastStart = start) {
if (start - lastStart > 0)
result = result + @stringSubstringInternal.@call(replacement, lastStart, start);
result = result + @stringSubstring.@call(replacement, lastStart, start);
start++;
if (start >= replacementLength)
result = result + "$";
@@ -207,20 +207,20 @@ function getSubstitution(matched, str, position, captures, namedCaptures, replac
break;
case "`":
if (position > 0)
result = result + @stringSubstringInternal.@call(str, 0, position);
result = result + @stringSubstring.@call(str, 0, position);
start++;
break;
case "'":
if (tailPos < stringLength)
result = result + @stringSubstringInternal.@call(str, tailPos);
result = result + @stringSubstring.@call(str, tailPos);
start++;
break;
case "<":
if (namedCaptures !== @undefined) {
var groupNameStartIndex = start + 1;
var groupNameEndIndex = @stringIndexOfInternal.@call(replacement, ">", groupNameStartIndex);
if (groupNameEndIndex !== -1) {
var groupName = @stringSubstringInternal.@call(replacement, groupNameStartIndex, groupNameEndIndex);
var groupName = @stringSubstring.@call(replacement, groupNameStartIndex, groupNameEndIndex);
var capture = namedCaptures[groupName];
if (capture !== @undefined)
result = result + @toString(capture);
@@ -241,7 +241,7 @@ function getSubstitution(matched, str, position, captures, namedCaptures, replac

var n = chCode - 0x30;
if (n > m) {
result = result + @stringSubstringInternal.@call(replacement, originalStart, start);
result = result + @stringSubstring.@call(replacement, originalStart, start);
break;
}

@@ -257,7 +257,7 @@ function getSubstitution(matched, str, position, captures, namedCaptures, replac
}

if (n == 0) {
result = result + @stringSubstringInternal.@call(replacement, originalStart, start);
result = result + @stringSubstring.@call(replacement, originalStart, start);
break;
}

@@ -271,7 +271,7 @@ function getSubstitution(matched, str, position, captures, namedCaptures, replac
}
}

return result + @stringSubstringInternal.@call(replacement, lastStart);
return result + @stringSubstring.@call(replacement, lastStart);
}

@overriddenName="[Symbol.replace]"
@@ -368,15 +368,15 @@ function replace(strArg, replace)
}

if (position >= nextSourcePosition) {
accumulatedResult = accumulatedResult + @stringSubstringInternal.@call(str, nextSourcePosition, position) + replacement;
accumulatedResult = accumulatedResult + @stringSubstring.@call(str, nextSourcePosition, position) + replacement;
nextSourcePosition = position + matchLength;
}
}

if (nextSourcePosition >= stringLength)
return accumulatedResult;

return accumulatedResult + @stringSubstringInternal.@call(str, nextSourcePosition);
return accumulatedResult + @stringSubstring.@call(str, nextSourcePosition);
}

// 21.2.5.9 RegExp.prototype[@@search] (string)
@@ -564,7 +564,7 @@ function split(string, limit)
// iv. Else e != p,
else {
// 1. Let T be a String value equal to the substring of S consisting of the elements at indices p (inclusive) through q (exclusive).
var subStr = @stringSubstringInternal.@call(str, position, matchPosition);
var subStr = @stringSubstring.@call(str, position, matchPosition);
// 2. Perform ! CreateDataProperty(A, ! ToString(lengthA), T).
// 3. Let lengthA be lengthA + 1.
@arrayPush(result, subStr);
@@ -599,7 +599,7 @@ function split(string, limit)
}
}
// 20. Let T be a String value equal to the substring of S consisting of the elements at indices p (inclusive) through size (exclusive).
var remainingStr = @stringSubstringInternal.@call(str, position, size);
var remainingStr = @stringSubstring.@call(str, position, size);
// 21. Perform ! CreateDataProperty(A, ! ToString(lengthA), T).
@arrayPush(result, remainingStr);
// 22. Return A.
@@ -113,7 +113,7 @@ function repeatCharactersSlowPath(string, count)
operand += operand;
}
if (remainingCharacters)
result += @stringSubstringInternal.@call(string, 0, remainingCharacters);
result += @stringSubstring.@call(string, 0, remainingCharacters);
return result;
}

@@ -105,7 +105,7 @@ class JSGlobalObject;
v(stringIncludesInternal, nullptr) \
v(stringIndexOfInternal, nullptr) \
v(stringSplitFast, nullptr) \
v(stringSubstringInternal, nullptr) \
v(stringSubstring, nullptr) \
v(makeBoundFunction, nullptr) \
v(hasOwnLengthProperty, nullptr) \
v(handleProxyGetTrapResult, nullptr) \
@@ -1404,6 +1404,7 @@ bool AbstractInterpreter<AbstractStateType>::executeEffects(unsigned clobberLimi
break;
}

case StringSubstring:
case StringSlice: {
setTypeForNode(node, SpecString);
break;
@@ -306,7 +306,8 @@ class BackwardsPropagationPhase {
break;
}

case StringSlice: {
case StringSlice:
case StringSubstring: {
node->child1()->mergeFlags(NodeBytecodeUsesAsValue);
node->child2()->mergeFlags(NodeBytecodeUsesAsNumber | NodeBytecodeUsesAsOther | NodeBytecodeUsesAsInt | NodeBytecodeUsesAsArrayIndex);
if (node->child3())
@@ -3685,6 +3685,7 @@ bool ByteCodeParser::handleIntrinsicCall(Node* callee, Operand result, Intrinsic
return true;
}

case StringPrototypeSubstringIntrinsic:
case StringPrototypeSliceIntrinsic: {
if (argumentCountIncludingThis < 2)
return false;
@@ -3698,7 +3699,7 @@ bool ByteCodeParser::handleIntrinsicCall(Node* callee, Operand result, Intrinsic
Node* end = nullptr;
if (argumentCountIncludingThis > 2)
end = get(virtualRegisterForArgumentIncludingThis(2, registerOffset));
Node* resultNode = addToGraph(StringSlice, thisString, start, end);
Node* resultNode = addToGraph(intrinsic == StringPrototypeSubstringIntrinsic ? StringSubstring : StringSlice, thisString, start, end);
setResult(resultNode);
return true;
}
@@ -2002,6 +2002,7 @@ void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFu
return;

case StringSlice:
case StringSubstring:
def(PureValue(node));
return;

@@ -400,6 +400,7 @@ bool doesGC(Graph& graph, Node* node)
case StringReplaceRegExp:
case StringReplaceString:
case StringSlice:
case StringSubstring:
case StringValueOf:
case CreateRest:
case ToLowerCase:
@@ -2680,7 +2680,8 @@ class FixupPhase : public Phase {
break;
}

case StringSlice: {
case StringSlice:
case StringSubstring: {
fixEdge<StringUse>(node->child1());
fixEdge<Int32Use>(node->child2());
if (node->child3())
@@ -537,6 +537,7 @@ namespace JSC { namespace DFG {
\
macro(StringValueOf, NodeMustGenerate | NodeResultJS) \
macro(StringSlice, NodeResultJS) \
macro(StringSubstring, NodeResultJS) \
macro(ToLowerCase, NodeResultJS) \
/* Nodes for DOM JIT */\
macro(CallDOMGetter, NodeResultJS | NodeMustGenerate) \
@@ -2680,6 +2680,24 @@ JSC_DEFINE_JIT_OPERATION(operationStringSlice, JSCell*, (JSGlobalObject* globalO
return stringSlice(globalObject, vm, string, string->length(), start, end);
}

JSC_DEFINE_JIT_OPERATION(operationStringSubstring, JSString*, (JSGlobalObject* globalObject, JSString* string, int32_t start))
{
VM& vm = globalObject->vm();
CallFrame* callFrame = DECLARE_CALL_FRAME(vm);
JITOperationPrologueCallFrameTracer tracer(vm, callFrame);

return stringSubstring(globalObject, string, start, std::nullopt);
}

JSC_DEFINE_JIT_OPERATION(operationStringSubstringWithEnd, JSString*, (JSGlobalObject* globalObject, JSString* string, int32_t start, int32_t end))
{
VM& vm = globalObject->vm();
CallFrame* callFrame = DECLARE_CALL_FRAME(vm);
JITOperationPrologueCallFrameTracer tracer(vm, callFrame);

return stringSubstring(globalObject, string, start, end);
}

JSC_DEFINE_JIT_OPERATION(operationToLowerCase, JSString*, (JSGlobalObject* globalObject, JSString* string, uint32_t failingIndex))
{
VM& vm = globalObject->vm();
@@ -250,6 +250,8 @@ JSC_DECLARE_JIT_OPERATION(operationStringReplaceStringStringWithTable8, JSString
JSC_DECLARE_JIT_OPERATION(operationStringReplaceStringStringWithoutSubstitutionWithTable8, JSString*, (JSGlobalObject*, JSString*, JSString*, JSString*, const BoyerMooreHorspoolTable<uint8_t>*));
JSC_DECLARE_JIT_OPERATION(operationStringReplaceStringEmptyStringWithTable8, JSString*, (JSGlobalObject*, JSString*, JSString*, const BoyerMooreHorspoolTable<uint8_t>*));
JSC_DECLARE_JIT_OPERATION(operationStringReplaceStringGeneric, JSString*, (JSGlobalObject*, JSString*, JSString*, EncodedJSValue));
JSC_DECLARE_JIT_OPERATION(operationStringSubstring, JSString*, (JSGlobalObject*, JSString*, int32_t));
JSC_DECLARE_JIT_OPERATION(operationStringSubstringWithEnd, JSString*, (JSGlobalObject*, JSString*, int32_t, int32_t));
JSC_DECLARE_JIT_OPERATION(operationToLowerCase, JSString*, (JSGlobalObject*, JSString*, uint32_t));

JSC_DECLARE_JIT_OPERATION(operationInt32ToString, char*, (JSGlobalObject*, int32_t, int32_t));
@@ -1002,6 +1002,7 @@ class PredictionPropagationPhase : public Phase {

case StringValueOf:
case StringSlice:
case StringSubstring:
case ToLowerCase:
setPrediction(SpecString);
break;

0 comments on commit 3de7e6f

Please sign in to comment.