LibJS: Skip generic call when using regexp builtins in StringPrototype

For StringPrototype functions that defer to RegExpPrototype builtins,
we can skip the generic call stuff (eliding the execution context etc)
and just call the builtin directly.

1.03x speedup on Octane/regexp.js
This commit is contained in:
Andreas Kling
2025-12-13 09:47:52 -06:00
committed by Andreas Kling
parent add8402536
commit 54b755126c
5 changed files with 54 additions and 12 deletions

View File

@@ -26,6 +26,9 @@ namespace JS::Bytecode {
O(MathSin, math_sin, Math, sin, 1) \
O(MathCos, math_cos, Math, cos, 1) \
O(MathTan, math_tan, Math, tan, 1) \
O(RegExpPrototypeExec, regexp_prototype_exec, RegExpPrototype, exec, 1) \
O(RegExpPrototypeReplace, regexp_prototype_replace, RegExpPrototype, replace, 2) \
O(RegExpPrototypeSplit, regexp_prototype_split, RegExpPrototype, split, 2) \
O(OrdinaryHasInstance, ordinary_has_instance, InternalBuiltin, ordinary_has_instance, 1) \
O(ArrayIteratorPrototypeNext, array_iterator_prototype_next, ArrayIteratorPrototype, next, 0) \
O(MapIteratorPrototypeNext, map_iterator_prototype_next, MapIteratorPrototype, next, 0) \

View File

@@ -2417,6 +2417,9 @@ static ThrowCompletionOr<Value> dispatch_builtin_call(Bytecode::Interpreter& int
return TRY(MathObject::cos_impl(interpreter.vm(), interpreter.get(arguments[0])));
case Builtin::MathTan:
return TRY(MathObject::tan_impl(interpreter.vm(), interpreter.get(arguments[0])));
case Builtin::RegExpPrototypeExec:
case Builtin::RegExpPrototypeReplace:
case Builtin::RegExpPrototypeSplit:
case Builtin::ArrayIteratorPrototypeNext:
case Builtin::MapIteratorPrototypeNext:
case Builtin::SetIteratorPrototypeNext:

View File

@@ -39,14 +39,14 @@ void RegExpPrototype::initialize(Realm& realm)
u8 attr = Attribute::Writable | Attribute::Configurable;
define_native_function(realm, vm.names.toString, to_string, 0, attr);
define_native_function(realm, vm.names.test, test, 1, attr);
define_native_function(realm, vm.names.exec, exec, 1, attr);
define_native_function(realm, vm.names.exec, exec, 1, attr, Bytecode::Builtin::RegExpPrototypeExec);
define_native_function(realm, vm.names.compile, compile, 2, attr);
define_native_function(realm, vm.well_known_symbol_match(), symbol_match, 1, attr);
define_native_function(realm, vm.well_known_symbol_match_all(), symbol_match_all, 1, attr);
define_native_function(realm, vm.well_known_symbol_replace(), symbol_replace, 2, attr);
define_native_function(realm, vm.well_known_symbol_replace(), symbol_replace, 2, attr, Bytecode::Builtin::RegExpPrototypeReplace);
define_native_function(realm, vm.well_known_symbol_search(), symbol_search, 1, attr);
define_native_function(realm, vm.well_known_symbol_split(), symbol_split, 2, attr);
define_native_function(realm, vm.well_known_symbol_split(), symbol_split, 2, attr, Bytecode::Builtin::RegExpPrototypeSplit);
define_native_accessor(realm, vm.names.flags, flags, {}, Attribute::Configurable);
define_native_accessor(realm, vm.names.source, source, {}, Attribute::Configurable);
@@ -459,8 +459,12 @@ ThrowCompletionOr<Value> regexp_exec(VM& vm, Object& regexp_object, GC::Ref<Prim
// 2. If IsCallable(exec) is true, then
if (exec.is_function()) {
auto& exec_function = exec.as_function();
if (&exec_function == vm.current_realm()->get_builtin_value(Bytecode::Builtin::RegExpPrototypeExec))
return regexp_builtin_exec(vm, static_cast<RegExpObject&>(regexp_object), string);
// a. Let result be ? Call(exec, R, « S »).
auto result = TRY(call(vm, exec.as_function(), &regexp_object, string));
auto result = TRY(call(vm, exec_function, &regexp_object, string));
// b. If Type(result) is neither Object nor Null, throw a TypeError exception.
if (!result.is_object() && !result.is_null())
@@ -718,6 +722,11 @@ JS_DEFINE_NATIVE_FUNCTION(RegExpPrototype::symbol_replace)
// 3. Let S be ? ToString(string).
auto string = TRY(string_value.to_primitive_string(vm));
return symbol_replace_impl(vm, *regexp_object, string, replace_value);
}
ThrowCompletionOr<Value> RegExpPrototype::symbol_replace_impl(VM& vm, Object& regexp_object, GC::Ref<PrimitiveString> string, Value replace_value)
{
// 4. Let lengthS be the number of code unit elements in S.
// 5. Let functionalReplace be IsCallable(replaceValue).
@@ -730,7 +739,7 @@ JS_DEFINE_NATIVE_FUNCTION(RegExpPrototype::symbol_replace)
// 7. Let flags be ? ToString(? Get(rx, "flags")).
static Bytecode::PropertyLookupCache cache;
auto flags_value = TRY(regexp_object->get(vm.names.flags, cache));
auto flags_value = TRY(regexp_object.get(vm.names.flags, cache));
auto flags = TRY(flags_value.to_string(vm));
// 8. If flags contains "g", let global be true. Otherwise, let global be false.
@@ -740,7 +749,7 @@ JS_DEFINE_NATIVE_FUNCTION(RegExpPrototype::symbol_replace)
if (global) {
// a. Perform ? Set(rx, "lastIndex", +0𝔽, true).
static Bytecode::PropertyLookupCache cache2;
TRY(regexp_object->set(vm.names.lastIndex, Value(0), cache2));
TRY(regexp_object.set(vm.names.lastIndex, Value(0), cache2));
}
// 10. Let results be a new empty List.
@@ -969,8 +978,6 @@ JS_DEFINE_NATIVE_FUNCTION(RegExpPrototype::source)
// 22.2.6.14 RegExp.prototype [ @@split ] ( string, limit ), https://tc39.es/ecma262/#sec-regexp.prototype-@@split
JS_DEFINE_NATIVE_FUNCTION(RegExpPrototype::symbol_split)
{
auto& realm = *vm.current_realm();
// 1. Let rx be the this value.
// 2. If Type(rx) is not Object, throw a TypeError exception.
auto regexp_object = TRY(this_object(vm));
@@ -978,12 +985,19 @@ JS_DEFINE_NATIVE_FUNCTION(RegExpPrototype::symbol_split)
// 3. Let S be ? ToString(string).
auto string = TRY(vm.argument(0).to_primitive_string(vm));
return symbol_split_impl(vm, *regexp_object, string, vm.argument(1));
}
ThrowCompletionOr<Value> RegExpPrototype::symbol_split_impl(VM& vm, Object& regexp_object, GC::Ref<PrimitiveString> string, Value limit_value)
{
auto& realm = *vm.current_realm();
// 4. Let C be ? SpeciesConstructor(rx, %RegExp%).
auto* constructor = TRY(species_constructor(vm, regexp_object, realm.intrinsics().regexp_constructor()));
// 5. Let flags be ? ToString(? Get(rx, "flags")).
static Bytecode::PropertyLookupCache cache;
auto flags_value = TRY(regexp_object->get(vm.names.flags, cache));
auto flags_value = TRY(regexp_object.get(vm.names.flags, cache));
auto flags = TRY(flags_value.to_string(vm));
// 6. If flags contains "u" or flags contains "v", let unicodeMatching be true.
@@ -995,7 +1009,7 @@ JS_DEFINE_NATIVE_FUNCTION(RegExpPrototype::symbol_split)
auto new_flags = flags.bytes_as_string_view().find('y').has_value() ? move(flags) : MUST(String::formatted("{}y", flags));
// 10. Let splitter be ? Construct(C, « rx, newFlags »).
auto splitter = TRY(construct(vm, *constructor, regexp_object, PrimitiveString::create(vm, move(new_flags))));
auto splitter = TRY(construct(vm, *constructor, &regexp_object, PrimitiveString::create(vm, move(new_flags))));
// 11. Let A be ! ArrayCreate(0).
auto array = MUST(Array::create(realm, 0));
@@ -1005,8 +1019,8 @@ JS_DEFINE_NATIVE_FUNCTION(RegExpPrototype::symbol_split)
// 13. If limit is undefined, let lim be 2^32 - 1; else let lim be (? ToUint32(limit)).
auto limit = NumericLimits<u32>::max();
if (!vm.argument(1).is_undefined())
limit = TRY(vm.argument(1).to_u32(vm));
if (!limit_value.is_undefined())
limit = TRY(limit_value.to_u32(vm));
// 14. If lim is 0, return A.
if (limit == 0)

View File

@@ -22,6 +22,9 @@ public:
virtual void initialize(Realm&) override;
virtual ~RegExpPrototype() override = default;
static ThrowCompletionOr<Value> symbol_replace_impl(VM&, Object& regexp_object, GC::Ref<PrimitiveString> string, Value replace_value);
static ThrowCompletionOr<Value> symbol_split_impl(VM&, Object& regexp_object, GC::Ref<PrimitiveString> string, Value limit_value);
private:
explicit RegExpPrototype(Realm&);

View File

@@ -22,6 +22,7 @@
#include <LibJS/Runtime/Intl/CollatorConstructor.h>
#include <LibJS/Runtime/PrimitiveString.h>
#include <LibJS/Runtime/RegExpObject.h>
#include <LibJS/Runtime/RegExpPrototype.h>
#include <LibJS/Runtime/StringIterator.h>
#include <LibJS/Runtime/StringObject.h>
#include <LibJS/Runtime/StringPrototype.h>
@@ -876,6 +877,12 @@ JS_DEFINE_NATIVE_FUNCTION(StringPrototype::replace)
// b. If replacer is not undefined, then
if (replacer) {
if (replacer == vm.current_realm()->get_builtin_value(Bytecode::Builtin::RegExpPrototypeReplace)) {
// OPTIMIZATION: The common case of RegExp.prototype[@@replace]
auto& rx = search_value.as_object();
auto string = TRY(this_object.to_primitive_string(vm));
return TRY(RegExpPrototype::symbol_replace_impl(vm, rx, *string, replace_value));
}
// i. Return ? Call(replacer, searchValue, « O, replaceValue »).
return TRY(call(vm, *replacer, search_value, this_object, replace_value));
}
@@ -972,6 +979,12 @@ JS_DEFINE_NATIVE_FUNCTION(StringPrototype::replace_all)
// d. If replacer is not undefined, then
if (replacer) {
if (replacer == vm.current_realm()->get_builtin_value(Bytecode::Builtin::RegExpPrototypeReplace)) {
// OPTIMIZATION: The common case of RegExp.prototype[@@replace]
auto& rx = search_value.as_object();
auto string = TRY(this_object.to_primitive_string(vm));
return TRY(RegExpPrototype::symbol_replace_impl(vm, rx, *string, replace_value));
}
// i. Return ? Call(replacer, searchValue, « O, replaceValue »).
return TRY(call(vm, *replacer, search_value, this_object, replace_value));
}
@@ -1154,6 +1167,12 @@ JS_DEFINE_NATIVE_FUNCTION(StringPrototype::split)
auto splitter = TRY(separator_argument.get_method(vm, vm.well_known_symbol_split(), cache));
// b. If splitter is not undefined, then
if (splitter) {
if (splitter == realm.get_builtin_value(Bytecode::Builtin::RegExpPrototypeSplit)) {
// OPTIMIZATION: The common case of RegExp.prototype[@@split]
auto& rx = separator_argument.as_object();
auto string = TRY(object.to_primitive_string(vm));
return TRY(RegExpPrototype::symbol_split_impl(vm, rx, *string, limit_argument));
}
// i. Return ? Call(splitter, separator, « O, limit »).
return TRY(call(vm, *splitter, separator_argument, object, limit_argument));
}