Skip to content
Permalink
Browse files
[JSC] Accelerate baseline String#replace performance
https://bugs.webkit.org/show_bug.cgi?id=245349

Reviewed by Ross Kirsling.

We found that C++ runtime String#replace implementation is too generic and doing various inefficient things.
This patch unifies the implementation with DFG operations for the fast path so that we do very simple and fast operations
for String#replace(String, String) case even in non DFG. It shows consistent improvement in VanillaXXX in Speedometer2.1 on Intel machines.

With --useDFGJIT=0, we see 35% improvement.

    string-replace-benchmark                      219.2726+-0.5822     ^    162.1922+-0.5799        ^ definitely 1.3519x faster

* 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/DFGDoesGC.cpp:
(JSC::DFG::doesGC):
* Source/JavaScriptCore/dfg/DFGFixupPhase.cpp:
(JSC::DFG::FixupPhase::fixupNode):
* Source/JavaScriptCore/dfg/DFGNode.h:
(JSC::DFG::Node::hasHeapPrediction):
* Source/JavaScriptCore/dfg/DFGNodeType.h:
* Source/JavaScriptCore/dfg/DFGOperations.cpp:
(JSC::DFG::JSC_DEFINE_JIT_OPERATION):
(JSC::DFG::stringReplaceStringString): Deleted.
* Source/JavaScriptCore/dfg/DFGOperations.h:
* Source/JavaScriptCore/dfg/DFGPredictionPropagationPhase.cpp:
* Source/JavaScriptCore/dfg/DFGSafeToExecute.h:
(JSC::DFG::safeToExecute):
* Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp:
* 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/interpreter/CachedCall.h:
(JSC::CachedCall::CachedCall):
(JSC::CachedCall::call):
* Source/JavaScriptCore/interpreter/CallFrameClosure.h:
* Source/JavaScriptCore/interpreter/Interpreter.cpp:
(JSC::Interpreter::prepareForRepeatCall):
* Source/JavaScriptCore/interpreter/Interpreter.h:
* Source/JavaScriptCore/interpreter/InterpreterInlines.h:
(JSC::Interpreter::execute):
* Source/JavaScriptCore/runtime/Intrinsic.cpp:
(JSC::intrinsicName):
* Source/JavaScriptCore/runtime/Intrinsic.h:
* Source/JavaScriptCore/runtime/StringPrototype.cpp:
(JSC::StringPrototype::finishCreation):
(JSC::jsSpliceSubstrings):
(JSC::removeUsingRegExpSearch):
(JSC::replaceUsingRegExpSearch):
(JSC::JSC_DEFINE_JIT_OPERATION):
(JSC::replace):
(JSC::JSC_DEFINE_HOST_FUNCTION):
(JSC::StringRange::StringRange): Deleted.
(JSC::jsSpliceSubstringsWithSeparators): Deleted.
(JSC::replaceUsingStringSearch): Deleted.
* Source/JavaScriptCore/runtime/StringPrototypeInlines.h:
(JSC::jsSpliceSubstringsWithSeparators):
(JSC::stringReplaceStringString):
(JSC::replaceUsingStringSearch):

Canonical link: https://commits.webkit.org/254659@main
  • Loading branch information
Constellation committed Sep 20, 2022
1 parent b2518c2 commit 195c7f139a84e0af36a5aaea6b61d7470b95a3b5
Show file tree
Hide file tree
Showing 27 changed files with 554 additions and 432 deletions.
@@ -2651,14 +2651,22 @@ bool AbstractInterpreter<AbstractStateType>::executeEffects(unsigned clobberLimi
case StringReplace:
case StringReplaceRegExp:
if (node->child1().useKind() == StringUse
&& (node->child2().useKind() == RegExpObjectUse || node->child2().useKind() == StringUse)
&& node->child2().useKind() == RegExpObjectUse
&& node->child3().useKind() == StringUse) {
// This doesn't clobber the world. It just reads and writes regexp state.
} else
clobberWorld();
setForNode(node, m_vm.stringStructure.get());
break;

case StringReplaceString:
if (node->child3().useKind() != StringUse) {
// child3 could be non String (e.g. function). In this case, any side effect can happen.
clobberWorld();
}
setForNode(node, m_vm.stringStructure.get());
break;

case Jump:
break;

@@ -3055,6 +3055,16 @@ bool ByteCodeParser::handleIntrinsicCall(Node* callee, Operand result, Intrinsic
setResult(resultNode);
return true;
}

case StringPrototypeReplaceStringIntrinsic: {
if (argumentCountIncludingThis < 3)
return false;

insertChecks();
Node* resultNode = addToGraph(StringReplaceString, OpInfo(0), OpInfo(prediction), get(virtualRegisterForArgumentIncludingThis(0, registerOffset)), get(virtualRegisterForArgumentIncludingThis(1, registerOffset)), get(virtualRegisterForArgumentIncludingThis(2, registerOffset)));
setResult(resultNode);
return true;
}

case RoundIntrinsic:
case FloorIntrinsic:
@@ -1813,14 +1813,16 @@ 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;

case StringReplaceString:
if (node->child3().useKind() == StringUse)
return;
clobberTop();
return;

case StringCharAt:
if (node->arrayMode().isOutOfBounds()) {
clobberTop();
@@ -398,6 +398,7 @@ bool doesGC(Graph& graph, Node* node)
case StrCat:
case StringReplace:
case StringReplaceRegExp:
case StringReplaceString:
case StringSlice:
case StringValueOf:
case CreateRest:
@@ -1511,11 +1511,12 @@ class FixupPhase : public Phase {
if (op == StringReplace
&& node->child1()->shouldSpeculateString()
&& node->child2()->shouldSpeculateString()
&& node->child3()->shouldSpeculateString()
&& m_graph.isWatchingStringSymbolReplaceWatchpoint(node)) {
node->setOp(StringReplaceString);
fixEdge<StringUse>(node->child1());
fixEdge<StringUse>(node->child2());
fixEdge<StringUse>(node->child3());
if (node->child3()->shouldSpeculateString())
fixEdge<StringUse>(node->child3());
break;
}

@@ -1543,6 +1544,14 @@ class FixupPhase : public Phase {
}
break;
}

case StringReplaceString: {
fixEdge<StringUse>(node->child1());
fixEdge<StringUse>(node->child2());
if (node->child3()->shouldSpeculateString())
fixEdge<StringUse>(node->child3());
break;
}

case Branch: {
if (node->child1()->shouldSpeculateBoolean()) {
@@ -1758,6 +1758,7 @@ struct Node {
case GetGlobalLexicalVariable:
case StringReplace:
case StringReplaceRegExp:
case StringReplaceString:
case ToNumber:
case ToNumeric:
case ToObject:
@@ -329,6 +329,7 @@ namespace JSC { namespace DFG {
macro(RegExpMatchFastGlobal, NodeResultJS) \
macro(StringReplace, NodeResultJS | NodeMustGenerate) \
macro(StringReplaceRegExp, NodeResultJS | NodeMustGenerate) \
macro(StringReplaceString, NodeResultJS | NodeMustGenerate) \
\
/* Optimizations for string access */ \
macro(StringCharCodeAt, NodeResultInt32) \
@@ -44,6 +44,7 @@
#include "FrameTracers.h"
#include "HasOwnPropertyCache.h"
#include "Interpreter.h"
#include "JITCodeInlines.h"
#include "JITWorklist.h"
#include "JSArrayInlines.h"
#include "JSArrayIterator.h"
@@ -2506,49 +2507,6 @@ JSC_DEFINE_JIT_OPERATION(operationStringValueOf, JSString*, (JSGlobalObject* glo
return nullptr;
}

enum class StringReplaceSubstitutions : bool { Yes, No };
enum class StringReplaceUseTable : bool { Yes, No };
template<StringReplaceSubstitutions substitutions, StringReplaceUseTable useTable, typename TableType>
static ALWAYS_INLINE JSString* stringReplaceStringString(JSGlobalObject* globalObject, JSString* stringCell, String string, String search, String replacement, const TableType* table)
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);

size_t matchStart;
if constexpr (useTable == StringReplaceUseTable::Yes)
matchStart = table->find(string, search);
else {
UNUSED_PARAM(table);
matchStart = string.find(search);
}
if (matchStart == notFound)
return stringCell;

size_t searchLength = search.length();
size_t matchEnd = matchStart + searchLength;
if constexpr (substitutions == StringReplaceSubstitutions::Yes) {
size_t dollarSignPosition = replacement.find('$');
if (dollarSignPosition != WTF::notFound) {
StringBuilder builder(StringBuilder::OverflowHandler::RecordOverflow);
int ovector[2] = { static_cast<int>(matchStart), static_cast<int>(matchEnd) };
substituteBackreferencesSlow(builder, replacement, string, ovector, nullptr, dollarSignPosition);
if (UNLIKELY(builder.hasOverflowed())) {
throwOutOfMemoryError(globalObject, scope);
return nullptr;
}
replacement = builder.toString();
}
}

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

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

JSC_DEFINE_JIT_OPERATION(operationStringReplaceStringString, JSString*, (JSGlobalObject* globalObject, JSString* stringCell, JSString* searchCell, JSString* replacementCell))
{
VM& vm = globalObject->vm();
@@ -2683,6 +2641,24 @@ JSC_DEFINE_JIT_OPERATION(operationStringReplaceStringEmptyStringWithTable8, JSSt
return jsString(vm, WTFMove(result));
}

JSC_DEFINE_JIT_OPERATION(operationStringReplaceStringGeneric, JSString*, (JSGlobalObject* globalObject, JSString* stringCell, JSString* searchCell, EncodedJSValue encodedReplaceValue))
{
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);

JSValue replaceValue = JSValue::decode(encodedReplaceValue);

RELEASE_AND_RETURN(scope, replaceUsingStringSearch(vm, globalObject, stringCell, WTFMove(string), WTFMove(search), replaceValue, StringReplaceMode::Single));
}

JSC_DEFINE_JIT_OPERATION(operationStringSubstr, JSCell*, (JSGlobalObject* globalObject, JSCell* cell, int32_t from, int32_t span))
{
VM& vm = globalObject->vm();
@@ -249,6 +249,7 @@ JSC_DECLARE_JIT_OPERATION(operationStringReplaceStringEmptyString, JSString*, (J
JSC_DECLARE_JIT_OPERATION(operationStringReplaceStringStringWithTable8, JSString*, (JSGlobalObject*, JSString*, JSString*, JSString*, const BoyerMooreHorspoolTable<uint8_t>*));
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(operationToLowerCase, JSString*, (JSGlobalObject*, JSString*, uint32_t));

JSC_DECLARE_JIT_OPERATION(operationInt32ToString, char*, (JSGlobalObject*, int32_t, int32_t));
@@ -873,6 +873,7 @@ class PredictionPropagationPhase : public Phase {
case RegExpMatchFastGlobal:
case StringReplace:
case StringReplaceRegExp:
case StringReplaceString:
case GetById:
case GetByIdFlush:
case GetByIdWithThis:
@@ -695,13 +695,16 @@ bool safeToExecute(AbstractStateType& state, Graph& graph, Node* node, bool igno
case DataViewSet:
case SetAdd:
case MapSet:
case StringReplaceRegExp:
case StringReplace:
case StringReplaceRegExp:
case ArithRandom:
case ArithIMul:
case TryGetById:
return false;

case StringReplaceString:
return node->child3().useKind() == StringUse;

case Inc:
case Dec:
return node->child1().useKind() != UntypedUse;
@@ -13299,10 +13299,65 @@ void SpeculativeJIT::compileStringReplace(Node* node)
m_jit.decrementSuperSamplerCount();
});

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

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

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

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

// If we fixed up the edge of child2, we inserted a Check(@child2, String).
OperandSpeculationMode child2SpeculationMode = AutomaticOperandSpeculation;
if (node->child2().useKind() == StringUse)
child2SpeculationMode = ManualOperandSpeculation;

JSValueOperand string(this, node->child1());
JSValueOperand search(this, node->child2(), child2SpeculationMode);
JSValueOperand replace(this, node->child3());
JSValueRegs stringRegs = string.jsValueRegs();
JSValueRegs searchRegs = search.jsValueRegs();
JSValueRegs replaceRegs = replace.jsValueRegs();

flushRegisters();
GPRFlushedCallResult result(this);
callOperation(operationStringProtoFuncReplaceGeneric, result.gpr(), JITCompiler::LinkableConstant(m_jit, m_graph.globalObjectFor(node->origin.semantic)), stringRegs, searchRegs, replaceRegs);
m_jit.exceptionCheck();
cellResult(result.gpr(), node);
}

void SpeculativeJIT::compileStringReplaceString(Node* node)
{
if (node->child3().useKind() == StringUse) {
const BoyerMooreHorspoolTable<uint8_t>* tablePointer = nullptr;
String searchString = node->child2()->tryGetString(m_graph);
if (!!searchString)
@@ -13373,60 +13428,20 @@ void SpeculativeJIT::compileStringReplace(Node* node)
return;
}

if (node->child1().useKind() == StringUse
&& node->child2().useKind() == RegExpObjectUse
&& node->child3().useKind() == StringUse) {
if (JSString* replace = node->child3()->dynamicCastConstant<JSString*>()) {
if (!replace->length()) {
SpeculateCellOperand string(this, node->child1());
SpeculateCellOperand regExp(this, node->child2());
GPRReg stringGPR = string.gpr();
GPRReg regExpGPR = regExp.gpr();
speculateString(node->child1(), stringGPR);
speculateRegExpObject(node->child2(), regExpGPR);

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

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

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

// If we fixed up the edge of child2, we inserted a Check(@child2, String).
OperandSpeculationMode child2SpeculationMode = AutomaticOperandSpeculation;
if (node->child2().useKind() == StringUse)
child2SpeculationMode = ManualOperandSpeculation;

JSValueOperand string(this, node->child1());
JSValueOperand search(this, node->child2(), child2SpeculationMode);
// Otherwise, maybe function. Let's call slow path.
SpeculateCellOperand string(this, node->child1());
SpeculateCellOperand search(this, node->child2());
JSValueOperand replace(this, node->child3());
JSValueRegs stringRegs = string.jsValueRegs();
JSValueRegs searchRegs = search.jsValueRegs();

GPRReg stringGPR = string.gpr();
GPRReg searchGPR = search.gpr();
JSValueRegs replaceRegs = replace.jsValueRegs();
speculateString(node->child1(), stringGPR);
speculateString(node->child2(), searchGPR);

flushRegisters();
GPRFlushedCallResult result(this);
callOperation(operationStringProtoFuncReplaceGeneric, result.gpr(), JITCompiler::LinkableConstant(m_jit, m_graph.globalObjectFor(node->origin.semantic)), stringRegs, searchRegs, replaceRegs);
callOperation(operationStringReplaceStringGeneric, result.gpr(), JITCompiler::LinkableConstant(m_jit, m_graph.globalObjectFor(node->origin.semantic)), stringGPR, searchGPR, replaceRegs);
m_jit.exceptionCheck();
cellResult(result.gpr(), node);
}
@@ -1482,6 +1482,7 @@ class SpeculativeJIT {
void compileRegExpTest(Node*);
void compileRegExpTestInline(Node*);
void compileStringReplace(Node*);
void compileStringReplaceString(Node*);
void compileIsObject(Node*);
void compileTypeOfIsObject(Node*);
void compileIsCallable(Node*, S_JITOperation_GC);
@@ -2717,6 +2717,11 @@ void SpeculativeJIT::compile(Node* node)
break;
}

case StringReplaceString: {
compileStringReplaceString(node);
break;
}

case GetRegExpObjectLastIndex: {
compileGetRegExpObjectLastIndex(node);
break;

0 comments on commit 195c7f1

Please sign in to comment.