Skip to content

Commit

Permalink
[MERGE #5724 @rhuanjl] Implement stable MergeSort for Array.prototype…
Browse files Browse the repository at this point in the history
….sort

Merge pull request #5724 from rhuanjl:mergeSort

Picking up on the discussion in #5661 This PR implements a stable bottom up Merge Sort as a JsBuiltin for arrays of any length up to 2^32 (well I hit out of memory trying to allocate an array with length above 2^29 but in theory).

I'm not sure if it's good enough to merge as is but would appreciate feedback.

**EDIT:** I've made some large edits to the below to reflect changes made.

**Issues to consider:**
1. **Performance - DefaultCompare** - My Default Compare sort is very slow despite cacheing all the string conversions at the start it. The string less than operation is a significant bottle neck - I have tried:
    - a native chakraLibrary method to compare strings - this was about the same performance as using less than
    - using charCodeAt in a loop - this was also about the same performance as using less than

1. **Insertion sort** - I have included an insertion sort directly in the Array.prototype.sort function used for short arrays - could consider what the best cut off is before switching to mergeSort instead - currently length of 2048 is used.

1. **Memory usage** - My implementation of merge sort needs a buffer array with length up to half the length of the array being sorted.

1. **Scope** - I've not looked at the sort method for TypedArrays obviously stabilising that doesn't make sense (though may be worth looking at its performance on xplat as it uses the earlier mentioned slow xplat qsort for arrays of any length)

1. **Tests** - I've consolidated most of the pre-existing array sort tests and also added a test for sorting a variety of random arrays and ensuring that the sort is both correct and stable

1. **General** - see other comments I've added below...

fixes: #5661
fixes: #5719
  • Loading branch information
sethbrenith committed Nov 1, 2018
2 parents 4d6cf88 + 94b481d commit c565b12
Show file tree
Hide file tree
Showing 23 changed files with 9,200 additions and 7,334 deletions.
1 change: 1 addition & 0 deletions lib/Runtime/Base/JnDirectFields.h
Expand Up @@ -650,6 +650,7 @@ ENTRY(Array_values)
ENTRY(Array_keys)
ENTRY(Array_entries)
ENTRY(Array_indexOf)
ENTRY(Array_sort)
ENTRY(Array_filter)
ENTRY(Array_flat)
ENTRY(Array_flatMap)
Expand Down
4 changes: 2 additions & 2 deletions lib/Runtime/ByteCode/ByteCodeCacheReleaseFileVersion.h
Expand Up @@ -4,6 +4,6 @@
//-------------------------------------------------------------------------------------------------------
// NOTE: If there is a merge conflict the correct fix is to make a new GUID.

// {D8913E7E-E430-4B28-81DD-EDD3EE5F263B}
// {49A3F597-0DAD-4BD8-82A0-CEAC52C99E63}
const GUID byteCodeCacheReleaseFileVersion =
{ 0xD8913E7E, 0xE430, 0x4B28, { 0x81, 0xDD, 0xED, 0xD3, 0xEE, 0x5F, 0x26, 0x3B } };
{ 0x49A3F597, 0x0DAD, 0x4BD8, { 0x82, 0xA0, 0xCE, 0xAC, 0x52, 0xC9, 0x9E, 0x63 } };
1,570 changes: 785 additions & 785 deletions lib/Runtime/Library/InJavascript/Intl.js.bc.32b.h

Large diffs are not rendered by default.

1,576 changes: 788 additions & 788 deletions lib/Runtime/Library/InJavascript/Intl.js.bc.64b.h

Large diffs are not rendered by default.

1,586 changes: 793 additions & 793 deletions lib/Runtime/Library/InJavascript/Intl.js.nojit.bc.32b.h

Large diffs are not rendered by default.

1,544 changes: 772 additions & 772 deletions lib/Runtime/Library/InJavascript/Intl.js.nojit.bc.64b.h

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/Runtime/Library/JavascriptLibrary.cpp
Expand Up @@ -1796,7 +1796,6 @@ namespace Js
builtinFuncs[BuiltinFunction::JavascriptArray_Reverse] = library->AddFunctionToLibraryObject(arrayPrototype, PropertyIds::reverse, &JavascriptArray::EntryInfo::Reverse, 0);
builtinFuncs[BuiltinFunction::JavascriptArray_Shift] = library->AddFunctionToLibraryObject(arrayPrototype, PropertyIds::shift, &JavascriptArray::EntryInfo::Shift, 0);
builtinFuncs[BuiltinFunction::JavascriptArray_Slice] = library->AddFunctionToLibraryObject(arrayPrototype, PropertyIds::slice, &JavascriptArray::EntryInfo::Slice, 2);
/* No inlining Array_Sort */ library->AddFunctionToLibraryObject(arrayPrototype, PropertyIds::sort, &JavascriptArray::EntryInfo::Sort, 1);
builtinFuncs[BuiltinFunction::JavascriptArray_Splice] = library->AddFunctionToLibraryObject(arrayPrototype, PropertyIds::splice, &JavascriptArray::EntryInfo::Splice, 2);

// The toString and toLocaleString properties are shared between Array.prototype and %TypedArray%.prototype.
Expand All @@ -1822,6 +1821,7 @@ namespace Js
{
builtinFuncs[BuiltinFunction::JavascriptArray_IndexOf] = library->AddFunctionToLibraryObject(arrayPrototype, PropertyIds::indexOf, &JavascriptArray::EntryInfo::IndexOf, 1);
builtinFuncs[BuiltinFunction::JavascriptArray_Includes] = library->AddFunctionToLibraryObject(arrayPrototype, PropertyIds::includes, &JavascriptArray::EntryInfo::Includes, 1);
/* No inlining Array_Sort */ library->AddFunctionToLibraryObject(arrayPrototype, PropertyIds::sort, &JavascriptArray::EntryInfo::Sort, 1);
}

builtinFuncs[BuiltinFunction::JavascriptArray_LastIndexOf] = library->AddFunctionToLibraryObject(arrayPrototype, PropertyIds::lastIndexOf, &JavascriptArray::EntryInfo::LastIndexOf, 1);
Expand Down
199 changes: 199 additions & 0 deletions lib/Runtime/Library/JsBuiltIn/JsBuiltIn.js
Expand Up @@ -174,6 +174,205 @@
}
});

platform.registerChakraLibraryFunction("MergeSort", function(array, length, compareFn) {
const buffer = [];
buffer.__proto__ = null;

let bucketSize = 2, lastSize = 1, position = 0;
const doubleLength = length + length;

while (bucketSize < doubleLength) {
while (position < length) {
const left = position;
const mid = left + lastSize;

// perform a merge but only if it's necessary
if (mid < length && compareFn(array[mid], array[mid - 1]) < 0) {
let right = position + bucketSize;
right = right < length ? right : length;
let i = mid - 1, j = 0, k = mid;

while (k < right) {
buffer[j++] = array[k++];
}

let rightElement = buffer[--j];
let leftElement = array[i];

for (;;) {
if (compareFn(rightElement, leftElement) < 0) {
array[--k] = leftElement;
if (i > left) {
leftElement = array[--i];
} else {
array[--k] = rightElement;
break;
}
} else {
array[--k] = rightElement;
if (j > 0) {
rightElement = buffer[--j];
} else {
break;
}
}
}

while (j > 0) {
array[--k] = buffer[--j];
}
}
position += bucketSize;
}
position = 0;
lastSize = bucketSize;
bucketSize *= 2;
}
});

platform.registerChakraLibraryFunction("DefaultStringSortCompare", function(left, right) {
// this version is used when the array was already strings
// as the sort only ever checks for < 0 on the return value of compare functions
// only have to handle this case
if (left < right) {
return -1;
}
return 0;
});

platform.registerChakraLibraryFunction("DefaultSortCompare", function(left, right) {
// as the sort only ever checks for < 0 on the return value of compare functions
// only have to handle this case
if (left.string < right.string) {
return -1;
}
return 0;
});

platform.registerChakraLibraryFunction("CreateCompareArray", function(array, length) {
let useCompareArray = false;
let i = 0;
while (i < length) {
if (typeof array[i++] !== "string") {
useCompareArray = true;
break;
}
}

if (useCompareArray === true) {
const compArray = [];
compArray.__proto__ = null;
i = 0;
let value;
while (i < length) {
value = array[i];
compArray[i++] = {
value : value,
string : "" + value
};
}
return compArray;
}
return array;
});

platform.registerChakraLibraryFunction("FillArrayHoles", function(array, length, offset) {
let i = offset, j = offset, holes = 0;
let value;
while (i < length) {
value = array[i];
if (value !== undefined) {
array[j++] = value;
} else if (!(i in array)) {
++holes;
}
++i;
}

const valuesLength = j;
const hasLength = length - holes;
while (j < hasLength) {
array[j++] = undefined;
}
while (j < length) {
delete array[j++];
}
return valuesLength;
});

platform.registerFunction(platform.FunctionKind.Array_sort, function (compareFn) {
//#sec-array.prototype.sort
if (compareFn !== undefined && typeof compareFn !== "function") {
__chakraLibrary.raiseFunctionArgument_NeedFunction("Array.prototype.sort");
}

const {o, len} = __chakraLibrary.CheckArrayAndGetLen(this, "Array.prototype.sort");

if (len < 2) { // early return if length < 2
return o;
}

// check for if the array has any missing values
// also pull in any values from the prototype
let i = 0, length = len;
while (i < len) {
if (o[i] === undefined) {
length = __chakraLibrary.FillArrayHoles(o, len, i);
break;
}
o[i] = o[i++];
}

let compArray = o;
if (compareFn === undefined && length > 1) {
compArray = __chakraLibrary.CreateCompareArray(o, length);
if (compArray === o) {
compareFn = __chakraLibrary.DefaultStringSortCompare;
} else {
compareFn = __chakraLibrary.DefaultSortCompare;
}
}

// for short arrays perform an insertion sort
if (length < 2048) {
let sortedCount = 1, lowerBound = 0, insertPoint = 0, upperBound = 0;
while (sortedCount < length) {
const item = compArray[sortedCount];
upperBound = sortedCount;
insertPoint = sortedCount - 1; // this lets us check for already ordered first
lowerBound = 0;
for (;;) {
if (compareFn (item, compArray[insertPoint]) < 0) {
upperBound = insertPoint;
} else {
lowerBound = insertPoint + 1;
}
if (lowerBound >= upperBound) {
break;
}
insertPoint = lowerBound + (upperBound - lowerBound >> 1);
}
insertPoint = sortedCount;
while (insertPoint > lowerBound) {
compArray[insertPoint--] = compArray[insertPoint];
}
compArray[lowerBound] = item;
++sortedCount;
}
} else {
__chakraLibrary.MergeSort(compArray, length, compareFn);
}

if (compArray !== o) {
i = 0;
while (i < length) {
o[i] = compArray[i++].value;
}
}

return o;
});

platform.registerFunction(platform.FunctionKind.Array_filter, function (callbackfn, thisArg = undefined) {
// ECMAScript 2017 #sec-array.prototype.filter

Expand Down

0 comments on commit c565b12

Please sign in to comment.