Skip to content

Commit 3130bd5

Browse files
committed
[JSC] Use the generic path in jsSpliceSubstringsWithSeparator(s) for short two-range results
https://bugs.webkit.org/show_bug.cgi?id=319023 Reviewed by Yusuke Suzuki. For a global RegExp replace with a single match (rangeCount == 2, separatorCount == 1), we create two substring StringImpls and concatenate them with jsString(). But when the result is short, jsString() flattens it via tryMakeString() anyway, so this costs three allocations and a double copy — strictly slower than the generic single-allocation path below. This hits the common thousands-separator idiom `String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",")` for 4-6 digit numbers. Take the special case only when jsString() would actually make a rope. The threshold is extracted into shouldMakeRope() in JSString.h and shared with the jsString() overloads. Baseline Patched string-replace-generic 19.7240+-0.6320 ^ 14.7327+-0.3753 ^ definitely 1.3388x faster string-replace-regexp-global-thousands-separator 153.5817+-1.6965 ^ 120.7386+-1.0667 ^ definitely 1.2720x faster string-replace-regexp-global-single-match 152.1385+-0.4575 ^ 102.3380+-0.7269 ^ definitely 1.4866x faster Tests: JSTests/microbenchmarks/string-replace-regexp-global-single-match.js JSTests/microbenchmarks/string-replace-regexp-global-thousands-separator.js * JSTests/microbenchmarks/string-replace-regexp-global-single-match.js: Added. (test): * JSTests/microbenchmarks/string-replace-regexp-global-thousands-separator.js: Added. (test): * Source/JavaScriptCore/runtime/JSString.h: (JSC::shouldMakeRope): * Source/JavaScriptCore/runtime/OperationsInlines.h: (JSC::jsString): * Source/JavaScriptCore/runtime/StringPrototypeInlines.h: (JSC::jsSpliceSubstringsWithSeparators): (JSC::jsSpliceSubstringsWithSeparator): Canonical link: https://commits.webkit.org/317282@main
1 parent 430a1af commit 3130bd5

6 files changed

Lines changed: 100 additions & 28 deletions

File tree

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
function test(str, re)
2+
{
3+
return str.replace(re, ",");
4+
}
5+
noInline(test);
6+
7+
// 4-6 digit numbers: the thousands-separator idiom produces exactly one match.
8+
const strs = [];
9+
for (let k = 0; k < 16; k++) {
10+
const digits = 4 + (k % 3);
11+
let s = String(1 + (k % 9));
12+
for (let d = 1; d < digits; d++)
13+
s += String((k + d * 7) % 10);
14+
strs.push(s);
15+
}
16+
17+
const re = /\B(?=(\d{3})+(?!\d))/g;
18+
19+
let result;
20+
for (let i = 0; i < 2000000; ++i)
21+
result = test(strs[i & 15], re);
22+
23+
if (test("1234", re) !== "1,234")
24+
throw new Error("bad result: " + test("1234", re));
25+
if (test("123456", re) !== "123,456")
26+
throw new Error("bad result: " + test("123456", re));
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
function test(str, re)
2+
{
3+
return str.replace(re, ",");
4+
}
5+
noInline(test);
6+
7+
// Mixed 4-8 digit numbers, as produced by the common numberWithCommas idiom.
8+
const strs = [];
9+
for (let k = 0; k < 16; k++)
10+
strs.push(String(Math.round(1000 * Math.pow(1.9, k))));
11+
12+
const re = /\B(?=(\d{3})+(?!\d))/g;
13+
14+
let result;
15+
for (let i = 0; i < 2000000; ++i)
16+
result = test(strs[i & 15], re);
17+
18+
if (test("1234567", re) !== "1,234,567")
19+
throw new Error("bad result: " + test("1234567", re));

Source/JavaScriptCore/runtime/JSString.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,13 @@ inline JSString* JSString::getIndex(JSGlobalObject* globalObject, unsigned i)
946946
return jsSingleCharacterString(vm, view[i]);
947947
}
948948

949+
// (1) Cost of making JSString : sizeof(JSString) (for new string) + sizeof(StringImpl header) + totalLength
950+
// (2) Cost of making JSRopeString: sizeof(JSRopeString) + newFiberCount * sizeof(JSString) (for fibers not already wrapped in a JSString)
951+
ALWAYS_INLINE bool shouldMakeRope(size_t totalLength, unsigned newFiberCount)
952+
{
953+
return StringImpl::headerSize<Latin1Character>() + totalLength >= sizeof(JSRopeString) + (newFiberCount - 1) * sizeof(JSString);
954+
}
955+
949956
inline JSString* jsString(VM& vm, const String& s)
950957
{
951958
int size = s.length();

Source/JavaScriptCore/runtime/OperationsInlines.h

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,10 @@ ALWAYS_INLINE JSString* jsString(JSGlobalObject* globalObject, const String& u1,
8989
return nullptr;
9090
}
9191

92-
// (1) Cost of making JSString : sizeof(JSString) (for new string) + sizeof(StringImpl header) + length1 + length2
93-
// (2) Cost of making JSRopeString: sizeof(JSString) (for u1) + sizeof(JSRopeString)
94-
// We do not account u1 cost in (2) since u1 may be shared StringImpl, and it may not introduce additional cost.
95-
// We conservatively consider the cost of u1. Currently, we are not considering about is8Bit() case because 16-bit
96-
// strings are relatively rare. But we can do that if we need to consider it.
97-
if (s2->isRope() || (StringImpl::headerSize<Latin1Character>() + length1 + length2) >= sizeof(JSRopeString))
92+
// We do not account the cost of copying u1's characters into a rope since u1 may be a shared
93+
// StringImpl, and it may not introduce additional cost. We conservatively consider the cost of
94+
// u1's JSString cell.
95+
if (s2->isRope() || shouldMakeRope(static_cast<size_t>(length1) + length2, /* newFiberCount */ 1))
9896
return JSRopeString::create(vm, jsString(vm, u1), s2);
9997

10098
ASSERT(!s2->isRope());
@@ -125,9 +123,7 @@ ALWAYS_INLINE JSString* jsString(JSGlobalObject* globalObject, JSString* s1, con
125123
return nullptr;
126124
}
127125

128-
// (1) Cost of making JSString : sizeof(JSString) (for new string) + sizeof(StringImpl header) + length1 + length2
129-
// (2) Cost of making JSRopeString: sizeof(JSString) (for u2) + sizeof(JSRopeString)
130-
if (s1->isRope() || (StringImpl::headerSize<Latin1Character>() + length1 + length2) >= sizeof(JSRopeString))
126+
if (s1->isRope() || shouldMakeRope(static_cast<size_t>(length1) + length2, /* newFiberCount */ 1))
131127
return JSRopeString::create(vm, s1, jsString(vm, u2));
132128

133129
ASSERT(!s1->isRope());
@@ -204,9 +200,7 @@ ALWAYS_INLINE JSString* jsString(JSGlobalObject* globalObject, const String& u1,
204200
return nullptr;
205201
}
206202

207-
// (1) Cost of making JSString : sizeof(JSString) (for new string) + sizeof(StringImpl header) + length1 + length2
208-
// (2) Cost of making JSRopeString: sizeof(JSString) (for u1) + sizeof(JSString) (for u2) + sizeof(JSRopeString)
209-
if ((StringImpl::headerSize<Latin1Character>() + length1 + length2) >= (sizeof(JSRopeString) + sizeof(JSString)))
203+
if (shouldMakeRope(static_cast<size_t>(length1) + length2, /* newFiberCount */ 2))
210204
return JSRopeString::create(vm, jsString(vm, u1), jsString(vm, u2));
211205

212206
String newString = tryMakeString(u1, u2);
@@ -244,9 +238,7 @@ ALWAYS_INLINE JSString* jsString(JSGlobalObject* globalObject, const String& u1,
244238
return nullptr;
245239
}
246240

247-
// (1) Cost of making JSString : sizeof(JSString) (for new string) + sizeof(StringImpl header) + length1 + length2 + length3
248-
// (2) Cost of making JSRopeString: sizeof(JSString) (for u1) + sizeof(JSString) (for u2) + sizeof(JSString) (for u3) + sizeof(JSRopeString)
249-
if ((StringImpl::headerSize<Latin1Character>() + length1 + length2 + length3) >= (sizeof(JSRopeString) + sizeof(JSString) * 2))
241+
if (shouldMakeRope(static_cast<size_t>(length1) + length2 + length3, /* newFiberCount */ 3))
250242
return JSRopeString::create(vm, jsString(vm, u1), jsString(vm, u2), jsString(vm, u3));
251243

252244
String newString = tryMakeString(u1, u2, u3);

Source/JavaScriptCore/runtime/StringPrototype.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -380,8 +380,10 @@ JSString* replaceUsingRegExpSearch(VM& vm, JSGlobalObject* globalObject, JSStrin
380380
break;
381381
}
382382

383-
if (global)
384-
RELEASE_AND_RETURN(scope, replaceAllWithStringUsingRegExpSearch(vm, globalObject, string, source, regExp, replacementString));
383+
if (global) {
384+
JSString* replacementVal = replaceValue.isString() ? asString(replaceValue) : nullptr;
385+
RELEASE_AND_RETURN(scope, replaceAllWithStringUsingRegExpSearch(vm, globalObject, string, source, regExp, replacementVal, replacementString));
386+
}
385387
RELEASE_AND_RETURN(scope, replaceOneWithStringUsingRegExpSearch(vm, globalObject, string, source, regExp, replacementString));
386388
}
387389

Source/JavaScriptCore/runtime/StringPrototypeInlines.h

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -171,9 +171,19 @@ ALWAYS_INLINE JSString* jsSpliceSubstringsWithSeparators(JSGlobalObject* globalO
171171
}
172172

173173
if (rangeCount == 2 && separatorCount == 1) {
174-
String leftPart(StringImpl::createSubstringSharingImpl(*source.impl(), substringRanges[0].begin(), substringRanges[0].distance()));
175-
String rightPart(StringImpl::createSubstringSharingImpl(*source.impl(), substringRanges[1].begin(), substringRanges[1].distance()));
176-
RELEASE_AND_RETURN(scope, jsString(globalObject, leftPart, separators[0], rightPart));
174+
// Only profitable when jsString() below would create a rope. For shorter results, jsString()
175+
// flattens via tryMakeString anyway, so the substrings and the extra copy are pure overhead
176+
// compared to the generic single-allocation path below.
177+
CheckedInt32 length = substringRanges[0].distance();
178+
length += substringRanges[1].distance();
179+
length += separators[0].length();
180+
if (!length.hasOverflowed()) [[likely]] {
181+
if (shouldMakeRope(length.value(), /* newFiberCount */ 3)) {
182+
String leftPart(StringImpl::createSubstringSharingImpl(*source.impl(), substringRanges[0].begin(), substringRanges[0].distance()));
183+
String rightPart(StringImpl::createSubstringSharingImpl(*source.impl(), substringRanges[1].begin(), substringRanges[1].distance()));
184+
RELEASE_AND_RETURN(scope, jsString(globalObject, leftPart, separators[0], rightPart));
185+
}
186+
}
177187
}
178188

179189
CheckedInt32 totalLength = 0;
@@ -244,7 +254,7 @@ ALWAYS_INLINE JSString* jsSpliceSubstringsWithSeparators(JSGlobalObject* globalO
244254
RELEASE_AND_RETURN(scope, jsString(vm, impl.releaseNonNull()));
245255
}
246256

247-
ALWAYS_INLINE JSString* jsSpliceSubstringsWithSeparator(JSGlobalObject* globalObject, JSString* sourceVal, const String& source, const Range<int32_t>* substringRanges, int rangeCount, const String& separator, int separatorCount)
257+
ALWAYS_INLINE JSString* jsSpliceSubstringsWithSeparator(JSGlobalObject* globalObject, JSString* sourceVal, const String& source, const Range<int32_t>* substringRanges, int rangeCount, JSString* separatorVal, const String& separator, int separatorCount)
248258
{
249259
VM& vm = getVM(globalObject);
250260
auto scope = DECLARE_THROW_SCOPE(vm);
@@ -260,9 +270,25 @@ ALWAYS_INLINE JSString* jsSpliceSubstringsWithSeparator(JSGlobalObject* globalOb
260270
}
261271

262272
if (rangeCount == 2 && separatorCount == 1) {
263-
String leftPart(StringImpl::createSubstringSharingImpl(*source.impl(), substringRanges[0].begin(), substringRanges[0].distance()));
264-
String rightPart(StringImpl::createSubstringSharingImpl(*source.impl(), substringRanges[1].begin(), substringRanges[1].distance()));
265-
RELEASE_AND_RETURN(scope, jsString(globalObject, leftPart, separator, rightPart));
273+
// Only profitable when jsString() below would create a rope. For shorter results, jsString()
274+
// flattens via tryMakeString anyway, so the substrings and the extra copy are pure overhead
275+
// compared to the generic single-allocation path below.
276+
CheckedInt32 length = substringRanges[0].distance();
277+
length += substringRanges[1].distance();
278+
length += separator.length();
279+
if (!length.hasOverflowed()) [[likely]] {
280+
if (separatorVal) {
281+
if (shouldMakeRope(length.value(), /* newFiberCount */ 2)) {
282+
JSString* leftPart = jsString(vm, StringImpl::createSubstringSharingImpl(*source.impl(), substringRanges[0].begin(), substringRanges[0].distance()));
283+
JSString* rightPart = jsString(vm, StringImpl::createSubstringSharingImpl(*source.impl(), substringRanges[1].begin(), substringRanges[1].distance()));
284+
RELEASE_AND_RETURN(scope, jsString(globalObject, leftPart, separatorVal, rightPart));
285+
}
286+
} else if (shouldMakeRope(length.value(), /* newFiberCount */ 3)) {
287+
String leftPart(StringImpl::createSubstringSharingImpl(*source.impl(), substringRanges[0].begin(), substringRanges[0].distance()));
288+
String rightPart(StringImpl::createSubstringSharingImpl(*source.impl(), substringRanges[1].begin(), substringRanges[1].distance()));
289+
RELEASE_AND_RETURN(scope, jsString(globalObject, leftPart, separator, rightPart));
290+
}
291+
}
266292
}
267293

268294
CheckedInt32 totalLength = 0;
@@ -1199,7 +1225,7 @@ static ALWAYS_INLINE JSString* tryTrimSpaces(VM& vm, JSGlobalObject* globalObjec
11991225
return nullptr;
12001226
}
12011227

1202-
ALWAYS_INLINE JSString* replaceAllWithStringUsingRegExpSearchNoBackreferences(VM& vm, JSGlobalObject* globalObject, JSString* string, const String& source, RegExp* regExp, const String& replacementString)
1228+
ALWAYS_INLINE JSString* replaceAllWithStringUsingRegExpSearchNoBackreferences(VM& vm, JSGlobalObject* globalObject, JSString* string, const String& source, RegExp* regExp, JSString* replacementVal, const String& replacementString)
12031229
{
12041230
auto scope = DECLARE_THROW_SCOPE(vm);
12051231

@@ -1245,7 +1271,7 @@ ALWAYS_INLINE JSString* replaceAllWithStringUsingRegExpSearchNoBackreferences(VM
12451271
if (!sourceRanges.tryConstructAndAppend(lastIndex, sourceLen)) [[unlikely]]
12461272
OUT_OF_MEMORY(globalObject, scope);
12471273
}
1248-
RELEASE_AND_RETURN(scope, jsSpliceSubstringsWithSeparator(globalObject, string, source, sourceRanges.span().data(), sourceRanges.size(), replacementString, replacementCount));
1274+
RELEASE_AND_RETURN(scope, jsSpliceSubstringsWithSeparator(globalObject, string, source, sourceRanges.span().data(), sourceRanges.size(), replacementVal, replacementString, replacementCount));
12491275
}
12501276

12511277
struct StringReplaceTemplatePart {
@@ -1406,13 +1432,13 @@ ALWAYS_INLINE void appendReplacementUsingTemplate(StringBuilder& result, std::sp
14061432
}
14071433
}
14081434

1409-
ALWAYS_INLINE JSString* replaceAllWithStringUsingRegExpSearch(VM& vm, JSGlobalObject* globalObject, JSString* string, const String& source, RegExp* regExp, const String& replacementString)
1435+
ALWAYS_INLINE JSString* replaceAllWithStringUsingRegExpSearch(VM& vm, JSGlobalObject* globalObject, JSString* string, const String& source, RegExp* regExp, JSString* replacementVal, const String& replacementString)
14101436
{
14111437
auto scope = DECLARE_THROW_SCOPE(vm);
14121438

14131439
size_t dollarPos = replacementString.find('$');
14141440
if (dollarPos == notFound)
1415-
RELEASE_AND_RETURN(scope, replaceAllWithStringUsingRegExpSearchNoBackreferences(vm, globalObject, string, source, regExp, replacementString));
1441+
RELEASE_AND_RETURN(scope, replaceAllWithStringUsingRegExpSearchNoBackreferences(vm, globalObject, string, source, regExp, replacementVal, replacementString));
14161442

14171443
StringReplaceTemplateParts templateParts;
14181444

0 commit comments

Comments
 (0)