Skip to content
Permalink
Browse files
[JSC] Optimize String.replace with String / String case more in DFG /…
… FTL

https://bugs.webkit.org/show_bug.cgi?id=244750

Reviewed by Saam Barati.

While DFG / FTL has optimization for String#replace(RegExp, String), we didn't have optimization for String#replace(String, String).
This patch adds that optimization since Speedometer2.1 includes String#replace(String, String).

1. We simply add StringReplace with 3 StringUse specialization in DFG. And we invoke very simple operation function for the fast processing.
   To achieve that, we need to have String.prototype.@@replace and Object.prototype.@@replace watchpoints to ensure that these properties
   are not defined.
2. We add String#replace(String, String) strength reduction. So if everything is constant, we will convert it into constant LazyJSString node.
3. To clean up further, we also introduce watchpoints for RegExp primordial properties. We would like to move to the new implementation leveraging
   this for String#replace(RegExp, String) case in the future too to avoid inserting tryGetById in Fixup phase.

Microbenchmark shows 2x improvement. On Intel machine, Speedometer2.1 gets 0.45% faster. And on AppleSilicon machine, 0.26% faster.
In particular Vanilla-ES2015-Babel-Webpack-TodoMVC gets 2% better, Vanilla-ES2015-TodoMVC gets 1.93% better, and VanillaJS-TodoMVC
gets 2.94% better.

                                      ToT                     Patched

    string-replace-string      707.7036+-2.0437     ^    349.5370+-1.3370        ^ definitely 2.0247x faster

* JSTests/microbenchmarks/string-replace-string.js: Added.
(test):
* Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h:
(JSC::DFG::AbstractInterpreter<AbstractStateType>::executeEffects):
* Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp:
(JSC::DFG::ByteCodeParser::handleIntrinsicCall):
* Source/JavaScriptCore/dfg/DFGClobberize.h:
(JSC::DFG::clobberize):
* Source/JavaScriptCore/dfg/DFGFixupPhase.cpp:
(JSC::DFG::FixupPhase::fixupNode):
* Source/JavaScriptCore/dfg/DFGGraph.h:
* Source/JavaScriptCore/dfg/DFGOperations.cpp:
(JSC::DFG::JSC_DEFINE_JIT_OPERATION):
* Source/JavaScriptCore/dfg/DFGOperations.h:
* Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp:
* Source/JavaScriptCore/dfg/DFGStrengthReductionPhase.cpp:
(JSC::DFG::StrengthReductionPhase::handleNode):
* Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp:
(JSC::FTL::DFG::LowerDFGToB3::compileCompareStrictEq):
* Source/JavaScriptCore/runtime/JSGlobalObject.cpp:
(JSC::setupAdaptiveWatchpoint):
(JSC::JSGlobalObject::init):
* Source/JavaScriptCore/runtime/JSGlobalObject.h:
(JSC::JSGlobalObject::stringSymbolReplaceWatchpointSet):
(JSC::JSGlobalObject::regExpPrimordialPropertiesWatchpointSet):

Canonical link: https://commits.webkit.org/254156@main
  • Loading branch information
Constellation committed Sep 5, 2022
1 parent 647e67b commit 965272b76dc3d55f84cb5702ab51ea62cc122e50
Show file tree
Hide file tree
Showing 18 changed files with 312 additions and 48 deletions.
@@ -0,0 +1,8 @@
function test(a, b, c)
{
return a.replace(b, c);
}
noInline(test);

for (var i = 0; i < 1e7; ++i)
test("Hello World", "World", "Hi");
@@ -0,0 +1,16 @@
function shouldBe(actual, expected) {
if (actual !== expected)
throw new Error('bad value: ' + actual);
}

function test(string) {
return string.replace("Hello", "World");
}
noInline(test);

for (var i = 0; i < 1e6; ++i)
shouldBe(test("Hello"), "World");

Object.prototype[Symbol.replace] = function () { return "Changed"; };
for (var i = 0; i < 1e6; ++i)
shouldBe(test("Hello"), "Changed");
@@ -0,0 +1,16 @@
function shouldBe(actual, expected) {
if (actual !== expected)
throw new Error('bad value: ' + actual);
}

function test(string) {
return string.replace(/Hello/, "World");
}
noInline(test);

for (var i = 0; i < 1e5; ++i)
shouldBe(test("Hello"), "World");

RegExp.prototype[Symbol.replace] = function () { return "Changed"; };
for (var i = 0; i < 1e5; ++i)
shouldBe(test("Hello"), "Changed");
@@ -0,0 +1,16 @@
function shouldBe(actual, expected) {
if (actual !== expected)
throw new Error('bad value: ' + actual);
}

function test(string) {
return string.replace("Hello", "World");
}
noInline(test);

for (var i = 0; i < 1e6; ++i)
shouldBe(test("Hello"), "World");

String.prototype[Symbol.replace] = function () { return "Changed"; };
for (var i = 0; i < 1e6; ++i)
shouldBe(test("Hello"), "Changed");
@@ -0,0 +1,12 @@
function shouldBe(actual, expected) {
if (actual !== expected)
throw new Error('bad value: ' + actual);
}

function test() {
return "Hello".replace("Hey", "World");
}
noInline(test);

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

function test() {
return "Hello".replace("Hello", "World");
}
noInline(test);

for (var i = 0; i < 1e6; ++i)
shouldBe(test(), `World`);
@@ -2655,7 +2655,7 @@ bool AbstractInterpreter<AbstractStateType>::executeEffects(unsigned clobberLimi
case StringReplace:
case StringReplaceRegExp:
if (node->child1().useKind() == StringUse
&& node->child2().useKind() == RegExpObjectUse
&& (node->child2().useKind() == RegExpObjectUse || node->child2().useKind() == StringUse)
&& node->child3().useKind() == StringUse) {
// This doesn't clobber the world. It just reads and writes regexp state.
} else
@@ -3051,36 +3051,7 @@ bool ByteCodeParser::handleIntrinsicCall(Node* callee, Operand result, Intrinsic
return false;

JSGlobalObject* globalObject = m_inlineStackTop->m_codeBlock->globalObject();
Structure* regExpStructure = globalObject->regExpStructure();
m_graph.registerStructure(regExpStructure);
ASSERT(regExpStructure->storedPrototype().isObject());
ASSERT(regExpStructure->storedPrototype().asCell()->classInfo() == RegExpPrototype::info());

FrozenValue* regExpPrototypeObjectValue = m_graph.freeze(regExpStructure->storedPrototype());
Structure* regExpPrototypeStructure = regExpPrototypeObjectValue->structure();

auto isRegExpPropertySame = [&] (JSValue primordialProperty, UniquedStringImpl* propertyUID) {
JSValue currentProperty;
if (!m_graph.getRegExpPrototypeProperty(regExpStructure->storedPrototypeObject(), regExpPrototypeStructure, propertyUID, currentProperty))
return false;

return currentProperty == primordialProperty;
};

// Check that searchRegExp.exec is still the primordial RegExp.prototype.exec
if (!isRegExpPropertySame(globalObject->regExpProtoExecFunction(), m_vm->propertyNames->exec.impl()))
return false;

// Check that searchRegExp.global is still the primordial RegExp.prototype.global
if (!isRegExpPropertySame(globalObject->regExpProtoGlobalGetter(), m_vm->propertyNames->global.impl()))
return false;

// Check that searchRegExp.unicode is still the primordial RegExp.prototype.unicode
if (!isRegExpPropertySame(globalObject->regExpProtoUnicodeGetter(), m_vm->propertyNames->unicode.impl()))
return false;

// Check that searchRegExp[Symbol.match] is still the primordial RegExp.prototype[Symbol.replace]
if (!isRegExpPropertySame(globalObject->regExpProtoSymbolReplaceFunction(), m_vm->propertyNames->replaceSymbol.impl()))
if (!globalObject->stringSymbolReplaceWatchpointSet().isStillValid() || !globalObject->regExpPrimordialPropertiesWatchpointSet().isStillValid())
return false;

insertChecks();
@@ -1824,6 +1824,10 @@ void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFu
write(RegExpState);
write(RegExpObject_lastIndex);
return;
} else if (node->child1().useKind() == StringUse
&& node->child2().useKind() == StringUse
&& node->child3().useKind() == StringUse) {
return;
}
clobberTop();
return;
@@ -1508,13 +1508,24 @@ class FixupPhase : public Phase {

case StringReplace:
case StringReplaceRegExp: {
if (op == StringReplace
&& node->child1()->shouldSpeculateString()
&& node->child2()->shouldSpeculateString()
&& node->child3()->shouldSpeculateString()
&& m_graph.isWatchingStringSymbolReplaceWatchpoint(node)) {
fixEdge<StringUse>(node->child1());
fixEdge<StringUse>(node->child2());
fixEdge<StringUse>(node->child3());
break;
}

if (node->child2()->shouldSpeculateString()) {
m_insertionSet.insertNode(
m_indexInBlock, SpecNone, Check, node->origin,
Edge(node->child2().node(), StringUse));
fixEdge<StringUse>(node->child2());
} else if (op == StringReplace) {
if (node->child2()->shouldSpeculateRegExpObject())
if (node->child2()->shouldSpeculateRegExpObject() && m_graph.isWatchingRegExpPrimordialPropertiesWatchpoint(node))
addStringReplacePrimordialChecks(node->child2().node());
else
m_insertionSet.insertNode(
@@ -840,6 +840,26 @@ class Graph final : public virtual Scannable {
return isWatchingGlobalObjectWatchpoint(globalObject, set);
}

bool isWatchingStringSymbolReplaceWatchpoint(Node* node)
{
if (m_plan.isUnlinked())
return false;

JSGlobalObject* globalObject = globalObjectFor(node->origin.semantic);
InlineWatchpointSet& set = globalObject->stringSymbolReplaceWatchpointSet();
return isWatchingGlobalObjectWatchpoint(globalObject, set);
}

bool isWatchingRegExpPrimordialPropertiesWatchpoint(Node* node)
{
if (m_plan.isUnlinked())
return false;

JSGlobalObject* globalObject = globalObjectFor(node->origin.semantic);
InlineWatchpointSet& set = globalObject->regExpPrimordialPropertiesWatchpointSet();
return isWatchingGlobalObjectWatchpoint(globalObject, set);
}

Profiler::Compilation* compilation() { return m_plan.compilation(); }

DesiredIdentifiers& identifiers() { return m_plan.identifiers(); }
@@ -2506,6 +2506,65 @@ JSC_DEFINE_JIT_OPERATION(operationStringValueOf, JSString*, (JSGlobalObject* glo
return nullptr;
}

JSC_DEFINE_JIT_OPERATION(operationStringReplaceStringString, JSString*, (JSGlobalObject* globalObject, JSString* stringCell, JSString* searchCell, JSString* replacementCell))
{
VM& vm = globalObject->vm();
CallFrame* callFrame = DECLARE_CALL_FRAME(vm);
JITOperationPrologueCallFrameTracer tracer(vm, callFrame);
auto scope = DECLARE_THROW_SCOPE(vm);

String string = stringCell->value(globalObject);
RETURN_IF_EXCEPTION(scope, nullptr);

String search = searchCell->value(globalObject);
RETURN_IF_EXCEPTION(scope, nullptr);

String replacement = replacementCell->value(globalObject);
RETURN_IF_EXCEPTION(scope, nullptr);

size_t matchStart = string.find(search);
if (matchStart == notFound)
return stringCell;

size_t searchLength = search.length();
size_t matchEnd = matchStart + searchLength;
auto result = tryMakeString(StringView(string).substring(0, matchStart), replacement, StringView(string).substring(matchEnd, string.length() - matchEnd));
if (!result) {
throwOutOfMemoryError(globalObject, scope);
return nullptr;
}

return jsString(vm, WTFMove(result));
}

JSC_DEFINE_JIT_OPERATION(operationStringReplaceStringEmptyString, JSString*, (JSGlobalObject* globalObject, JSString* stringCell, JSString* searchCell))
{
VM& vm = globalObject->vm();
CallFrame* callFrame = DECLARE_CALL_FRAME(vm);
JITOperationPrologueCallFrameTracer tracer(vm, callFrame);
auto scope = DECLARE_THROW_SCOPE(vm);

String string = stringCell->value(globalObject);
RETURN_IF_EXCEPTION(scope, nullptr);

String search = searchCell->value(globalObject);
RETURN_IF_EXCEPTION(scope, nullptr);

size_t matchStart = string.find(search);
if (matchStart == notFound)
return stringCell;

size_t searchLength = search.length();
size_t matchEnd = matchStart + searchLength;
auto result = tryMakeString(StringView(string).substring(0, matchStart), StringView(string).substring(matchEnd, string.length() - matchEnd));
if (!result) {
throwOutOfMemoryError(globalObject, scope);
return nullptr;
}

return jsString(vm, WTFMove(result));
}

JSC_DEFINE_JIT_OPERATION(operationStringSubstr, JSCell*, (JSGlobalObject* globalObject, JSCell* cell, int32_t from, int32_t span))
{
VM& vm = globalObject->vm();
@@ -242,6 +242,8 @@ JSC_DECLARE_JIT_OPERATION(operationSingleCharacterString, JSString*, (VM*, int32
JSC_DECLARE_JIT_OPERATION(operationStringSubstr, JSCell*, (JSGlobalObject*, JSCell*, int32_t, int32_t));
JSC_DECLARE_JIT_OPERATION(operationStringSlice, JSCell*, (JSGlobalObject*, JSCell*, int32_t, int32_t));
JSC_DECLARE_JIT_OPERATION(operationStringValueOf, JSString*, (JSGlobalObject*, EncodedJSValue));
JSC_DECLARE_JIT_OPERATION(operationStringReplaceStringString, JSString*, (JSGlobalObject*, JSString*, JSString*, JSString*));
JSC_DECLARE_JIT_OPERATION(operationStringReplaceStringEmptyString, JSString*, (JSGlobalObject*, JSString*, JSString*));
JSC_DECLARE_JIT_OPERATION(operationToLowerCase, JSString*, (JSGlobalObject*, JSString*, uint32_t));

JSC_DECLARE_JIT_OPERATION(operationInt32ToString, char*, (JSGlobalObject*, int32_t, int32_t));
@@ -13310,6 +13310,48 @@ void SpeculativeJIT::compileStringReplace(Node* node)
bool sample = false;
if (sample)
m_jit.incrementSuperSamplerCount();
auto scopeExit = WTF::makeScopeExit([&] {
if (sample)
m_jit.decrementSuperSamplerCount();
});

if (node->op() == StringReplace
&& node->child1().useKind() == StringUse
&& node->child2().useKind() == StringUse
&& node->child3().useKind() == StringUse) {
if (JSString* replace = node->child3()->dynamicCastConstant<JSString*>(); replace && !replace->length()) {
SpeculateCellOperand string(this, node->child1());
SpeculateCellOperand search(this, node->child2());
GPRReg stringGPR = string.gpr();
GPRReg searchGPR = search.gpr();
speculateString(node->child1(), stringGPR);
speculateString(node->child2(), searchGPR);

flushRegisters();
GPRFlushedCallResult result(this);
callOperation(operationStringReplaceStringEmptyString, result.gpr(), JITCompiler::LinkableConstant(m_jit, m_graph.globalObjectFor(node->origin.semantic)), stringGPR, searchGPR);
m_jit.exceptionCheck();
cellResult(result.gpr(), node);
return;
}

SpeculateCellOperand string(this, node->child1());
SpeculateCellOperand search(this, node->child2());
SpeculateCellOperand replace(this, node->child3());
GPRReg stringGPR = string.gpr();
GPRReg searchGPR = search.gpr();
GPRReg replaceGPR = replace.gpr();
speculateString(node->child1(), stringGPR);
speculateString(node->child2(), searchGPR);
speculateString(node->child3(), replaceGPR);

flushRegisters();
GPRFlushedCallResult result(this);
callOperation(operationStringReplaceStringString, result.gpr(), JITCompiler::LinkableConstant(m_jit, m_graph.globalObjectFor(node->origin.semantic)), stringGPR, searchGPR, replaceGPR);
m_jit.exceptionCheck();
cellResult(result.gpr(), node);
return;
}

if (node->child1().useKind() == StringUse
&& node->child2().useKind() == RegExpObjectUse
@@ -13328,8 +13370,6 @@ void SpeculativeJIT::compileStringReplace(Node* node)
callOperation(operationStringProtoFuncReplaceRegExpEmptyStr, result.gpr(), JITCompiler::LinkableConstant(m_jit, m_graph.globalObjectFor(node->origin.semantic)), stringGPR, regExpGPR);
m_jit.exceptionCheck();
cellResult(result.gpr(), node);
if (sample)
m_jit.decrementSuperSamplerCount();
return;
}
}
@@ -13349,8 +13389,6 @@ void SpeculativeJIT::compileStringReplace(Node* node)
callOperation(operationStringProtoFuncReplaceRegExpString, result.gpr(), JITCompiler::LinkableConstant(m_jit, m_graph.globalObjectFor(node->origin.semantic)), stringGPR, regExpGPR, replaceGPR);
m_jit.exceptionCheck();
cellResult(result.gpr(), node);
if (sample)
m_jit.decrementSuperSamplerCount();
return;
}

@@ -13371,8 +13409,6 @@ void SpeculativeJIT::compileStringReplace(Node* node)
callOperation(operationStringProtoFuncReplaceGeneric, result.gpr(), JITCompiler::LinkableConstant(m_jit, m_graph.globalObjectFor(node->origin.semantic)), stringRegs, searchRegs, replaceRegs);
m_jit.exceptionCheck();
cellResult(result.gpr(), node);
if (sample)
m_jit.decrementSuperSamplerCount();
}

void SpeculativeJIT::compileRegExpExecNonGlobalOrSticky(Node* node)

0 comments on commit 965272b

Please sign in to comment.