[builtins] Port String.prototype.{padStart, padEnd} to CSA

- Extract core StringPrototypeRepeat code into a TFS builtin (StringRepeat)
  - Assumes arguments are a string and smi (no range checks)
- Add StringPrototypePadStart and StringPrototypePadEnd TFJ builtins
  - Added StringPadAssembler to ensure common behavior
- Removed functionality from string.js

A quick benchmark shows significant performance gains for unoptimized
code (2.1x to 2.46x) and optimized code (1.03x - 1.56x).

https: //github.com/peterwmwong/v8-perf/blob/master/string-pad/README.md
Bug: v8:5049
Change-Id: I6e4fe99fb62a3edb3d6906fd4f78b3576b5b0d13
Reviewed-on: https://chromium-review.googlesource.com/720067
Commit-Queue: Jakob Gruber <jgruber@chromium.org>
Reviewed-by: Jakob Gruber <jgruber@chromium.org>
Cr-Commit-Position: refs/heads/master@{#48595}
This commit is contained in:
peterwmwong 2017-10-16 08:23:28 -05:00 committed by Commit Bot
parent 61c7af2e97
commit 33b23529f4
8 changed files with 226 additions and 113 deletions

View File

@ -1992,6 +1992,10 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
SimpleInstallFunction(prototype, "normalize",
Builtins::kStringPrototypeNormalize, 0, false);
#endif // V8_INTL_SUPPORT
SimpleInstallFunction(prototype, "padEnd", Builtins::kStringPrototypePadEnd,
1, false);
SimpleInstallFunction(prototype, "padStart",
Builtins::kStringPrototypePadStart, 1, false);
SimpleInstallFunction(prototype, "repeat", Builtins::kStringPrototypeRepeat,
1, true);
SimpleInstallFunction(prototype, "replace",

View File

@ -99,6 +99,7 @@ namespace internal {
TFS(StringIndexOf, kReceiver, kSearchString, kPosition) \
TFC(StringLessThan, Compare, 1) \
TFC(StringLessThanOrEqual, Compare, 1) \
TFS(StringRepeat, kString, kCount) \
\
/* OrderedHashTable helpers */ \
TFS(OrderedHashTableHealIndex, kTable, kIndex) \
@ -939,6 +940,11 @@ namespace internal {
TFJ(StringPrototypeMatch, 1, kRegexp) \
/* ES6 #sec-string.prototype.localecompare */ \
CPP(StringPrototypeLocaleCompare) \
/* ES6 #sec-string.prototype.padEnd */ \
TFJ(StringPrototypePadEnd, SharedFunctionInfo::kDontAdaptArgumentsSentinel) \
/* ES6 #sec-string.prototype.padStart */ \
TFJ(StringPrototypePadStart, \
SharedFunctionInfo::kDontAdaptArgumentsSentinel) \
/* ES6 #sec-string.prototype.repeat */ \
TFJ(StringPrototypeRepeat, 1, kCount) \
/* ES6 #sec-string.prototype.replace */ \

View File

@ -1200,20 +1200,19 @@ TF_BUILTIN(StringPrototypeRepeat, StringBuiltinsAssembler) {
// Verifies a valid count and takes a fast path when the result will be an
// empty string.
{
Label next(this), if_count_isheapnumber(this, Label::kDeferred);
Label if_count_isheapnumber(this, Label::kDeferred);
GotoIfNot(TaggedIsSmi(var_count.value()), &if_count_isheapnumber);
// If count is a SMI, throw a RangeError if less than 0 or greater than
// the maximum string length.
{
GotoIf(SmiLessThan(var_count.value(), SmiConstant(0)), &invalid_count);
GotoIf(SmiEqual(var_count.value(), SmiConstant(0)), &return_emptystring);
GotoIf(is_stringempty, &return_emptystring);
GotoIf(SmiGreaterThan(var_count.value(), SmiConstant(String::kMaxLength)),
&invalid_string_length);
Goto(&next);
}
GotoIf(SmiLessThan(var_count.value(), SmiConstant(0)), &invalid_count);
GotoIf(SmiEqual(var_count.value(), SmiConstant(0)), &return_emptystring);
GotoIf(is_stringempty, &return_emptystring);
GotoIf(SmiGreaterThan(var_count.value(), SmiConstant(String::kMaxLength)),
&invalid_string_length);
Return(CallBuiltin(Builtins::kStringRepeat, context, string,
var_count.value()));
// If count is a Heap Number...
// 1) If count is Infinity, throw a RangeError exception
@ -1229,49 +1228,6 @@ TF_BUILTIN(StringPrototypeRepeat, StringBuiltinsAssembler) {
&invalid_count);
Branch(is_stringempty, &return_emptystring, &invalid_string_length);
}
BIND(&next);
}
// The receiver is repeated with the following algorithm:
// let n = count;
// let power_of_two_repeats = receiver;
// let result = "";
// while (true) {
// if (n & 1) result += s;
// n >>= 1;
// if (n === 0) return result;
// power_of_two_repeats += power_of_two_repeats;
// }
{
VARIABLE(var_result, MachineRepresentation::kTagged, EmptyStringConstant());
VARIABLE(var_temp, MachineRepresentation::kTagged, string);
Callable stringadd_callable =
CodeFactory::StringAdd(isolate(), STRING_ADD_CHECK_NONE, NOT_TENURED);
Label loop(this, {&var_count, &var_result, &var_temp}), return_result(this);
Goto(&loop);
BIND(&loop);
{
{
Label next(this);
GotoIfNot(SmiToWord32(SmiAnd(var_count.value(), SmiConstant(1))),
&next);
var_result.Bind(CallStub(stringadd_callable, context,
var_result.value(), var_temp.value()));
Goto(&next);
BIND(&next);
}
var_count.Bind(SmiShr(var_count.value(), 1));
GotoIf(SmiEqual(var_count.value(), SmiConstant(0)), &return_result);
var_temp.Bind(CallStub(stringadd_callable, context, var_temp.value(),
var_temp.value()));
Goto(&loop);
}
BIND(&return_result);
Return(var_result.value());
}
BIND(&return_emptystring);
@ -1291,6 +1247,58 @@ TF_BUILTIN(StringPrototypeRepeat, StringBuiltinsAssembler) {
}
}
// Helper with less checks
TF_BUILTIN(StringRepeat, StringBuiltinsAssembler) {
Node* const context = Parameter(Descriptor::kContext);
Node* const string = Parameter(Descriptor::kString);
Node* const count = Parameter(Descriptor::kCount);
CSA_ASSERT(this, IsString(string));
CSA_ASSERT(this, Word32BinaryNot(IsEmptyString(string)));
CSA_ASSERT(this, TaggedIsPositiveSmi(count));
CSA_ASSERT(this, SmiLessThanOrEqual(count, SmiConstant(String::kMaxLength)));
// The string is repeated with the following algorithm:
// let n = count;
// let power_of_two_repeats = string;
// let result = "";
// while (true) {
// if (n & 1) result += s;
// n >>= 1;
// if (n === 0) return result;
// power_of_two_repeats += power_of_two_repeats;
// }
VARIABLE(var_result, MachineRepresentation::kTagged, EmptyStringConstant());
VARIABLE(var_temp, MachineRepresentation::kTagged, string);
VARIABLE(var_count, MachineRepresentation::kTagged, count);
Callable stringadd_callable =
CodeFactory::StringAdd(isolate(), STRING_ADD_CHECK_NONE, NOT_TENURED);
Label loop(this, {&var_count, &var_result, &var_temp}), return_result(this);
Goto(&loop);
BIND(&loop);
{
{
Label next(this);
GotoIfNot(SmiToWord32(SmiAnd(var_count.value(), SmiConstant(1))), &next);
var_result.Bind(CallStub(stringadd_callable, context, var_result.value(),
var_temp.value()));
Goto(&next);
BIND(&next);
}
var_count.Bind(SmiShr(var_count.value(), 1));
GotoIf(SmiEqual(var_count.value(), SmiConstant(0)), &return_result);
var_temp.Bind(CallStub(stringadd_callable, context, var_temp.value(),
var_temp.value()));
Goto(&loop);
}
BIND(&return_result);
Return(var_result.value());
}
// ES6 #sec-string.prototype.replace
TF_BUILTIN(StringPrototypeReplace, StringBuiltinsAssembler) {
Label out(this);
@ -1531,6 +1539,130 @@ TF_BUILTIN(StringPrototypeMatch, StringMatchSearchAssembler) {
Generate(kMatch, "String.prototype.match", receiver, maybe_regexp, context);
}
class StringPadAssembler : public StringBuiltinsAssembler {
public:
explicit StringPadAssembler(compiler::CodeAssemblerState* state)
: StringBuiltinsAssembler(state) {}
protected:
enum Variant { kStart, kEnd };
void Generate(Variant variant, const char* method_name) {
Node* const context = Parameter(BuiltinDescriptor::kContext);
Node* argc =
ChangeInt32ToIntPtr(Parameter(BuiltinDescriptor::kArgumentsCount));
CodeStubArguments arguments(this, argc);
Node* const receiver = arguments.GetReceiver();
Node* const receiver_string = ToThisString(context, receiver, method_name);
Node* const string_length = LoadStringLength(receiver_string);
VARIABLE(var_fill_string, MachineRepresentation::kTagged,
StringConstant(" "));
VARIABLE(var_fill_length, MachineRepresentation::kTagged, SmiConstant(1));
Label argc_2(this), dont_pad(this), invalid_string_length(this), pad(this);
// If no max_length was provided, return the string.
GotoIf(IntPtrEqual(argc, IntPtrConstant(0)), &dont_pad);
Node* const max_length = ToLength_Inline(context, arguments.AtIndex(0));
CSA_ASSERT(this, IsNumberNormalized(max_length));
// Throw if max_length is not a smi or greater than the max string length.
GotoIfNot(Word32And(TaggedIsSmi(max_length),
SmiLessThanOrEqual(max_length,
SmiConstant(String::kMaxLength))),
&invalid_string_length);
// If the max_length is less than length of the string, return the string.
CSA_ASSERT(this, TaggedIsPositiveSmi(max_length));
GotoIf(SmiLessThanOrEqual(max_length, string_length), &dont_pad);
Branch(IntPtrEqual(argc, IntPtrConstant(1)), &pad, &argc_2);
BIND(&argc_2);
{
Node* const fill = arguments.AtIndex(1);
GotoIf(IsUndefined(fill), &pad);
var_fill_string.Bind(ToString_Inline(context, fill));
var_fill_length.Bind(LoadStringLength(var_fill_string.value()));
Branch(SmiGreaterThan(var_fill_length.value(), SmiConstant(0)), &pad,
&dont_pad);
}
BIND(&pad);
{
CSA_ASSERT(this, SmiGreaterThan(var_fill_length.value(), SmiConstant(0)));
CSA_ASSERT(this, SmiGreaterThan(max_length, string_length));
Callable stringadd_callable =
CodeFactory::StringAdd(isolate(), STRING_ADD_CHECK_NONE, NOT_TENURED);
Node* const pad_length = SmiSub(max_length, string_length);
VARIABLE(var_pad, MachineRepresentation::kTagged);
Label single_char_fill(this), multi_char_fill(this), return_result(this);
Branch(SmiEqual(var_fill_length.value(), SmiConstant(1)),
&single_char_fill, &multi_char_fill);
// Fast path for a single character fill. No need to calculate number of
// repetitions or remainder.
BIND(&single_char_fill);
{
var_pad.Bind(CallBuiltin(Builtins::kStringRepeat, context,
var_fill_string.value(), pad_length));
Goto(&return_result);
}
BIND(&multi_char_fill);
{
Node* const fill_length_word32 = SmiToWord32(var_fill_length.value());
Node* const pad_length_word32 = SmiToWord32(pad_length);
Node* const repetitions_word32 =
Int32Div(pad_length_word32, fill_length_word32);
Node* const remaining_word32 =
Int32Mod(pad_length_word32, fill_length_word32);
var_pad.Bind(CallBuiltin(Builtins::kStringRepeat, context,
var_fill_string.value(),
SmiFromWord32(repetitions_word32)));
GotoIfNot(remaining_word32, &return_result);
{
Callable substring_callable = CodeFactory::SubString(isolate());
Node* const remainder_string =
CallStub(substring_callable, context, var_fill_string.value(),
SmiConstant(0), SmiFromWord32(remaining_word32));
var_pad.Bind(CallStub(stringadd_callable, context, var_pad.value(),
remainder_string));
Goto(&return_result);
}
}
BIND(&return_result);
CSA_ASSERT(this, SmiEqual(pad_length, LoadStringLength(var_pad.value())));
arguments.PopAndReturn(variant == kStart
? CallStub(stringadd_callable, context,
var_pad.value(), receiver_string)
: CallStub(stringadd_callable, context,
receiver_string, var_pad.value()));
}
BIND(&dont_pad);
arguments.PopAndReturn(receiver_string);
BIND(&invalid_string_length);
{
CallRuntime(Runtime::kThrowInvalidStringLength, context);
Unreachable();
}
}
};
TF_BUILTIN(StringPrototypePadEnd, StringPadAssembler) {
Generate(kEnd, "String.prototype.padEnd");
}
TF_BUILTIN(StringPrototypePadStart, StringPadAssembler) {
Generate(kStart, "String.prototype.padStart");
}
// ES6 #sec-string.prototype.search
TF_BUILTIN(StringPrototypeSearch, StringMatchSearchAssembler) {
Node* const receiver = Parameter(Descriptor::kReceiver);

View File

@ -609,6 +609,8 @@ bool BuiltinHasNoSideEffect(Builtins::Name id) {
case Builtins::kStringPrototypeItalics:
case Builtins::kStringPrototypeLastIndexOf:
case Builtins::kStringPrototypeLink:
case Builtins::kStringPrototypePadEnd:
case Builtins::kStringPrototypePadStart:
case Builtins::kStringPrototypeRepeat:
case Builtins::kStringPrototypeSlice:
case Builtins::kStringPrototypeSmall:

View File

@ -13,67 +13,6 @@
var GlobalString = global.String;
//-------------------------------------------------------------------
function StringPad(thisString, maxLength, fillString) {
maxLength = TO_LENGTH(maxLength);
var stringLength = thisString.length;
if (maxLength <= stringLength) return "";
if (IS_UNDEFINED(fillString)) {
fillString = " ";
} else {
fillString = TO_STRING(fillString);
if (fillString === "") {
// If filler is the empty String, return S.
return "";
}
}
var fillLength = maxLength - stringLength;
var repetitions = (fillLength / fillString.length) | 0;
var remainingChars = (fillLength - fillString.length * repetitions) | 0;
var filler = "";
while (true) {
if (repetitions & 1) filler += fillString;
repetitions >>= 1;
if (repetitions === 0) break;
fillString += fillString;
}
if (remainingChars) {
filler += %_SubString(fillString, 0, remainingChars);
}
return filler;
}
DEFINE_METHODS_LEN(
GlobalString.prototype,
{
/* ES#sec-string.prototype.padstart */
/* String.prototype.padStart(maxLength [, fillString]) */
padStart(maxLength, fillString) {
CHECK_OBJECT_COERCIBLE(this, "String.prototype.padStart");
var thisString = TO_STRING(this);
return StringPad(thisString, maxLength, fillString) + thisString;
}
/* ES#sec-string.prototype.padend */
/* String.prototype.padEnd(maxLength [, fillString]) */
padEnd(maxLength, fillString) {
CHECK_OBJECT_COERCIBLE(this, "String.prototype.padEnd");
var thisString = TO_STRING(this);
return thisString + StringPad(thisString, maxLength, fillString);
}
},
1 /* Set functions length */
);
// -------------------------------------------------------------------
// String methods related to templates

View File

@ -94,3 +94,14 @@
(function TestTruncation() {
assertEquals("ab", "a".padEnd(2, "bc"));
})();
(function TestMaxLength() {
assertThrows(() => "123".padEnd(Math.pow(2, 40)), RangeError);
assertThrows(() => "123".padEnd(1 << 30), RangeError);
})();
(function TestNoArguments() {
assertEquals("abc", "abc".padEnd());
})();

View File

@ -94,3 +94,14 @@
(function TestTruncation() {
assertEquals("ba", "a".padStart(2, "bc"));
})();
(function TestMaxLength() {
assertThrows(() => "123".padStart(Math.pow(2, 40)), RangeError);
assertThrows(() => "123".padStart(1 << 30), RangeError);
})();
(function TestNoArguments() {
assertEquals("abc", "abc".padStart());
})();

View File

@ -449,6 +449,14 @@ test(function() {
}, "Invalid typed array length: -1", RangeError);
// kThrowInvalidStringLength
test(function() {
"a".padEnd(1 << 30);
}, "Invalid string length", RangeError);
test(function() {
"a".padStart(1 << 30);
}, "Invalid string length", RangeError);
test(function() {
"a".repeat(1 << 30);
}, "Invalid string length", RangeError);