Commit 4863a550 authored by Simon Zünd's avatar Simon Zünd Committed by Commit Bot

[typedarray] Replace quicksort with mergesort to make TA#sort stable

This CL replaces the current TypedArray#sort with a simpler mergesort.
The fastpath when the user does not provide a comparison function
is still used.

In addition, TypedArray#sort now converts all elements in the
TypedArray to tagged values upfront, sorts them and writes them
back into the TypedArray as the final step.

R=jgruber@chromium.org, tebbi@chromium.org

Bug: v8:8567
Change-Id: Ib672c5cf510f7c0a2e722d1baa2704305a9ff235
Reviewed-on: https://chromium-review.googlesource.com/c/1445987
Commit-Queue: Simon Zünd <szuend@chromium.org>
Reviewed-by: 's avatarJakob Gruber <jgruber@chromium.org>
Reviewed-by: 's avatarMathias Bynens <mathias@chromium.org>
Reviewed-by: 's avatarTobias Tebbi <tebbi@chromium.org>
Cr-Commit-Position: refs/heads/master@{#59271}
parent 238ccdef
......@@ -89,16 +89,18 @@ namespace typed_array {
return Undefined;
}
transitioning macro CallCompareWithDetachedCheck(
context: Context, array: JSTypedArray, comparefn: Callable, a: Object,
b: Object): Number
labels Detached {
transitioning macro CallCompare(
implicit context: Context, array: JSTypedArray,
comparefn: Callable)(a: Object, b: Object): Number {
// a. Let v be ? ToNumber(? Call(comparefn, undefined, x, y)).
const v: Number =
ToNumber_Inline(context, Call(context, comparefn, Undefined, a, b));
// b. If IsDetachedBuffer(buffer) is true, throw a TypeError exception.
if (IsDetachedBuffer(array.buffer)) goto Detached;
if (IsDetachedBuffer(array.buffer)) {
ThrowTypeError(
context, kDetachedOperation, '%TypedArray%.prototype.sort');
}
// c. If v is NaN, return +0.
if (NumberIsNaN(v)) return 0;
......@@ -107,174 +109,56 @@ namespace typed_array {
return v;
}
// InsertionSort is used for smaller arrays.
transitioning macro TypedArrayInsertionSort(
context: Context, array: JSTypedArray, fromArg: Smi, toArg: Smi,
comparefn: Callable, load: LoadFn, store: StoreFn)
labels Detached {
let from: Smi = fromArg;
let to: Smi = toArg;
if (IsDetachedBuffer(array.buffer)) goto Detached;
for (let i: Smi = from + 1; i < to; ++i) {
const element: Object = load(context, array, i);
let j: Smi = i - 1;
for (; j >= from; --j) {
const tmp: Object = load(context, array, j);
const order: Number = CallCompareWithDetachedCheck(
context, array, comparefn, tmp, element) otherwise Detached;
if (order > 0) {
store(context, array, j + 1, tmp);
// Merges two sorted runs [from, middle) and [middle, to)
// from "source" into "target".
transitioning macro
TypedArrayMerge(
implicit context: Context, array: JSTypedArray, comparefn: Callable)(
source: FixedArray, from: Smi, middle: Smi, to: Smi, target: FixedArray) {
let left: Smi = from;
let right: Smi = middle;
for (let targetIndex: Smi = from; targetIndex < to; ++targetIndex) {
if (left < middle && right >= to) {
// If the left run has elements, but the right does not, we take
// from the left.
target[targetIndex] = source[left++];
} else if (left < middle) {
// If both have elements, we need to compare.
const leftElement: Object = source[left];
const rightElement: Object = source[right];
if (CallCompare(leftElement, rightElement) <= 0) {
target[targetIndex] = leftElement;
left++;
} else {
break;
target[targetIndex] = rightElement;
right++;
}
} else {
// No elements on the left, but the right does, so we take
// from the right.
assert(left == middle);
target[targetIndex] = source[right++];
}
store(context, array, j + 1, element);
}
}
transitioning macro TypedArrayQuickSortImpl(
context: Context, array: JSTypedArray, fromArg: Smi, toArg: Smi,
comparefn: Callable, load: LoadFn, store: StoreFn)
labels Detached {
let from: Smi = fromArg;
let to: Smi = toArg;
while (to - from > 1) {
if (to - from <= 10) {
// TODO(szuend): Investigate InsertionSort removal.
// Currently it does not make any difference when the
// benchmarks are run locally.
TypedArrayInsertionSort(
context, array, from, to, comparefn, load, store)
otherwise Detached;
break;
}
// TODO(szuend): Check if a more involved thirdIndex calculation is
// worth it for very large arrays.
const thirdIndex: Smi = from + ((to - from) >> 1);
if (IsDetachedBuffer(array.buffer)) goto Detached;
transitioning builtin
TypedArrayMergeSort(
implicit context: Context, array: JSTypedArray, comparefn: Callable)(
source: FixedArray, from: Smi, to: Smi, target: FixedArray): Object {
assert(to - from > 1);
const middle: Smi = from + ((to - from) >> 1);
// Find a pivot as the median of first, last and middle element.
let v0: Object = load(context, array, from);
let v1: Object = load(context, array, to - 1);
let v2: Object = load(context, array, thirdIndex);
// On the next recursion step source becomes target and vice versa.
// This saves the copy of the relevant range from the original
// array into a work array on each recursion step.
if (middle - from > 1) TypedArrayMergeSort(target, from, middle, source);
if (to - middle > 1) TypedArrayMergeSort(target, middle, to, source);
const c01: Number = CallCompareWithDetachedCheck(
context, array, comparefn, v0, v1) otherwise Detached;
if (c01 > 0) {
// v1 < v0, so swap them.
let tmp: Object = v0;
v0 = v1;
v1 = tmp;
}
// v0 <= v1.
const c02: Number = CallCompareWithDetachedCheck(
context, array, comparefn, v0, v2) otherwise Detached;
if (c02 >= 0) {
// v2 <= v0 <= v1.
const tmp: Object = v0;
v0 = v2;
v2 = v1;
v1 = tmp;
} else {
// v0 <= v1 && v0 < v2.
const c12: Number = CallCompareWithDetachedCheck(
context, array, comparefn, v1, v2) otherwise Detached;
if (c12 > 0) {
// v0 <= v2 < v1.
const tmp: Object = v1;
v1 = v2;
v2 = tmp;
}
}
// v0 <= v1 <= v2.
store(context, array, from, v0);
store(context, array, to - 1, v2);
const pivot: Object = v1;
let lowEnd: Smi = from + 1; // Upper bound of elems lower than pivot.
let highStart: Smi = to - 1; // Lower bound of elems greater than pivot.
let lowEndValue: Object = load(context, array, lowEnd);
store(context, array, thirdIndex, lowEndValue);
store(context, array, lowEnd, pivot);
// From lowEnd to idx are elements equal to pivot.
// From idx to highStart are elements that haven"t been compared yet.
for (let idx: Smi = lowEnd + 1; idx < highStart; idx++) {
let element: Object = load(context, array, idx);
let order: Number = CallCompareWithDetachedCheck(
context, array, comparefn, element, pivot) otherwise Detached;
if (order < 0) {
lowEndValue = load(context, array, lowEnd);
store(context, array, idx, lowEndValue);
store(context, array, lowEnd, element);
lowEnd++;
} else if (order > 0) {
let breakFor: bool = false;
while (order > 0) {
highStart--;
if (highStart == idx) {
breakFor = true;
break;
}
const topElement: Object = load(context, array, highStart);
order = CallCompareWithDetachedCheck(
context, array, comparefn, topElement, pivot)
otherwise Detached;
}
if (breakFor) {
break;
}
const highStartValue: Object = load(context, array, highStart);
store(context, array, idx, highStartValue);
store(context, array, highStart, element);
if (order < 0) {
element = load(context, array, idx);
lowEndValue = load(context, array, lowEnd);
store(context, array, idx, lowEndValue);
store(context, array, lowEnd, element);
lowEnd++;
}
}
}
if ((to - highStart) < (lowEnd - from)) {
TypedArrayQuickSort(
context, array, highStart, to, comparefn, load, store);
to = lowEnd;
} else {
TypedArrayQuickSort(
context, array, from, lowEnd, comparefn, load, store);
from = highStart;
}
}
}
TypedArrayMerge(source, from, middle, to, target);
transitioning builtin TypedArrayQuickSort(
context: Context, array: JSTypedArray, from: Smi, to: Smi,
comparefn: Callable, load: LoadFn, store: StoreFn): JSTypedArray {
try {
TypedArrayQuickSortImpl(context, array, from, to, comparefn, load, store)
otherwise Detached;
}
label Detached {
ThrowTypeError(
context, kDetachedOperation, '%TypedArray%.prototype.sort');
}
return array;
return Undefined;
}
// https://tc39.github.io/ecma262/#sec-%typedarray%.prototype.sort
......@@ -304,62 +188,76 @@ namespace typed_array {
// 4. Let len be obj.[[ArrayLength]].
const len: Smi = array.length;
try {
const comparefn: Callable =
Cast<Callable>(comparefnObj) otherwise CastError;
let loadfn: LoadFn;
let storefn: StoreFn;
let elementsKind: ElementsKind = array.elements_kind;
if (IsElementsKindGreaterThan(elementsKind, UINT32_ELEMENTS)) {
if (elementsKind == INT32_ELEMENTS) {
loadfn = LoadFixedElement<FixedInt32Array>;
storefn = StoreFixedElement<FixedInt32Array>;
} else if (elementsKind == FLOAT32_ELEMENTS) {
loadfn = LoadFixedElement<FixedFloat32Array>;
storefn = StoreFixedElement<FixedFloat32Array>;
} else if (elementsKind == FLOAT64_ELEMENTS) {
loadfn = LoadFixedElement<FixedFloat64Array>;
storefn = StoreFixedElement<FixedFloat64Array>;
} else if (elementsKind == UINT8_CLAMPED_ELEMENTS) {
loadfn = LoadFixedElement<FixedUint8ClampedArray>;
storefn = StoreFixedElement<FixedUint8ClampedArray>;
} else if (elementsKind == BIGUINT64_ELEMENTS) {
loadfn = LoadFixedElement<FixedBigUint64Array>;
storefn = StoreFixedElement<FixedBigUint64Array>;
} else if (elementsKind == BIGINT64_ELEMENTS) {
loadfn = LoadFixedElement<FixedBigInt64Array>;
storefn = StoreFixedElement<FixedBigInt64Array>;
} else {
unreachable;
}
// Arrays of length 1 or less are considered sorted.
if (len < 2) return array;
const comparefn: Callable =
Cast<Callable>(comparefnObj) otherwise unreachable;
let loadfn: LoadFn;
let storefn: StoreFn;
let elementsKind: ElementsKind = array.elements_kind;
if (IsElementsKindGreaterThan(elementsKind, UINT32_ELEMENTS)) {
if (elementsKind == INT32_ELEMENTS) {
loadfn = LoadFixedElement<FixedInt32Array>;
storefn = StoreFixedElement<FixedInt32Array>;
} else if (elementsKind == FLOAT32_ELEMENTS) {
loadfn = LoadFixedElement<FixedFloat32Array>;
storefn = StoreFixedElement<FixedFloat32Array>;
} else if (elementsKind == FLOAT64_ELEMENTS) {
loadfn = LoadFixedElement<FixedFloat64Array>;
storefn = StoreFixedElement<FixedFloat64Array>;
} else if (elementsKind == UINT8_CLAMPED_ELEMENTS) {
loadfn = LoadFixedElement<FixedUint8ClampedArray>;
storefn = StoreFixedElement<FixedUint8ClampedArray>;
} else if (elementsKind == BIGUINT64_ELEMENTS) {
loadfn = LoadFixedElement<FixedBigUint64Array>;
storefn = StoreFixedElement<FixedBigUint64Array>;
} else if (elementsKind == BIGINT64_ELEMENTS) {
loadfn = LoadFixedElement<FixedBigInt64Array>;
storefn = StoreFixedElement<FixedBigInt64Array>;
} else {
if (elementsKind == UINT8_ELEMENTS) {
loadfn = LoadFixedElement<FixedUint8Array>;
storefn = StoreFixedElement<FixedUint8Array>;
} else if (elementsKind == INT8_ELEMENTS) {
loadfn = LoadFixedElement<FixedInt8Array>;
storefn = StoreFixedElement<FixedInt8Array>;
} else if (elementsKind == UINT16_ELEMENTS) {
loadfn = LoadFixedElement<FixedUint16Array>;
storefn = StoreFixedElement<FixedUint16Array>;
} else if (elementsKind == INT16_ELEMENTS) {
loadfn = LoadFixedElement<FixedInt16Array>;
storefn = StoreFixedElement<FixedInt16Array>;
} else if (elementsKind == UINT32_ELEMENTS) {
loadfn = LoadFixedElement<FixedUint32Array>;
storefn = StoreFixedElement<FixedUint32Array>;
} else {
unreachable;
}
unreachable;
}
} else {
if (elementsKind == UINT8_ELEMENTS) {
loadfn = LoadFixedElement<FixedUint8Array>;
storefn = StoreFixedElement<FixedUint8Array>;
} else if (elementsKind == INT8_ELEMENTS) {
loadfn = LoadFixedElement<FixedInt8Array>;
storefn = StoreFixedElement<FixedInt8Array>;
} else if (elementsKind == UINT16_ELEMENTS) {
loadfn = LoadFixedElement<FixedUint16Array>;
storefn = StoreFixedElement<FixedUint16Array>;
} else if (elementsKind == INT16_ELEMENTS) {
loadfn = LoadFixedElement<FixedInt16Array>;
storefn = StoreFixedElement<FixedInt16Array>;
} else if (elementsKind == UINT32_ELEMENTS) {
loadfn = LoadFixedElement<FixedUint32Array>;
storefn = StoreFixedElement<FixedUint32Array>;
} else {
unreachable;
}
TypedArrayQuickSort(context, array, 0, len, comparefn, loadfn, storefn);
}
label CastError {
unreachable;
// Prepare the two work arrays. All numbers are converted to tagged
// objects first, and merge sorted between the two FixedArrays.
// The result is then written back into the JSTypedArray.
const work1: FixedArray = AllocateZeroedFixedArray(Convert<intptr>(len));
const work2: FixedArray = AllocateZeroedFixedArray(Convert<intptr>(len));
for (let i: Smi = 0; i < len; ++i) {
const element: Object = loadfn(context, array, i);
work1[i] = element;
work2[i] = element;
}
TypedArrayMergeSort(work2, 0, len, work1);
// work1 contains the sorted numbers. Write them back.
for (let i: Smi = 0; i < len; ++i) storefn(context, array, i, work1[i]);
return array;
}
}
......@@ -69,6 +69,18 @@ for (var constructor of typedArrayConstructors) {
assertThrows(() => array.sort(), TypeError);
}
// Check that TypedArray.p.sort is stable.
for (let constructor of typedArrayConstructors) {
const template = [2, 1, 0, 4, 4, 4, 4, 4, 4, 4, 4];
const array = new constructor(template);
// Treat 0..3, 4..7, etc. as equal.
const compare = (a, b) => (a / 4 | 0) - (b / 4 | 0);
array.sort(compare);
assertArrayLikeEquals(array.slice(0, 3), [2, 1, 0], constructor);
}
// The following creates a test for each typed element kind, where the array
// to sort consists of some max/min/zero elements.
//
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment