Title: [287800] trunk
Revision
287800
Author
[email protected]
Date
2022-01-07 17:52:18 -0800 (Fri, 07 Jan 2022)

Log Message

Expand the set of objects we take JSArray::fastSlice() path for
https://bugs.webkit.org/show_bug.cgi?id=234539

Patch by Alexey Shvayka <[email protected]> on 2022-01-07
Reviewed by Yusuke Suzuki.

JSTests:

* microbenchmarks/array-slice-call-cloned-arguments.js: Added.
* stress/array-slice-beyond-length.js: Added.
* stress/array-slice-length-lookup.js: Added.

Source/_javascript_Core:

Currently, Array.prototype's slice() / splice() methods take a fast path only for
JSArray source objects. With this change, gcSafeMemcpy-based path is taken for any
object with ordinary getOwnPropertySlotByIndex() method, which speeds up the common
case of `[].slice.call(arguments)` by 140% (in strict mode only, see ClonedArguments).

Also, once is https://webkit.org/b/234538 resolved, calling Array.prototype.slice()
on a static NodeList, which is a common idiom to acquire map() / filter() methods,
will become faster as well.

This patch was thoroughly evaluated to be spec-perfect and memory-safe:

  - indexing mode check and holesMustForwardToPrototype() guarantee that there
    are no observable userland code to be invoked;
  - fastSlice() signature is upgraded to uint64_t so `nullptr` is returned in case
    of large "length", resulting in a RangeError being thrown on the slow path;
  - to handle the case of source array being shrinked after "length" lookup (see r175420),
    OOB read check is moved to JSArray::fastSlice() and refined to rely on vectorLength()
    so the double "length" lookup is avoided (added a test for this).

All this (and more) is well covered by the test262 suite.

This change improves Speedometer2/EmberJS-Debug-TodoMVC score by 0.5%: although the test
is slow on its own, `[].slice.call(arguments)` is performed ~56k times per run.

* runtime/ArrayPrototype.cpp:
(JSC::JSC_DEFINE_HOST_FUNCTION):
* runtime/JSArray.cpp:
(JSC::JSArray::fastSlice):
* runtime/JSArray.h:

Modified Paths

Added Paths

Diff

Modified: trunk/JSTests/ChangeLog (287799 => 287800)


--- trunk/JSTests/ChangeLog	2022-01-08 01:00:43 UTC (rev 287799)
+++ trunk/JSTests/ChangeLog	2022-01-08 01:52:18 UTC (rev 287800)
@@ -1,3 +1,14 @@
+2022-01-07  Alexey Shvayka  <[email protected]>
+
+        Expand the set of objects we take JSArray::fastSlice() path for
+        https://bugs.webkit.org/show_bug.cgi?id=234539
+
+        Reviewed by Yusuke Suzuki.
+
+        * microbenchmarks/array-slice-call-cloned-arguments.js: Added.
+        * stress/array-slice-beyond-length.js: Added.
+        * stress/array-slice-length-lookup.js: Added.
+
 2022-01-06  Saam Barati  <[email protected]>
 
         preparePatchpointForExceptions needs to handle tuples

Added: trunk/JSTests/microbenchmarks/array-slice-call-cloned-arguments.js (0 => 287800)


--- trunk/JSTests/microbenchmarks/array-slice-call-cloned-arguments.js	                        (rev 0)
+++ trunk/JSTests/microbenchmarks/array-slice-call-cloned-arguments.js	2022-01-08 01:52:18 UTC (rev 287800)
@@ -0,0 +1,14 @@
+(function() {
+    "use strict";
+    var TEST_LENGTH = 10;
+
+    var slice = Array.prototype.slice;
+    var clonedArguments = (function() { return arguments; }).apply(null, new Array(TEST_LENGTH).fill(1));
+
+    var slicedArray;
+    for (var i = 0; i < 1e6; i++)
+        slicedArray = slice.call(clonedArguments);
+
+    if (slicedArray.length !== TEST_LENGTH)
+        throw new Error("Bad assertion!");
+})();

Added: trunk/JSTests/stress/array-slice-beyond-length.js (0 => 287800)


--- trunk/JSTests/stress/array-slice-beyond-length.js	                        (rev 0)
+++ trunk/JSTests/stress/array-slice-beyond-length.js	2022-01-08 01:52:18 UTC (rev 287800)
@@ -0,0 +1,30 @@
+function shouldBe(actual, expected) {
+    if (actual !== expected)
+        throw new Error(`Bad value: ${actual}!`);
+}
+
+(function sourceIsJSArray() {
+    for (var i = 0; i < 10_000; i++) {
+        var sourceObj = [0, 1, 2];
+        var slicedArr = sourceObj.slice(0, 1000);
+
+        shouldBe(slicedArr.length, 3);
+        shouldBe(slicedArr.join(), "0,1,2");
+    }
+})();
+
+const MAX_ARRAY_LENGTH = 2 ** 32 - 1;
+
+(function sourceIsFinalObject() {
+    for (var i = 0; i < 10_000; i++) {
+        var sourceObj = {};
+        sourceObj[0] = "x";
+        sourceObj[MAX_ARRAY_LENGTH] = "y";
+        sourceObj.length = MAX_ARRAY_LENGTH + 1;
+        sourceObj.slice = Array.prototype.slice;
+        var slicedArr = sourceObj.slice(MAX_ARRAY_LENGTH, MAX_ARRAY_LENGTH + 2);
+
+        shouldBe(slicedArr.length, 1);
+        shouldBe(slicedArr[0], "y");
+    }
+})();

Added: trunk/JSTests/stress/array-slice-length-lookup.js (0 => 287800)


--- trunk/JSTests/stress/array-slice-length-lookup.js	                        (rev 0)
+++ trunk/JSTests/stress/array-slice-length-lookup.js	2022-01-08 01:52:18 UTC (rev 287800)
@@ -0,0 +1,23 @@
+function shouldBe(actual, expected) {
+    if (actual !== expected)
+        throw new Error(`Bad value: ${actual}!`);
+}
+
+(function sourceIsFinalObject() {
+    var lengthLookups = 0;
+    var sourceObj = {
+        0: 0, 1: 1, 2: 2,
+        get length() {
+            lengthLookups++;
+            return 3;
+        },
+    };
+
+    var slicedArr;
+    for (var i = 0; i < 1e6; i++) {
+        slicedArr = [].slice.call(sourceObj);
+    }
+
+    shouldBe(slicedArr.length, 3);
+    shouldBe(lengthLookups, 1e6);
+})();

Modified: trunk/Source/_javascript_Core/ChangeLog (287799 => 287800)


--- trunk/Source/_javascript_Core/ChangeLog	2022-01-08 01:00:43 UTC (rev 287799)
+++ trunk/Source/_javascript_Core/ChangeLog	2022-01-08 01:52:18 UTC (rev 287800)
@@ -1,3 +1,40 @@
+2022-01-07  Alexey Shvayka  <[email protected]>
+
+        Expand the set of objects we take JSArray::fastSlice() path for
+        https://bugs.webkit.org/show_bug.cgi?id=234539
+
+        Reviewed by Yusuke Suzuki.
+
+        Currently, Array.prototype's slice() / splice() methods take a fast path only for
+        JSArray source objects. With this change, gcSafeMemcpy-based path is taken for any
+        object with ordinary getOwnPropertySlotByIndex() method, which speeds up the common
+        case of `[].slice.call(arguments)` by 140% (in strict mode only, see ClonedArguments).
+
+        Also, once is https://webkit.org/b/234538 resolved, calling Array.prototype.slice()
+        on a static NodeList, which is a common idiom to acquire map() / filter() methods,
+        will become faster as well.
+
+        This patch was thoroughly evaluated to be spec-perfect and memory-safe:
+
+          - indexing mode check and holesMustForwardToPrototype() guarantee that there
+            are no observable userland code to be invoked;
+          - fastSlice() signature is upgraded to uint64_t so `nullptr` is returned in case
+            of large "length", resulting in a RangeError being thrown on the slow path;
+          - to handle the case of source array being shrinked after "length" lookup (see r175420),
+            OOB read check is moved to JSArray::fastSlice() and refined to rely on vectorLength()
+            so the double "length" lookup is avoided (added a test for this).
+
+        All this (and more) is well covered by the test262 suite.
+
+        This change improves Speedometer2/EmberJS-Debug-TodoMVC score by 0.5%: although the test
+        is slow on its own, `[].slice.call(arguments)` is performed ~56k times per run.
+
+        * runtime/ArrayPrototype.cpp:
+        (JSC::JSC_DEFINE_HOST_FUNCTION):
+        * runtime/JSArray.cpp:
+        (JSC::JSArray::fastSlice):
+        * runtime/JSArray.h:
+
 2022-01-07  Tim Horton  <[email protected]>
 
         Adopt linkedOnOrAfter() in more places

Modified: trunk/Source/_javascript_Core/runtime/ArrayPrototype.cpp (287799 => 287800)


--- trunk/Source/_javascript_Core/runtime/ArrayPrototype.cpp	2022-01-08 01:00:43 UTC (rev 287799)
+++ trunk/Source/_javascript_Core/runtime/ArrayPrototype.cpp	2022-01-08 01:52:18 UTC (rev 287800)
@@ -1139,10 +1139,8 @@
     if (UNLIKELY(speciesResult.first == SpeciesConstructResult::Exception))
         return { };
 
-    bool okToDoFastPath = speciesResult.first == SpeciesConstructResult::FastPath && isJSArray(thisObj) && length == toLength(globalObject, thisObj);
-    RETURN_IF_EXCEPTION(scope, { });
-    if (LIKELY(okToDoFastPath)) {
-        if (JSArray* result = asArray(thisObj)->fastSlice(globalObject, static_cast<uint32_t>(begin), static_cast<uint32_t>(end - begin)))
+    if (LIKELY(speciesResult.first == SpeciesConstructResult::FastPath)) {
+        if (JSArray* result = JSArray::fastSlice(globalObject, thisObj, begin, end - begin))
             return JSValue::encode(result);
     }
 
@@ -1235,10 +1233,8 @@
         return JSValue::encode(jsUndefined());
 
     JSObject* result = nullptr;
-    bool okToDoFastPath = speciesResult.first == SpeciesConstructResult::FastPath && isJSArray(thisObj) && length == toLength(globalObject, thisObj);
-    RETURN_IF_EXCEPTION(scope, encodedJSValue());
-    if (LIKELY(okToDoFastPath))
-        result = asArray(thisObj)->fastSlice(globalObject, static_cast<uint32_t>(actualStart), static_cast<uint32_t>(actualDeleteCount));
+    if (LIKELY(speciesResult.first == SpeciesConstructResult::FastPath))
+        result = JSArray::fastSlice(globalObject, thisObj, actualStart, actualDeleteCount);
 
     if (!result) {
         if (speciesResult.first == SpeciesConstructResult::CreatedObject)

Modified: trunk/Source/_javascript_Core/runtime/JSArray.cpp (287799 => 287800)


--- trunk/Source/_javascript_Core/runtime/JSArray.cpp	2022-01-08 01:00:43 UTC (rev 287799)
+++ trunk/Source/_javascript_Core/runtime/JSArray.cpp	2022-01-08 01:52:18 UTC (rev 287800)
@@ -725,20 +725,29 @@
     pushInline(globalObject, value);
 }
 
-JSArray* JSArray::fastSlice(JSGlobalObject* globalObject, unsigned startIndex, unsigned count)
+JSArray* JSArray::fastSlice(JSGlobalObject* globalObject, JSObject* source, uint64_t startIndex, uint64_t count)
 {
     VM& vm = globalObject->vm();
 
-    ensureWritable(vm);
+    // FIXME: Avoid converting the source from CoW since we aren't modifying it.
+    // https://bugs.webkit.org/show_bug.cgi?id=234990
+    source->ensureWritable(vm);
 
-    auto arrayType = indexingMode();
+    Structure* sourceStructure = source->structure(vm);
+    if (sourceStructure->typeInfo().interceptsGetOwnPropertySlotByIndexEvenWhenLengthIsNotZero())
+        return nullptr;
+
+    auto arrayType = source->indexingMode() | IsArray;
     switch (arrayType) {
     case ArrayWithDouble:
     case ArrayWithInt32:
     case ArrayWithContiguous: {
-        if (count >= MIN_SPARSE_ARRAY_INDEX || structure(vm)->holesMustForwardToPrototype(vm, this))
+        if (count >= MIN_SPARSE_ARRAY_INDEX || sourceStructure->holesMustForwardToPrototype(vm, source))
             return nullptr;
 
+        if (startIndex + count > source->butterfly()->vectorLength())
+            return nullptr;
+
         Structure* resultStructure = globalObject->arrayStructureForIndexingTypeDuringAllocation(arrayType);
         if (UNLIKELY(hasAnyArrayStorage(resultStructure->indexingType())))
             return nullptr;
@@ -745,15 +754,15 @@
 
         ASSERT(!globalObject->isHavingABadTime());
         ObjectInitializationScope scope(vm);
-        JSArray* resultArray = JSArray::tryCreateUninitializedRestricted(scope, resultStructure, count);
+        JSArray* resultArray = JSArray::tryCreateUninitializedRestricted(scope, resultStructure, static_cast<uint32_t>(count));
         if (UNLIKELY(!resultArray))
             return nullptr;
 
         auto& resultButterfly = *resultArray->butterfly();
         if (arrayType == ArrayWithDouble)
-            gcSafeMemcpy(resultButterfly.contiguousDouble().data(), butterfly()->contiguousDouble().data() + startIndex, sizeof(JSValue) * count);
+            gcSafeMemcpy(resultButterfly.contiguousDouble().data(), source->butterfly()->contiguousDouble().data() + startIndex, sizeof(JSValue) * static_cast<uint32_t>(count));
         else
-            gcSafeMemcpy(resultButterfly.contiguous().data(), butterfly()->contiguous().data() + startIndex, sizeof(JSValue) * count);
+            gcSafeMemcpy(resultButterfly.contiguous().data(), source->butterfly()->contiguous().data() + startIndex, sizeof(JSValue) * static_cast<uint32_t>(count));
 
         ASSERT(resultButterfly.publicLength() == count);
         return resultArray;

Modified: trunk/Source/_javascript_Core/runtime/JSArray.h (287799 => 287800)


--- trunk/Source/_javascript_Core/runtime/JSArray.h	2022-01-08 01:00:43 UTC (rev 287799)
+++ trunk/Source/_javascript_Core/runtime/JSArray.h	2022-01-08 01:52:18 UTC (rev 287800)
@@ -104,7 +104,7 @@
     JS_EXPORT_PRIVATE void push(JSGlobalObject*, JSValue);
     JS_EXPORT_PRIVATE JSValue pop(JSGlobalObject*);
 
-    JSArray* fastSlice(JSGlobalObject*, unsigned startIndex, unsigned count);
+    static JSArray* fastSlice(JSGlobalObject*, JSObject* source, uint64_t startIndex, uint64_t count);
 
     bool canFastCopy(VM&, JSArray* otherArray);
     bool canDoFastIndexedAccess(VM&);
_______________________________________________
webkit-changes mailing list
[email protected]
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to