Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
[JSC] Add fast path to TypedArray.from
https://bugs.webkit.org/show_bug.cgi?id=239936

Reviewed by Mark Lam, Ross Kirsling and Keith Miller.

This patch adds a fast path to TypedArray.from. Similar to what we do with Array, we integrate TypedArray's iterator-protocol WatchpointSet.
And now TypedArray has isIteratorProtocolFastAndNonObservable() function which answers whether we can do fast-iteration without
user observable behavior withhout using normal generic for-of iterator protocol.

The most complicated part of this WatchpointSet is that "length" property of TypedArray. Unlike Array, TypedArray does not have
its own "length" property. Instead it has "length" getter in TypedArray.prototype. Thus, we need to ensure the following.

1. Uint8Array instance does not have its own "length" property
2. Uint8Array instance's [[Prototype]] is Uint8Array.prototype
3. Uint8Array.prototype does not have "length" property
4. Uint8Array.prototype's [[Prototype]] is TypedArray.prototype
5. TypedArray.prototype has default "length" getter

We set "length" absence ObjectPropertyCondition (OPC) on Uint8Array.prototype. And we also set "length" equivalence OPC on TypedArray.prototype.length.
As the same with Array's iterator protocol, we also ensure @@iterator function's absence status on Uint8Array.prototype and TypedArray.prototype too.
By installing these watchpoints, we can quickly answer whether this TypedArray instance can get fast iteration.

And in TypedArray.from, we use this condition to bypass normal for-of iteration protocol. Also we extend this function to accept normal Array: using
Array's iterator protocol WatchpointSet to make it fast for Int32 and Double Arrays.

TypedArray.from from TypedArray gets 396.0x faster. And TypedArray.from from Array gets 6.8x faster.

                                   ToT                     Patched

typed-array-from-array      913.8926+-1.7045     ^    133.6719+-0.1703        ^ definitely 6.8368x faster
typed-array-from           1183.6522+-2.1020     ^      2.9890+-0.3176        ^ definitely 396.0042x faster

* microbenchmarks/typed-array-from.js: Added.
* stress/typed-array-from-array-iterator-protocol.js: Added.
(shouldBe):
(shouldBeArray):
(values.__proto__.next):
* stress/typed-array-from.js: Added.
(shouldBe):
(shouldBeArray):
(shouldThrow):
(shouldBeArray.TestArray):
(Uint8Array.prototype.__proto__.Symbol.iterator):
* Source/JavaScriptCore/builtins/BuiltinNames.h:
* Source/JavaScriptCore/builtins/TypedArrayConstructor.js:
* Source/JavaScriptCore/bytecode/LinkTimeConstant.h:
* Source/JavaScriptCore/runtime/JSGlobalObject.cpp:
(JSC::JSGlobalObject::init):
* Source/JavaScriptCore/runtime/JSTypedArrayViewPrototype.cpp:
(JSC::isTypedArrayViewConstructor):
(JSC::JSC_DEFINE_HOST_FUNCTION):
(JSC::JSTypedArrayViewPrototype::finishCreation):
* Source/JavaScriptCore/runtime/JSTypedArrayViewPrototype.h:

Canonical link: https://commits.webkit.org/252976@main
  • Loading branch information
Constellation committed Jul 30, 2022
1 parent 1297883 commit 1b440ef
Show file tree
Hide file tree
Showing 25 changed files with 571 additions and 17 deletions.
4 changes: 4 additions & 0 deletions JSTests/microbenchmarks/typed-array-from-array.js
@@ -0,0 +1,4 @@
var a1 = new Array(1024 * 1024 * 1);
a1.fill(99);
for (var i = 0; i < 1e2; ++i)
var a2 = Uint8Array.from(a1);
4 changes: 4 additions & 0 deletions JSTests/microbenchmarks/typed-array-from.js
@@ -0,0 +1,4 @@
var a1 = new Uint8Array(1024 * 1024 * 1);
a1.fill(99);
for (var i = 0; i < 1e2; ++i)
var a2 = Uint8Array.from(a1);
29 changes: 29 additions & 0 deletions JSTests/stress/bigint64-array-from-array-throw.js
@@ -0,0 +1,29 @@
function shouldThrow(func, errorMessage) {
var errorThrown = false;
var error = null;
try {
func();
} catch (e) {
errorThrown = true;
error = e;
}
if (!errorThrown)
throw new Error('not thrown');
if (String(error) !== errorMessage)
throw new Error(`bad error: ${String(error)}`);
}

shouldThrow(() => {
var a = new Int32Array([0, 1, 2, 3]);
BigInt64Array.from(a);
}, `TypeError: Content types of source and destination typed arrays are different`);

shouldThrow(() => {
var a = [0, 1, 2, 3];
BigInt64Array.from(a);
}, `TypeError: Invalid argument type in ToBigInt operation`);

shouldThrow(() => {
var a = [0.0, 0.1, 0.2, 0.3];
BigInt64Array.from(a);
}, `TypeError: Invalid argument type in ToBigInt operation`);
38 changes: 38 additions & 0 deletions JSTests/stress/typed-array-from-array-iterator-protocol.js
@@ -0,0 +1,38 @@
function shouldBe(actual, expected) {
if (actual !== expected)
throw new Error('bad value: ' + actual);
}

function shouldBeArray(actual, expected) {
shouldBe(actual.length, expected.length);
for (var i = 0; i < expected.length; ++i) {
try {
shouldBe(actual[i], expected[i]);
} catch(e) {
print(JSON.stringify(actual));
throw e;
}
}
}

{
let length = 5;
let index = 0;
[].values().__proto__.next = function () {
if (index < length) {
++index;
return {
value: index,
done: false
};
}
return {
value: null,
done: true
};
};

let a0 = new Uint32Array([0xffffffff, 0xfffffffe, 0xfffffff0, 0xfffff0f0]);
let a1 = Uint8Array.from(a0);
shouldBeArray(a1, [1, 2, 3, 4, 5]);
}
11 changes: 11 additions & 0 deletions JSTests/stress/typed-array-from-custom-length-parent.js
@@ -0,0 +1,11 @@
function shouldBe(actual, expected) {
if (actual !== expected)
throw new Error('bad value: ' + actual);
}

var array = new Uint8Array(128);
Reflect.defineProperty(Uint8Array.prototype, 'length', {
value: 42
});
var result = Uint8Array.from(array);
shouldBe(result.length, 42);
11 changes: 11 additions & 0 deletions JSTests/stress/typed-array-from-custom-length.js
@@ -0,0 +1,11 @@
function shouldBe(actual, expected) {
if (actual !== expected)
throw new Error('bad value: ' + actual);
}

var array = new Uint8Array(128);
Reflect.defineProperty(array, 'length', {
value: 42
});
var result = Uint8Array.from(array);
shouldBe(result.length, 42);
21 changes: 21 additions & 0 deletions JSTests/stress/typed-array-from-different-type.js
@@ -0,0 +1,21 @@
function shouldBe(actual, expected) {
if (actual !== expected)
throw new Error('bad value: ' + actual);
}

{
let a = new Int32Array([0, 1, 2]);
let b = Float64Array.from(a);
shouldBe(b.length, 3);
shouldBe(b[0], 0);
shouldBe(b[1], 1);
shouldBe(b[2], 2);
}
{
let a = new Float32Array([0, 1, 2]);
let b = Int32Array.from(a);
shouldBe(b.length, 3);
shouldBe(b[0], 0);
shouldBe(b[1], 1);
shouldBe(b[2], 2);
}
37 changes: 37 additions & 0 deletions JSTests/stress/typed-array-from-hole.js
@@ -0,0 +1,37 @@
function shouldBe(actual, expected) {
if (actual !== expected)
throw new Error('bad value: ' + actual);
}

{
let a = Int32Array.from([0, , , 6]);
shouldBe(a.length, 4);
shouldBe(a[0], 0);
shouldBe(a[1], 0);
shouldBe(a[2], 0);
shouldBe(a[3], 6);
}
{
let a = Int32Array.from([0.2, , , 6.1]);
shouldBe(a.length, 4);
shouldBe(a[0], 0);
shouldBe(a[1], 0);
shouldBe(a[2], 0);
shouldBe(a[3], 6);
}
{
let a = Float64Array.from([0, , , 6]);
shouldBe(a.length, 4);
shouldBe(a[0], 0);
shouldBe(Number.isNaN(a[1]), true);
shouldBe(Number.isNaN(a[2]), true);
shouldBe(a[3], 6);
}
{
let a = Float64Array.from([0.2, , , 6.1]);
shouldBe(a.length, 4);
shouldBe(a[0], 0.2);
shouldBe(Number.isNaN(a[1]), true);
shouldBe(Number.isNaN(a[2]), true);
shouldBe(a[3], 6.1);
}
81 changes: 81 additions & 0 deletions JSTests/stress/typed-array-from.js
@@ -0,0 +1,81 @@
function shouldBe(actual, expected) {
if (actual !== expected)
throw new Error('bad value: ' + actual);
}

function shouldBeArray(actual, expected) {
shouldBe(actual.length, expected.length);
for (var i = 0; i < expected.length; ++i) {
try {
shouldBe(actual[i], expected[i]);
} catch(e) {
print(JSON.stringify(actual));
throw e;
}
}
}

function shouldThrow(func, errorMessage) {
var errorThrown = false;
var error = null;
try {
func();
} catch (e) {
errorThrown = true;
error = e;
}
if (!errorThrown)
throw new Error('not thrown');
if (String(error) !== errorMessage)
throw new Error(`bad error: ${String(error)}`);
}

{
let a0 = new Uint32Array([0xffffffff, 0xfffffffe, 0xfffffff0, 0xfffff0f0]);
let a1 = Uint8Array.from(a0);
shouldBeArray(a1, [0xff, 0xfe, 0xf0, 0xf0]);

let a2 = Uint32Array.from(a0);
shouldBeArray(a2, a0);
}
{
class TestArray extends Uint8Array {
constructor(size) {
super(size);
}
}

let a0 = new Uint32Array([0xffffffff, 0xfffffffe, 0xfffffff0, 0xfffff0f0]);
let a1 = TestArray.from(a0);
shouldBeArray(a1, [0xff, 0xfe, 0xf0, 0xf0]);

let a2 = Uint32Array.from(a0);
shouldBeArray(a2, a0);
}
{
let a0 = new Uint32Array([0xffffffff, 0xfffffffe, 0xfffffff0, 0xfffff0f0]);
class TestArray extends Uint8Array {
constructor(size) {
super(size);
transferArrayBuffer(a0.buffer);
}
}

let a1 = TestArray.from(a0);
shouldBeArray(a1, [0xff, 0xfe, 0xf0, 0xf0]); // This should pass. When TestArray is created, we should have already collected the data from a0.

shouldThrow(() => {
Uint32Array.from(a0);
}, `TypeError: Underlying ArrayBuffer has been detached from the view`);
}

Uint8Array.prototype.__proto__[Symbol.iterator] = function *() {
for (var i = 0; i < this.length; ++i)
yield 42;
};

{
let a0 = new Uint32Array([0xffffffff, 0xfffffffe, 0xfffffff0, 0xfffff0f0]);
let a1 = Uint8Array.from(a0);
shouldBeArray(a1, [42, 42, 42, 42]);
}
1 change: 1 addition & 0 deletions Source/JavaScriptCore/builtins/BuiltinNames.h
Expand Up @@ -130,6 +130,7 @@ namespace JSC {
macro(isSharedTypedArrayView) \
macro(isDetached) \
macro(typedArrayDefaultComparator) \
macro(typedArrayFromFast) \
macro(isBoundFunction) \
macro(hasInstanceBoundFunction) \
macro(instanceOf) \
Expand Down
9 changes: 8 additions & 1 deletion Source/JavaScriptCore/builtins/TypedArrayConstructor.js
@@ -1,5 +1,6 @@
/*
* Copyright (C) 2015 Apple Inc. All rights reserved.
* Copyright (C) 2015-2022 Apple Inc. All rights reserved.
* Copyright (C) 2022 Jarred Sumner. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
Expand Down Expand Up @@ -59,6 +60,12 @@ function from(items /* [ , mapfn [ , thisArg ] ] */)

var arrayLike = @toObject(items, "TypedArray.from requires an array-like object - not null or undefined");

if (!mapFn) {
var fastResult = @typedArrayFromFast(this, arrayLike);
if (fastResult)
return fastResult;
}

var iteratorMethod = items.@@iterator;
if (!@isUndefinedOrNull(iteratorMethod)) {
if (!@isCallable(iteratorMethod))
Expand Down
1 change: 1 addition & 0 deletions Source/JavaScriptCore/bytecode/LinkTimeConstant.h
Expand Up @@ -56,6 +56,7 @@ class JSGlobalObject;
v(typedArraySort, nullptr) \
v(isTypedArrayView, nullptr) \
v(isSharedTypedArrayView, nullptr) \
v(typedArrayFromFast, nullptr) \
v(isDetached, nullptr) \
v(typedArrayDefaultComparator, nullptr) \
v(isBoundFunction, nullptr) \
Expand Down
29 changes: 29 additions & 0 deletions Source/JavaScriptCore/runtime/JSArrayBufferView.cpp
Expand Up @@ -348,6 +348,35 @@ JSArrayBufferView* validateTypedArray(JSGlobalObject* globalObject, JSValue type
return typedArray;
}

bool JSArrayBufferView::isIteratorProtocolFastAndNonObservable()
{
// Excluding DataView.
if (!isTypedArrayType(type()))
return false;

JSGlobalObject* globalObject = this->globalObject();
TypedArrayType typedArrayType = JSC::typedArrayType(type());
if (!globalObject->isTypedArrayPrototypeIteratorProtocolFastAndNonObservable(typedArrayType))
return false;

VM& vm = globalObject->vm();
Structure* structure = this->structure();
// This is the fast case. Many TypedArrays will be an original typed array structure.
if (globalObject->isOriginalTypedArrayStructure(structure))
return true;

if (getPrototypeDirect() != globalObject->typedArrayPrototype(typedArrayType))
return false;

if (getDirectOffset(vm, vm.propertyNames->iteratorSymbol) != invalidOffset)
return false;

if (getDirectOffset(vm, vm.propertyNames->length) != invalidOffset)
return false;

return true;
}

} // namespace JSC

namespace WTF {
Expand Down
2 changes: 2 additions & 0 deletions Source/JavaScriptCore/runtime/JSArrayBufferView.h
Expand Up @@ -207,6 +207,8 @@ class JSArrayBufferView : public JSNonFinalObject {
static RefPtr<ArrayBufferView> toWrapped(VM&, JSValue);
static RefPtr<ArrayBufferView> toWrappedAllowShared(VM&, JSValue);

bool isIteratorProtocolFastAndNonObservable();

private:
enum Requester { Mutator, ConcurrentThread };
template<Requester, typename ResultType> ResultType byteOffsetImpl();
Expand Down
1 change: 1 addition & 0 deletions Source/JavaScriptCore/runtime/JSGenericTypedArrayView.h
Expand Up @@ -81,6 +81,7 @@ JS_EXPORT_PRIVATE const ClassInfo* getBigUint64ArrayClassInfo();
// static int8_t toNativeFromInt32(int32_t);
// static int8_t toNativeFromUint32(uint32_t);
// static int8_t toNativeFromDouble(double);
// static int8_t toNativeFromUndefined();
// static JSValue toJSValue(int8_t);
// template<T> static T::Type convertTo(uint8_t);
// };
Expand Down
Expand Up @@ -273,10 +273,6 @@ bool JSGenericTypedArrayView<Adaptor>::set(
if (typedArrayType == Adaptor::typeValue)
return memmoveFastPath(jsCast<JSArrayBufferView*>(object));

auto isSomeUint8 = [] (TypedArrayType type) {
return type == TypedArrayType::TypeUint8 || type == TypedArrayType::TypeUint8Clamped;
};

if (isSomeUint8(typedArrayType) && isSomeUint8(Adaptor::typeValue))
return memmoveFastPath(jsCast<JSArrayBufferView*>(object));

Expand Down

0 comments on commit 1b440ef

Please sign in to comment.