Skip to content

Conversation

@rhuanjl
Copy link
Collaborator

@rhuanjl rhuanjl commented Sep 22, 2018

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
  2. 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.

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

  4. 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)

  5. 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

  6. General - see other comments I've added below...

fixes: #5661
fixes: #5719

if (!scriptContext->IsJsBuiltInEnabled())
{
builtinFuncs[BuiltinFunction::JavascriptArray_IndexOf] = library->AddFunctionToLibraryObject(arrayPrototype, PropertyIds::indexOf, &JavascriptArray::EntryInfo::IndexOf, 1);
/* No inlining Array_Sort */ library->AddFunctionToLibraryObject(arrayPrototype, PropertyIds::sort, &JavascriptArray::EntryInfo::Sort, 1);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have not added this to the builtinFuncs object - I'm unsure if that is necessary - seems to be just for referencing methods from intl.js?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

builtinFuncs is, I think, how the JIT gets information for CallDirecting native functions -- "inlining", technically, but not in the same way as inlining a JS function.

__chakraLibrary.raiseFunctionArgument_NeedFunction = platform.raiseFunctionArgument_NeedFunction;
__chakraLibrary.functionBind = platform.builtInJavascriptFunctionEntryBind;
__chakraLibrary.objectDefineProperty = _objectDefineProperty;
__chakraLibrary.toString = platform.String;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not certain if this is the right way to access String()? (copied logic from intl.js)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String() is actually not the same as the specification's ToString() operation; the former has a special case for Symbols. ToString(symbol) will throw; String(symbol) will not. Attempting to sort an array with any symbols in it using the default comparator should throw, and it does at least in V8:

> ([ Symbol(), Symbol(), Symbol() ]).sort()
TypeError: Cannot convert a Symbol value to a string
    at Array.sort (native)

I'm not sure there's a clean way to trigger ToString() coercion other than

let stringifiedValue = "" + valueToStringify;

}
});

platform.registerChakraLibraryFunction("DefaultMergeSort", function(array, length) {
Copy link
Collaborator Author

@rhuanjl rhuanjl Sep 22, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method handles the no comparator function case. I made it a whole separate function to make the logic simpler.

As the default comparison is to compare strings I make an array of Strings of every element in the input array first - as converting for each comparison was slower.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The specification says to do a ToString() coercion on the two items to be compared for every comparison. Can ToString() have side effects? If so your optimization will be an observable spec violation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the other hand, no particular sorting algorithm is specified and I suppose it's entirely possible for each item to only be passed to the comparator once so maybe it's not meaningfully observable after all.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ToString() can have side effects, but I believe that that's listed as a situation where the resulting data structure is implementation-defined.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Penguinwizzard What I meant was, the specification says the default compare function should do ToString() on each operand each time it’s called, while @rhuanjl claims to have ToString()’d the whole array in advance to compare the stringified items directly, and I wasn’t sure if that was a spec violation or not.

It’s probably fine in practice since the actual order and number of comparisons is already implementation-defined.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty sure it's fine per spec BUT - it's still not fast enough I think I need to make my mechanism for default compare better before you can merge this - whilst the results with a provided compare method are comparable (per the numbers in the gist above) currently there is a large regression with the default compare.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default compare "intentionally" (by design) causes numbers to sort as strings -- obviously a custom comparison function will achieve better performance. What do you have in mind to improve the default comparator? Seems like the ToString operation would be the bottleneck.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Running it through a profiler at the moment the bottleneck actually seems to be the less than operation being used on the strings I'm not sure if either there isn't a Jit fast path for it OR it isn't being hit- I got similar performance scrapping the cacheing and implementing a method in JsBuiltinEngineInterfaceObject.cpp to convert to string and compare - if I have a method that just compares combined with cacheing I may be a little better - if that doesn't work I have a few other ideas.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the < operator doesn't seem to have any fast path for strings. I tried a small JS file that used < on strings a bunch and it generated code that:

  1. Checks whether both operands are ints (they never were, but I guess maybe it pays to be optimistic sometimes)
  2. If not, branches to a helper block
  3. In the helper block, calls Op_Less, which will check whether the operands are numbers again and then switch based on their type
  4. Checks whether there was an implicit call and bails out if so

I don't think this speed regression should block merging this PR, because I expect most callers pass a comparator function anyway, and stable output is a very nice upgrade. I could imagine two options for improvement:

  • Expose a native method for string-string comparisons and call it here
  • Change the lowerer to generate nicer code for comparisons between likely-string values (which could benefit code outside this method too)

Code for < operation:

 GLOBOPT INSTR:                        BrLt_A         $L9, s28[LikelyCanBeTaggedValue_String].var, s32[LikelyCanBeTaggedValue_String].var #0000   Func # (#1.1), #2 Bailout: #0009 Func  (#1.1), #2 (BailOutOnImplicitCalls)


 1E4D781120F   s107(r15).i64 = MOV    s28(r10)[LikelyCanBeTaggedValue_String].var
 1E4D7811212   s107(r15).i64 = SHR    s107(r15).i64, 48 (0x30).i8
 1E4D7811216   s108(rbx).i64 = MOV    s32(r11)[LikelyCanBeTaggedValue_String].var
 1E4D7811219   s108(rbx).i64 = SHR    s108(rbx).i64, 32 (0x20).i8
 1E4D781121D   s107(r15).i64 = OR     s107(r15).i64, s108(rbx).i64
 1E4D7811220           CMP            s107(r15).i32, 65537 (0x10001).i32
 1E4D7811227           JNE            $L24
 1E4D781122D           CMP            s28(r10)[LikelyCanBeTaggedValue_String].i32, s32(r11)[LikelyCanBeTaggedValue_String].i32
 1E4D7811230           JLT            $L9
 1E4D7811232  $L25:
 1E4D7811232   s162(rbx).i32 = XOR    s162(rbx).i32, s162(rbx).i32             Func # (#1.1), #2

The helper at L24 looks like this:

 1E4D7811584  $L24: [helper]
 1E4D7811584   s28<-32>.var = MOV     (r10).var                                Func # (#1.1), #2
 1E4D7811588   s32<-40>.var = MOV     (r11).var                                Func # (#1.1), #2
 1E4D781158C   s168<-80>.i64 = MOV    (r8).i64                                 Func # (#1.1), #2
 1E4D7811590   s167<-72>.i64 = MOV    (r9).i64                                 Func # (#1.1), #2
 1E4D7811594   s166<-64>.i64 = MOV    (rdx).i64                                Func # (#1.1), #2
 1E4D7811598   s165<-56>.i64 = MOV    (rcx).i64                                Func # (#1.1), #2
 1E4D781159C   s164<-48>.i64 = MOV    (rax).i64                                Func # (#1.1), #2
 1E4D78115A0   s109(rbx).var = MOV    s32(r11)[LikelyCanBeTaggedValue_String].var  Func # (#1.1), #2
 1E4D78115A3   s110(r15).var = MOV    s28(r10)[LikelyCanBeTaggedValue_String].var  Func # (#1.1), #2
 1E4D78115A6   arg3(s113)(r8).u64 = MOV  0x01DCD5DCC300 (ScriptContext).u64
 1E4D78115B0   arg2(s114)(rdx).var = MOV  s109(rbx).var
 1E4D78115B3   arg1(s115)(rcx).var = MOV  s110(r15).var
 1E4D78115B6   s116(rax).u64 = MOV    Op_Less.u64
 1E4D78115C0   s99(rbx).u64 = MOV     s99<-24>.u64
 1E4D78115C4   [s99(rbx).u64 <0x01DCD5DC1FE0 (&ImplicitCallFlags)>].u8 = MOV  1 (0x1).i8
 1E4D78115C7   s28<-32>.var = MOV     s28(r10).var
 1E4D78115CB           NOP            3 (0x3).i8
 1E4D78115CE   s32<-40>.var = MOV     s32(r11).var
 1E4D78115D2   s112(rax).i64 = CALL   s116(rax).u64                            Func # (#1.1), #2
 1E4D78115D5   s111(rbx).i64 = MOV    s112(rax).i64
 1E4D78115D8   s99(r11).u64 = MOV     s99<-24>.u64                             Func # (#1.1), #2
 1E4D78115DC           CMP            [s99(r11).u64 <0x01DCD5DC1FE0 (&ImplicitCallFlags)>].u8, 1 (0x1).i8  Func # (#1.1), #2
 1E4D78115E0   (rax).i64 = MOV        s164<-48>.i64
 1E4D78115E4   (rcx).i64 = MOV        s165<-56>.i64
 1E4D78115E8   (rdx).i64 = MOV        s166<-64>.i64
 1E4D78115EC   (r9).i64 = MOV         s167<-72>.i64
 1E4D78115F0   (r8).i64 = MOV         s168<-80>.i64
 1E4D78115F4   (r11).var = MOV        s32<-40>.var
 1E4D78115F8   (r10).var = MOV        s28<-32>.var
 1E4D78115FC           JEQ            $L26                                     Func # (#1.1), #2
 1E4D78115FE  $L27: [helper]
 1E4D78115FE   s28<-32>.var = MOV     s28(r10).var
 1E4D7811602   s32<-40>.var = MOV     s32(r11).var
 1E4D7811606   (rdx).i64 = MOV        s111(rbx).i64                            Func # (#1.1), #2
 1E4D7811609   (rcx).u64 = MOV        0x01E4D75EDC60 (BailOutRecord).u64       Func # (#1.1), #2
 1E4D7811613   (rax).u64 = MOV        SaveAllRegistersAndBranchBailOut.u64     Func # (#1.1), #2
 1E4D781161D           CALL           (rax).u64                                Func # (#1.1), #2 Bailout: #0009 Func  (#1.1), #2 (BailOutOnImplicitCalls)
 1E4D7811620           JMP            $L23
 1E4D7811625  $L26: [helper]
 1E4D7811625           TEST           s111(rbx).i64, s111(rbx).i64
 1E4D7811628           JNE            $L9                                      Func # (#1.1), #2
 1E4D781162E           JMP            $L25

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might also want to add the capability to know that the array is entirely strings; then we'd be able to do this particular operation significantly more quickly, and it wouldn't surprise me if there's a good number of rwc situations where arrays of densely packed strings are common.


assert.isTrue(getterCalled);
assert.areEqual([101,11,22,,77,16], arr, '77 and 16 are not part of the sort so they are not sorted');
assert.areEqual([101,11,101,,77,16], arr, '77 and 16 are not part of the sort so they are not sorted');
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test case involves a getter which provides a value each time it's called but does not set anything back. The order of comparisons in your sort therefore effects what values end up in your sorted array - hence the change of output.

The test was meant to check that 77 and 16 which are added to the end of the array by the getter aren't sorted - that is still the case.

write(arr);

function comparefn(x,y) { arr[0]="test"; return x - y; }
function comparefnOne(x,y) { arr[0]="test"; return x - y; }
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test case had two identically named compare functions therefore hoisting meant only the second one was used. Renamed so both are used.

try {
var propName = 1;
Object.defineProperty(Array.prototype, propName, {
Object.defineProperty(Object.prototype, propName, {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test case was testing incorrect behaviour see #5719

@rhuanjl rhuanjl force-pushed the mergeSort branch 3 times, most recently from ab369a9 to 3f290d8 Compare September 23, 2018 21:45
@dilijev dilijev added External Contributor Bytecode-Update This PR updates bytecode and will cause merge conflicts with other PRs with this label labels Sep 24, 2018
while (position < length) {
const left = position;
let right = position + bucketSize;
right < length ? right : length;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does this line do?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That looks like a no-op; it evaluates to either right; or length; which will have no effect. Remnant from refactoring maybe?

Copy link
Collaborator Author

@rhuanjl rhuanjl Sep 24, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without this you walk off the end of the array unless its length is a power of 2. Perhaps I should add comments explaining the algorithm?

Example - array of length 7:

  1. step 1 - the first loop does pairs:
    0-1, 2-3, 4-5 (you'll note that it actually runs with i = 1 -> i < length always looking at i and i-1 so no risk of walking off the array)

  2. step 2 (now in this loop) - bucket size = 4:
    0-4, 5-8
    BUT if you run with right = 8 you would access array[7] which doesn't exist - this condition limits it so you look at 5-7 instead which is fine.

  3. step 3 - bucket size = 8:
    0-8
    Again you need right capped at length which is 7.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thing is, you have a bare ternary here:

// this is the entire statement
right < length ? right : length;

Which is basically the same thing as writing

0;

Did you mean to assign the result of the ternary to something?

Copy link
Collaborator Author

@rhuanjl rhuanjl Sep 24, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok yes it's an error It's meant to be: right = right < length ? right : length;

I'm confused that this doesn't a fail a test with this error here. I'm either covering the same case with a condition somewhere else - which I cannot currently see or there's no array in a test case with a length that isn't a power of 2.

EDIT: OK yes this error introduces a bug not currently caught by a test case:

let arr = [0, 1, 2, 3, 4, 7, 9];

Object.defineProperty(Array.prototype, 7, { get: function () { print ("SURPRISE"); return 5;}});

print (arr.sort((a, b) => b - a));

"SURPRISE" will be printed and the value 5 will be added into the array - I guess it's an odd case unique to an incorrect implementation of merge sort - I'll obviously update the code - unsure if it's worth adding a test case?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm hoping that fixing this issue will also improve performance, because indexing out of bounds on arrays is very slow, and putting undefined inputs into the comparator function is likely deoptimizing it.

Copy link
Collaborator Author

@rhuanjl rhuanjl Sep 24, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed and I've simplified some of the related logic a little.

My tests don't show any consistent improvement for fixing this - though I imagine that numerous sorts of small arrays would show an improvement whereas I have been testing with sorting just 6 large arrays.

@rhuanjl
Copy link
Collaborator Author

rhuanjl commented Sep 25, 2018

Looking at ways to make this faster - around 10% of runtime is spent in arrayCreateDataPropertyOrThrow which is only necessary to avoid hitting setters or read only values placed in the array prototype chain - are there any alternatives I could use to do that?

@sethbrenith
Copy link
Contributor

You mentioned that you've tried CreateObject(null) to avoid the prototype issues; have you also tried removing the prototypes from the temporary arrays, like const arr = []; arr.__proto__ = null;? This would let you safely write to any array index with the [] operator, but I don't know if it would cause some other slowdown.

@fatcerberus
Copy link
Contributor

fatcerberus commented Sep 25, 2018

I can verify experimentally that stripping the prototype from an array represents a 20-25% speedup for writing new entries into that selfsame array--and a much smaller but still measurable boost for reading.

Test code (run in miniSphere using spherun -p):

let n = 1e7;

let a = [];
let b = [];
b.__proto__ = null;

SSj.instrument(() => {
	for (let i = 0; i < n; ++i)
		a[i] = 812;
}, "10m x WRITE a[]")();

SSj.instrument(() => {
	for (let i = 0; i < n; ++i)
		a[i];
}, "10m x READ  a[]")();

SSj.instrument(() => {
	for (let i = 0; i < n; ++i)
		b[i] = 812;
}, "10m x WRITE b[]")();

SSj.instrument(() => {
	for (let i = 0; i < n; ++i)
		b[i];
}, "10m x READ  b[]")();

Results (i7-6700HQ):

miniSphere X.X.X JS game engine (x64)
a lightweight JavaScript-powered game engine
(c) 2015-2018 Fat Cerberus


=================================
 performance report - 100.0% LF |
===============================================================
 event            count  time (us)   % run  avg (us)    % avg |
---------------------------------------------------------------
 10m x WRITE a[]      1     99,934  53.0 %    99,934   53.3 % |
 10m x WRITE b[]      1     73,539  39.0 %    73,539   39.2 % |
 10m x READ  a[]      1      7,419   3.9 %     7,419    4.0 % |
 10m x READ  b[]      1      6,654   3.5 %     6,654    3.5 % |
---------------------------------------------------------------
 TOTAL                4    187,549  99.5 %   187,549  100.0 % |
---------------------------------------------------------------

@fatcerberus
Copy link
Contributor

On further testing, it turns out that the pattern above reverses if the arrays are pre-allocated using new Array(size); in that case the version with its prototype intact is significantly faster on write and the gap for reading disappears completely.

@sethbrenith
Copy link
Contributor

Interesting, thanks @fatcerberus. I think builtins can use arraySpeciesCreate(undefined, size) to specify a size, like new Array(size), but it still needs to either strip the prototype or use arrayCreateDataPropertyOrThrow when writing in holes.


// perform a merge but only if it's necessary
if (mid < length && compareArray[mid] < compareArray[mid - 1]) {
right = right < length ? right : length;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I think it would be a little cleaner to move let right = position + bucketSize down here so all the right computation is visually together and the right variable is scoped to the smallest block that uses it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Contributor

@sethbrenith sethbrenith left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

@sigatrev
Copy link
Contributor

unless/until we implement a JIT fast-path for arrayCreateDataPropertyOrThrow it is going to be dramatically slower than normal array assignment. I think it would be fine to remove the prototypes from the local temporary arrays and use normal array assignment unless there is some other reason not to do so.

Regarding insertion sort - I've heard of sort implementations that use mergeSort/quickSort at large scale, and insertion sort segments once they drop below some predetermined size. I'm not sure it'd be worth either the added complexity or the effort to test, but I would be curious to see what the results would be given that you've already confirmed insertion sort being faster for smaller arrays.

@rhuanjl
Copy link
Collaborator Author

rhuanjl commented Sep 25, 2018

Thanks for the feedback so far.

  1. I've updated to use __proto__ = null and get rid of the arrayCreateDataPropertyOrThrow calls - this resulted in a decent speed improvement (10-15% for my number sorting test cases).

  2. I've experimented with putting in an insertion sort to reduce the number of merges but have not detected any measurable speed improvement from doing so - there is likely more room to experiment here though.

  3. I've tried a couple of ideas for optimising the string comparisons without success:

    • a Js method looping through string.charCodeAt (aware it would create a problem with if it was overwritten - but wanted to see if it was a path worth pursuing) was basically the same speed as using lessThan so no good.
    • A native method specifically for doing rightString < leftString that returned a bool - again about the same speed as JsLessThan - was sure this should be faster but I guess all the function calls added up
    • beginning to run out of ideas and wondering if a native sort should be retained when using string comparisons (whilst this sort is comparable to current performance when using a numerical comparison or the like with the default string comparison it takes more than twice as long as the current sort)
  4. I'd like to look at adding an opposite direction merge or some equivalent for the case where the length of the array is significantly less than the nearest power of two above it - as currently this case results in extending the buffer to more than half the length of the array.

@LouisLaf
Copy link
Collaborator

The main problem we have with JS built-ins right now is that we generate poor code if the built-in is not inlined and uses different type of objects. The JIT'd code for these needs to be generic enough to allow all of these different objects (or arrays in this case) and the resulting code is slow. In your perf test, you have int and float arrays. We can generate ok code for this, but if you call sort on strings or objects after that, the code will degrade.

We've avoided this for now by making sure all built-ins are inlined, and assuming the types are stable at the call-site most of the times. This also has the benefit of getting callback functions inlined (note that it probably was in your case because all call-sites had the same compare function).

Insertion sort is a lot smaller than merge sort and could probably be inlined, getting all these benefits.

As for the default compare case, I'm assuming people using it are passing an array of strings. We could handle this by optimizing toString() calls on strings, and avoid the extra code bloat.

@rhuanjl rhuanjl force-pushed the mergeSort branch 2 times, most recently from 2a57d9e to 1d340d0 Compare September 27, 2018 22:24
@rhuanjl
Copy link
Collaborator Author

rhuanjl commented Sep 27, 2018

I have made the following updates based on points above:

  1. Added an insertion sort directly in the Array.prototype.sort function to be used when the length is < 2048. I'm hoping this is nicer to inline than the full merge sort and a few tests with -trace:inline suggests that it does inline consistently (though normally it inlines, bails out due to no profile then inlines again after which it's fine to keep running). (Note with lengths much above 2000 it starts being too slow to be a good idea even if inlined)

  2. Removed the duplication of the merge sort - for now I have done this by making an array of objects that have .value and .string properties when using the default sort - this allows me to largely retain the optimisation of toString'ing in advance without all the duplication. (my thought was that if/when a faster path for toString was added this doing it in advance step could be removed but for now doing it this away avoids all the code duplication without sacrificing the performance gain)

  3. Removed the special case handling of pairs from my mergeSort implementation - this was a tiny optimisation that was barely measurable and is less relevant when shorter arrays will now be insertion sorted. Additionally it increased the size/complexity of the mergeSort function.

I assume this could do with some more serious/robust performance testing and so remains not mergeable as is. Additionally if anyone uses the default sort method there really needs to be a faster path for the stringA < stringB operation.

Please let me know if you have any other feedback/points for me to update for or alternatively if this is a bad time for this and I should leave it/close it.

let j = 0, k = 0, l = 0;
while (i < length) {
const item = compArray[i];
l = i;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that l comes after ijk, but it's also really hard to read. Could you use a different name please?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, I would prefer more descriptive names too, like i->numSorted, j->lowerBound, k->insertionPoint, l->upperBound.


In reply to: 221307258 [](ancestors = 221307258)

// Test 1000 random arrays of 1000 elements, print out the failures.
stressTestSort(1000, 1000);
// Test 1000 random arrays of 3000 elements, print out the failures.
stressTestSort(1000, 3000);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to see this test expanded to cover all of the cases where our behavior could vary: longer and shorter than the insertion/merge cutoff point, numbers and objects and strings, with and without comparators. For the object cases, part of the verification could be checking that the output was stable. We might have to knock down the iteration count to 100 or so to keep the test quick enough, but I think this could provide a good level of validation.

let value;
while (i < length) {
value = array[i];
compArray[i++] = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would we gain any noticeable improvement on arrays of strings using default comparison if we tried to detect that case and avoided creating these extra objects? It would mean an extra walk through the array up-front doing typeof checks, but I imagine that would still be faster than allocating all of these temporary objects.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But would typeof be detectable e.g. via a proxy - per spec array.prototype.sort doesn’t use typeof. I suppose I could make a native method that does this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t think typeof can trigger any side effects but I’d have to double-check the specification to be sure.

@rhuanjl
Copy link
Collaborator Author

rhuanjl commented Sep 30, 2018

I’ve pushed an update to:

  1. Specifically handle default sorts of string arrays to avoid unnecessary string conversions and object allocations
  2. Update tests:
    a) cleaned up and collated existing test cases to make what was being tested clearer
    b) Marked the qsort specific tests with -JsBuiltin- as these tests are intended for the xplat qsort only
    c) revised the qsort random test to test merge sort and insertion sort including the stability of these sorts. Note this test case wasn’t previously being run at all as it’s file name was wrong in the rl.xml file

@rhuanjl
Copy link
Collaborator Author

rhuanjl commented Oct 4, 2018

I've added a further update to reverse the merge direction - this reduces the memory requirement and number of operations for sorts of arrays with lengths that are not powers of 2.

My only remaining concern is the performance in the following 3 cases:

  1. nojit - when the jit is disabled this sort is very slow compared with a native equivalent (as can be seen with the timing out test cases causing the CI failure)
  2. default comparison - as already discussed handling of the default comparison is slow
  3. cases where a sort is called only once or twice by a piece of a code but on a large array - in such cases using this jsbuiltin makes it noticeably slower than a native equivalent (this has a measurable impact on some of the benchmarks in Kraken and Octane)

Possible solutions:

  1. Accept it as is (and revise test/disable for nojit)
  2. Introduce a native a fall back for each of the 3 cases above (or perhaps 1 or 2 of them e.g. the nojit case)
  3. Scrap this and have a native stable sort for all cases instead

My preference is option 2 as I think it will have the best overall performance + obviously will use the code I've written - I'm happy to write the additional code for this option, obviously the downside is that it will increase the complexity of the sort implementation as there will be native and Js versions.

@dilijev
Copy link
Contributor

dilijev commented Oct 9, 2018

Perf is not usually a target concern for NoJIT, so I think it would be "fine" to take the perf hit in that case and just disable the tests in NoJIT, and that would at least get this PR completed. Then we can follow up with a native fallback and detect the cases where it should be used.

@rhuanjl rhuanjl force-pushed the mergeSort branch 3 times, most recently from d9549b5 to ae8d6aa Compare October 10, 2018 06:20
@rhuanjl
Copy link
Collaborator Author

rhuanjl commented Oct 28, 2018

Aside from the merge conflicts which should be fairly easy to solve is this blocked based on anything else? Performance questions perhaps?

Please let me know if there's anything further I should do on this.


for (let i = 1; i < size; ++i)
{
if (array[i-1].value > array[i].value)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also check that if array[i-1] === array[i], then array[i-1].index < array[i].index (to verify that stability is maintained while moving things around).

{
if (sorted[i-1] > sorted[i])
{
throw new Error ("Default sort has not sorted by strings");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The output of a randomized test is only actionable if it contains the input array data. The case on line 82 currently prints out the input array, but we need to extend that pattern to apply to all of the test cases. We could JSON.stringify the array to get nice printable data for the object test case.

});

platform.registerChakraLibraryFunction("CreateCompareArray", function(array, length) {
let useCompareArray = false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: use a ;

@sethbrenith
Copy link
Contributor

Thanks for your patience, and great work on this @rhuanjl! I have a few minor comments, but otherwise this looks good to me. Once those comments are addressed, I'll merge the change unless somebody else says not to within two days.

Benchmark results on my machine (test-pogo-x64), for whoever is curious:

Kraken                            Left run time      Right run time     ∆ Run time  ∆ Run time %  Comment
--------------------------------  -----------------  -----------------  ----------  ------------  ---------------
Ai-astar                           362.00 ms ±0.84%   359.67 ms ±0.40%    -2.33 ms        -0.64%
Audio-beat-detection               148.40 ms ±0.59%   149.00 ms ±0.39%     0.60 ms         0.40%
Audio-dft                          131.00 ms ±1.00%   132.33 ms ±0.50%     1.33 ms         1.02%
Audio-fft                          115.67 ms ±0.99%   115.50 ms ±0.90%    -0.17 ms        -0.14%
Audio-oscillator                   165.75 ms ±0.87%   165.43 ms ±0.96%    -0.32 ms        -0.19%
Imaging-darkroom                   187.22 ms ±0.63%   186.60 ms ±0.95%    -0.62 ms        -0.33%
Imaging-desaturate                 132.33 ms ±0.50%   131.67 ms ±0.91%    -0.67 ms        -0.50%
Imaging-gaussian-blur              153.20 ms ±0.38%   154.25 ms ±0.41%     1.05 ms         0.69%
Json-parse-financial                65.81 ms ±0.97%    66.95 ms ±1.22%     1.14 ms         1.73%
Json-stringify-tinderbox            36.45 ms ±0.93%    36.94 ms ±0.97%     0.48 ms         1.32%
Stanford-crypto-aes                220.50 ms ±0.23%   220.25 ms ±0.82%    -0.25 ms        -0.11%
Stanford-crypto-ccm                149.27 ms ±0.77%   146.82 ms ±0.64%    -2.45 ms        -1.64%
Stanford-crypto-pbkdf2             246.13 ms ±0.25%   235.38 ms ±0.48%   -10.75 ms        -4.37%  Improved
Stanford-crypto-sha256-iterative    78.94 ms ±0.65%    75.94 ms ±1.29%    -3.00 ms        -3.80%  Likely improved
--------------------------------  -----------------  -----------------  ----------  ------------  ---------------
Total                             2192.67 ms ±0.64%  2176.72 ms ±0.69%   -15.96 ms        -0.73%
JetStream      Left score     Right score    ∆ Score  ∆ Score %  Comment
-------------  -------------  -------------  -------  ---------  ---------
Bigfib.cpp     368.82 ±0.14%  368.51 ±0.35%    -0.31     -0.09%
Cdjs            80.74 ±1.21%   75.17 ±0.96%    -5.57     -6.90%  Regressed
Container.cpp  334.10 ±0.59%  331.76 ±0.93%    -2.33     -0.70%
Dry.c          287.51 ±0.76%  290.73 ±0.98%     3.22      1.12%
Float-mm.c     371.38 ±0.19%  372.65 ±0.37%     1.27      0.34%
Gcc-loops.cpp  414.42 ±0.72%  416.95 ±0.38%     2.52      0.61%
Hash-map       130.35 ±0.98%  132.49 ±0.62%     2.14      1.64%
N-body.c       231.40 ±0.99%  231.90 ±0.83%     0.50      0.22%
Quicksort.c    269.76 ±0.60%  268.31 ±0.80%    -1.45     -0.54%
Towers.c       216.93 ±1.18%  222.80 ±1.15%     5.87      2.71%
-------------  -------------  -------------  -------  ---------  ---------
Total          244.93 ±0.74%  244.47 ±0.74%    -0.46     -0.19%
Octane            Left score       Right score      ∆ Score  ∆ Score %
----------------  ---------------  ---------------  -------  ---------
Box2d             22412.56 ±0.99%  22730.25 ±0.76%   317.69      1.42%
Code-load         14001.33 ±0.89%  13971.25 ±0.98%   -30.08     -0.21%
Crypto            21532.67 ±0.40%  21071.17 ±0.92%  -461.50     -2.14%
Deltablue         18458.44 ±1.04%  17998.20 ±1.00%  -460.24     -2.49%
Earley-boyer      30655.00 ±0.97%  30641.25 ±0.89%   -13.75     -0.04%
Gbemu             32642.25 ±0.36%  32544.57 ±0.91%   -97.68     -0.30%
Mandreel          17052.89 ±0.71%  16699.57 ±0.85%  -353.32     -2.07%
Mandreel latency  55269.25 ±0.91%  54430.20 ±0.56%  -839.05     -1.52%
Navier-stokes     29235.50 ±0.84%  29700.00 ±0.64%   464.50      1.59%
Pdfjs             12855.71 ±0.49%  12991.80 ±0.43%   136.09      1.06%
Raytrace          29092.82 ±1.98%  28291.75 ±1.19%  -801.07     -2.75%
Regexp             3616.86 ±0.70%   3630.80 ±0.54%    13.94      0.39%
Richards          16775.67 ±0.63%  16628.00 ±0.97%  -147.67     -0.88%
Splay             17055.70 ±0.78%  16756.86 ±0.71%  -298.84     -1.75%
Splay latency     30632.67 ±0.35%  31035.33 ±0.53%   402.67      1.31%
Typescript        28648.50 ±0.90%  28578.20 ±0.94%   -70.30     -0.25%
Zlib              71004.80 ±0.85%  70689.33 ±0.70%  -315.47     -0.44%
----------------  ---------------  ---------------  -------  ---------
Total             22292.70 ±0.81%  22171.31 ±0.80%  -121.39     -0.54%

@chakrabot chakrabot merged commit 94b481d into chakra-core:master Nov 1, 2018
chakrabot pushed a commit that referenced this pull request Nov 1, 2018
….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
@rhuanjl rhuanjl deleted the mergeSort branch November 1, 2018 19:12
@mathiasbynens
Copy link
Contributor

This doesn’t seem to be fully stable just yet. Test case:

sort-stability.js
const array = [
  { name: 'A00', rating: 2 },
  { name: 'A01', rating: 2 },
  { name: 'A02', rating: 2 },
  { name: 'A03', rating: 2 },
  { name: 'A04', rating: 2 },
  { name: 'A05', rating: 2 },
  { name: 'A06', rating: 2 },
  { name: 'A07', rating: 2 },
  { name: 'A08', rating: 2 },
  { name: 'A09', rating: 2 },
  { name: 'A10', rating: 2 },
  { name: 'A11', rating: 2 },
  { name: 'A12', rating: 2 },
  { name: 'A13', rating: 2 },
  { name: 'A14', rating: 2 },
  { name: 'A15', rating: 2 },
  { name: 'A16', rating: 2 },
  { name: 'A17', rating: 2 },
  { name: 'A18', rating: 2 },
  { name: 'A19', rating: 2 },
  { name: 'A20', rating: 2 },
  { name: 'A21', rating: 2 },
  { name: 'A22', rating: 2 },
  { name: 'A23', rating: 2 },
  { name: 'A24', rating: 2 },
  { name: 'A25', rating: 2 },
  { name: 'A26', rating: 2 },
  { name: 'A27', rating: 2 },
  { name: 'A28', rating: 2 },
  { name: 'A29', rating: 2 },
  { name: 'A30', rating: 2 },
  { name: 'A31', rating: 2 },
  { name: 'A32', rating: 2 },
  { name: 'A33', rating: 2 },
  { name: 'A34', rating: 2 },
  { name: 'A35', rating: 2 },
  { name: 'A36', rating: 2 },
  { name: 'A37', rating: 2 },
  { name: 'A38', rating: 2 },
  { name: 'A39', rating: 2 },
  { name: 'A40', rating: 2 },
  { name: 'A41', rating: 2 },
  { name: 'A42', rating: 2 },
  { name: 'A43', rating: 2 },
  { name: 'A44', rating: 2 },
  { name: 'A45', rating: 2 },
  { name: 'A46', rating: 2 },
  { name: 'B00', rating: 3 },
  { name: 'B01', rating: 3 },
  { name: 'B02', rating: 3 },
  { name: 'B03', rating: 3 },
  { name: 'B04', rating: 3 },
  { name: 'B05', rating: 3 },
  { name: 'B06', rating: 3 },
  { name: 'B07', rating: 3 },
  { name: 'B08', rating: 3 },
  { name: 'B09', rating: 3 },
  { name: 'B10', rating: 3 },
  { name: 'B11', rating: 3 },
  { name: 'B12', rating: 3 },
  { name: 'B13', rating: 3 },
  { name: 'B14', rating: 3 },
  { name: 'B15', rating: 3 },
  { name: 'B16', rating: 3 },
  { name: 'B17', rating: 3 },
  { name: 'B18', rating: 3 },
  { name: 'B19', rating: 3 },
  { name: 'B20', rating: 3 },
  { name: 'B21', rating: 3 },
  { name: 'B22', rating: 3 },
  { name: 'B23', rating: 3 },
  { name: 'B24', rating: 3 },
  { name: 'B25', rating: 3 },
  { name: 'B26', rating: 3 },
  { name: 'B27', rating: 3 },
  { name: 'B28', rating: 3 },
  { name: 'B29', rating: 3 },
  { name: 'B30', rating: 3 },
  { name: 'B31', rating: 3 },
  { name: 'B32', rating: 3 },
  { name: 'B33', rating: 3 },
  { name: 'B34', rating: 3 },
  { name: 'B35', rating: 3 },
  { name: 'B36', rating: 3 },
  { name: 'B37', rating: 3 },
  { name: 'B38', rating: 3 },
  { name: 'B39', rating: 3 },
  { name: 'B40', rating: 3 },
  { name: 'B41', rating: 3 },
  { name: 'B42', rating: 3 },
  { name: 'B43', rating: 3 },
  { name: 'B44', rating: 3 },
  { name: 'B45', rating: 3 },
  { name: 'B46', rating: 3 },
  { name: 'C00', rating: 2 },
  { name: 'C01', rating: 2 },
  { name: 'C02', rating: 2 },
  { name: 'C03', rating: 2 },
  { name: 'C04', rating: 2 },
  { name: 'C05', rating: 2 },
  { name: 'C06', rating: 2 },
  { name: 'C07', rating: 2 },
  { name: 'C08', rating: 2 },
  { name: 'C09', rating: 2 },
  { name: 'C10', rating: 2 },
  { name: 'C11', rating: 2 },
  { name: 'C12', rating: 2 },
  { name: 'C13', rating: 2 },
  { name: 'C14', rating: 2 },
  { name: 'C15', rating: 2 },
  { name: 'C16', rating: 2 },
  { name: 'C17', rating: 2 },
  { name: 'C18', rating: 2 },
  { name: 'C19', rating: 2 },
  { name: 'C20', rating: 2 },
  { name: 'C21', rating: 2 },
  { name: 'C22', rating: 2 },
  { name: 'C23', rating: 2 },
  { name: 'C24', rating: 2 },
  { name: 'C25', rating: 2 },
  { name: 'C26', rating: 2 },
  { name: 'C27', rating: 2 },
  { name: 'C28', rating: 2 },
  { name: 'C29', rating: 2 },
  { name: 'C30', rating: 2 },
  { name: 'C31', rating: 2 },
  { name: 'C32', rating: 2 },
  { name: 'C33', rating: 2 },
  { name: 'C34', rating: 2 },
  { name: 'C35', rating: 2 },
  { name: 'C36', rating: 2 },
  { name: 'C37', rating: 2 },
  { name: 'C38', rating: 2 },
  { name: 'C39', rating: 2 },
  { name: 'C40', rating: 2 },
  { name: 'C41', rating: 2 },
  { name: 'C42', rating: 2 },
  { name: 'C43', rating: 2 },
  { name: 'C44', rating: 2 },
  { name: 'C45', rating: 2 },
  { name: 'C46', rating: 2 },
  { name: 'D00', rating: 4 },
  { name: 'D01', rating: 4 },
  { name: 'D02', rating: 4 },
  { name: 'D03', rating: 4 },
  { name: 'D04', rating: 4 },
  { name: 'D05', rating: 4 },
  { name: 'D06', rating: 4 },
  { name: 'D07', rating: 4 },
  { name: 'D08', rating: 4 },
  { name: 'D09', rating: 4 },
  { name: 'D10', rating: 4 },
  { name: 'D11', rating: 4 },
  { name: 'D12', rating: 4 },
  { name: 'D13', rating: 4 },
  { name: 'D14', rating: 4 },
  { name: 'D15', rating: 4 },
  { name: 'D16', rating: 4 },
  { name: 'D17', rating: 4 },
  { name: 'D18', rating: 4 },
  { name: 'D19', rating: 4 },
  { name: 'D20', rating: 4 },
  { name: 'D21', rating: 4 },
  { name: 'D22', rating: 4 },
  { name: 'D23', rating: 4 },
  { name: 'D24', rating: 4 },
  { name: 'D25', rating: 4 },
  { name: 'D26', rating: 4 },
  { name: 'D27', rating: 4 },
  { name: 'D28', rating: 4 },
  { name: 'D29', rating: 4 },
  { name: 'D30', rating: 4 },
  { name: 'D31', rating: 4 },
  { name: 'D32', rating: 4 },
  { name: 'D33', rating: 4 },
  { name: 'D34', rating: 4 },
  { name: 'D35', rating: 4 },
  { name: 'D36', rating: 4 },
  { name: 'D37', rating: 4 },
  { name: 'D38', rating: 4 },
  { name: 'D39', rating: 4 },
  { name: 'D40', rating: 4 },
  { name: 'D41', rating: 4 },
  { name: 'D42', rating: 4 },
  { name: 'D43', rating: 4 },
  { name: 'D44', rating: 4 },
  { name: 'D45', rating: 4 },
  { name: 'D46', rating: 4 },
  { name: 'E00', rating: 3 },
  { name: 'E01', rating: 3 },
  { name: 'E02', rating: 3 },
  { name: 'E03', rating: 3 },
  { name: 'E04', rating: 3 },
  { name: 'E05', rating: 3 },
  { name: 'E06', rating: 3 },
  { name: 'E07', rating: 3 },
  { name: 'E08', rating: 3 },
  { name: 'E09', rating: 3 },
  { name: 'E10', rating: 3 },
  { name: 'E11', rating: 3 },
  { name: 'E12', rating: 3 },
  { name: 'E13', rating: 3 },
  { name: 'E14', rating: 3 },
  { name: 'E15', rating: 3 },
  { name: 'E16', rating: 3 },
  { name: 'E17', rating: 3 },
  { name: 'E18', rating: 3 },
  { name: 'E19', rating: 3 },
  { name: 'E20', rating: 3 },
  { name: 'E21', rating: 3 },
  { name: 'E22', rating: 3 },
  { name: 'E23', rating: 3 },
  { name: 'E24', rating: 3 },
  { name: 'E25', rating: 3 },
  { name: 'E26', rating: 3 },
  { name: 'E27', rating: 3 },
  { name: 'E28', rating: 3 },
  { name: 'E29', rating: 3 },
  { name: 'E30', rating: 3 },
  { name: 'E31', rating: 3 },
  { name: 'E32', rating: 3 },
  { name: 'E33', rating: 3 },
  { name: 'E34', rating: 3 },
  { name: 'E35', rating: 3 },
  { name: 'E36', rating: 3 },
  { name: 'E37', rating: 3 },
  { name: 'E38', rating: 3 },
  { name: 'E39', rating: 3 },
  { name: 'E40', rating: 3 },
  { name: 'E41', rating: 3 },
  { name: 'E42', rating: 3 },
  { name: 'E43', rating: 3 },
  { name: 'E44', rating: 3 },
  { name: 'E45', rating: 3 },
  { name: 'E46', rating: 3 },
  { name: 'F00', rating: 3 },
  { name: 'F01', rating: 3 },
  { name: 'F02', rating: 3 },
  { name: 'F03', rating: 3 },
  { name: 'F04', rating: 3 },
  { name: 'F05', rating: 3 },
  { name: 'F06', rating: 3 },
  { name: 'F07', rating: 3 },
  { name: 'F08', rating: 3 },
  { name: 'F09', rating: 3 },
  { name: 'F10', rating: 3 },
  { name: 'F11', rating: 3 },
  { name: 'F12', rating: 3 },
  { name: 'F13', rating: 3 },
  { name: 'F14', rating: 3 },
  { name: 'F15', rating: 3 },
  { name: 'F16', rating: 3 },
  { name: 'F17', rating: 3 },
  { name: 'F18', rating: 3 },
  { name: 'F19', rating: 3 },
  { name: 'F20', rating: 3 },
  { name: 'F21', rating: 3 },
  { name: 'F22', rating: 3 },
  { name: 'F23', rating: 3 },
  { name: 'F24', rating: 3 },
  { name: 'F25', rating: 3 },
  { name: 'F26', rating: 3 },
  { name: 'F27', rating: 3 },
  { name: 'F28', rating: 3 },
  { name: 'F29', rating: 3 },
  { name: 'F30', rating: 3 },
  { name: 'F31', rating: 3 },
  { name: 'F32', rating: 3 },
  { name: 'F33', rating: 3 },
  { name: 'F34', rating: 3 },
  { name: 'F35', rating: 3 },
  { name: 'F36', rating: 3 },
  { name: 'F37', rating: 3 },
  { name: 'F38', rating: 3 },
  { name: 'F39', rating: 3 },
  { name: 'F40', rating: 3 },
  { name: 'F41', rating: 3 },
  { name: 'F42', rating: 3 },
  { name: 'F43', rating: 3 },
  { name: 'F44', rating: 3 },
  { name: 'F45', rating: 3 },
  { name: 'F46', rating: 3 },
  { name: 'G00', rating: 4 },
  { name: 'G01', rating: 4 },
  { name: 'G02', rating: 4 },
  { name: 'G03', rating: 4 },
  { name: 'G04', rating: 4 },
  { name: 'G05', rating: 4 },
  { name: 'G06', rating: 4 },
  { name: 'G07', rating: 4 },
  { name: 'G08', rating: 4 },
  { name: 'G09', rating: 4 },
  { name: 'G10', rating: 4 },
  { name: 'G11', rating: 4 },
  { name: 'G12', rating: 4 },
  { name: 'G13', rating: 4 },
  { name: 'G14', rating: 4 },
  { name: 'G15', rating: 4 },
  { name: 'G16', rating: 4 },
  { name: 'G17', rating: 4 },
  { name: 'G18', rating: 4 },
  { name: 'G19', rating: 4 },
  { name: 'G20', rating: 4 },
  { name: 'G21', rating: 4 },
  { name: 'G22', rating: 4 },
  { name: 'G23', rating: 4 },
  { name: 'G24', rating: 4 },
  { name: 'G25', rating: 4 },
  { name: 'G26', rating: 4 },
  { name: 'G27', rating: 4 },
  { name: 'G28', rating: 4 },
  { name: 'G29', rating: 4 },
  { name: 'G30', rating: 4 },
  { name: 'G31', rating: 4 },
  { name: 'G32', rating: 4 },
  { name: 'G33', rating: 4 },
  { name: 'G34', rating: 4 },
  { name: 'G35', rating: 4 },
  { name: 'G36', rating: 4 },
  { name: 'G37', rating: 4 },
  { name: 'G38', rating: 4 },
  { name: 'G39', rating: 4 },
  { name: 'G40', rating: 4 },
  { name: 'G41', rating: 4 },
  { name: 'G42', rating: 4 },
  { name: 'G43', rating: 4 },
  { name: 'G44', rating: 4 },
  { name: 'G45', rating: 4 },
  { name: 'G46', rating: 4 },
  { name: 'H00', rating: 3 },
  { name: 'H01', rating: 3 },
  { name: 'H02', rating: 3 },
  { name: 'H03', rating: 3 },
  { name: 'H04', rating: 3 },
  { name: 'H05', rating: 3 },
  { name: 'H06', rating: 3 },
  { name: 'H07', rating: 3 },
  { name: 'H08', rating: 3 },
  { name: 'H09', rating: 3 },
  { name: 'H10', rating: 3 },
  { name: 'H11', rating: 3 },
  { name: 'H12', rating: 3 },
  { name: 'H13', rating: 3 },
  { name: 'H14', rating: 3 },
  { name: 'H15', rating: 3 },
  { name: 'H16', rating: 3 },
  { name: 'H17', rating: 3 },
  { name: 'H18', rating: 3 },
  { name: 'H19', rating: 3 },
  { name: 'H20', rating: 3 },
  { name: 'H21', rating: 3 },
  { name: 'H22', rating: 3 },
  { name: 'H23', rating: 3 },
  { name: 'H24', rating: 3 },
  { name: 'H25', rating: 3 },
  { name: 'H26', rating: 3 },
  { name: 'H27', rating: 3 },
  { name: 'H28', rating: 3 },
  { name: 'H29', rating: 3 },
  { name: 'H30', rating: 3 },
  { name: 'H31', rating: 3 },
  { name: 'H32', rating: 3 },
  { name: 'H33', rating: 3 },
  { name: 'H34', rating: 3 },
  { name: 'H35', rating: 3 },
  { name: 'H36', rating: 3 },
  { name: 'H37', rating: 3 },
  { name: 'H38', rating: 3 },
  { name: 'H39', rating: 3 },
  { name: 'H40', rating: 3 },
  { name: 'H41', rating: 3 },
  { name: 'H42', rating: 3 },
  { name: 'H43', rating: 3 },
  { name: 'H44', rating: 3 },
  { name: 'H45', rating: 3 },
  { name: 'H46', rating: 3 },
  { name: 'I00', rating: 2 },
  { name: 'I01', rating: 2 },
  { name: 'I02', rating: 2 },
  { name: 'I03', rating: 2 },
  { name: 'I04', rating: 2 },
  { name: 'I05', rating: 2 },
  { name: 'I06', rating: 2 },
  { name: 'I07', rating: 2 },
  { name: 'I08', rating: 2 },
  { name: 'I09', rating: 2 },
  { name: 'I10', rating: 2 },
  { name: 'I11', rating: 2 },
  { name: 'I12', rating: 2 },
  { name: 'I13', rating: 2 },
  { name: 'I14', rating: 2 },
  { name: 'I15', rating: 2 },
  { name: 'I16', rating: 2 },
  { name: 'I17', rating: 2 },
  { name: 'I18', rating: 2 },
  { name: 'I19', rating: 2 },
  { name: 'I20', rating: 2 },
  { name: 'I21', rating: 2 },
  { name: 'I22', rating: 2 },
  { name: 'I23', rating: 2 },
  { name: 'I24', rating: 2 },
  { name: 'I25', rating: 2 },
  { name: 'I26', rating: 2 },
  { name: 'I27', rating: 2 },
  { name: 'I28', rating: 2 },
  { name: 'I29', rating: 2 },
  { name: 'I30', rating: 2 },
  { name: 'I31', rating: 2 },
  { name: 'I32', rating: 2 },
  { name: 'I33', rating: 2 },
  { name: 'I34', rating: 2 },
  { name: 'I35', rating: 2 },
  { name: 'I36', rating: 2 },
  { name: 'I37', rating: 2 },
  { name: 'I38', rating: 2 },
  { name: 'I39', rating: 2 },
  { name: 'I40', rating: 2 },
  { name: 'I41', rating: 2 },
  { name: 'I42', rating: 2 },
  { name: 'I43', rating: 2 },
  { name: 'I44', rating: 2 },
  { name: 'I45', rating: 2 },
  { name: 'I46', rating: 2 },
  { name: 'J00', rating: 2 },
  { name: 'J01', rating: 2 },
  { name: 'J02', rating: 2 },
  { name: 'J03', rating: 2 },
  { name: 'J04', rating: 2 },
  { name: 'J05', rating: 2 },
  { name: 'J06', rating: 2 },
  { name: 'J07', rating: 2 },
  { name: 'J08', rating: 2 },
  { name: 'J09', rating: 2 },
  { name: 'J10', rating: 2 },
  { name: 'J11', rating: 2 },
  { name: 'J12', rating: 2 },
  { name: 'J13', rating: 2 },
  { name: 'J14', rating: 2 },
  { name: 'J15', rating: 2 },
  { name: 'J16', rating: 2 },
  { name: 'J17', rating: 2 },
  { name: 'J18', rating: 2 },
  { name: 'J19', rating: 2 },
  { name: 'J20', rating: 2 },
  { name: 'J21', rating: 2 },
  { name: 'J22', rating: 2 },
  { name: 'J23', rating: 2 },
  { name: 'J24', rating: 2 },
  { name: 'J25', rating: 2 },
  { name: 'J26', rating: 2 },
  { name: 'J27', rating: 2 },
  { name: 'J28', rating: 2 },
  { name: 'J29', rating: 2 },
  { name: 'J30', rating: 2 },
  { name: 'J31', rating: 2 },
  { name: 'J32', rating: 2 },
  { name: 'J33', rating: 2 },
  { name: 'J34', rating: 2 },
  { name: 'J35', rating: 2 },
  { name: 'J36', rating: 2 },
  { name: 'J37', rating: 2 },
  { name: 'J38', rating: 2 },
  { name: 'J39', rating: 2 },
  { name: 'J40', rating: 2 },
  { name: 'J41', rating: 2 },
  { name: 'J42', rating: 2 },
  { name: 'J43', rating: 2 },
  { name: 'J44', rating: 2 },
  { name: 'J45', rating: 2 },
  { name: 'J46', rating: 2 },
  { name: 'K00', rating: 2 },
  { name: 'K01', rating: 2 },
  { name: 'K02', rating: 2 },
  { name: 'K03', rating: 2 },
  { name: 'K04', rating: 2 },
  { name: 'K05', rating: 2 },
  { name: 'K06', rating: 2 },
  { name: 'K07', rating: 2 },
  { name: 'K08', rating: 2 },
  { name: 'K09', rating: 2 },
  { name: 'K10', rating: 2 },
  { name: 'K11', rating: 2 },
  { name: 'K12', rating: 2 },
  { name: 'K13', rating: 2 },
  { name: 'K14', rating: 2 },
  { name: 'K15', rating: 2 },
  { name: 'K16', rating: 2 },
  { name: 'K17', rating: 2 },
  { name: 'K18', rating: 2 },
  { name: 'K19', rating: 2 },
  { name: 'K20', rating: 2 },
  { name: 'K21', rating: 2 },
  { name: 'K22', rating: 2 },
  { name: 'K23', rating: 2 },
  { name: 'K24', rating: 2 },
  { name: 'K25', rating: 2 },
  { name: 'K26', rating: 2 },
  { name: 'K27', rating: 2 },
  { name: 'K28', rating: 2 },
  { name: 'K29', rating: 2 },
  { name: 'K30', rating: 2 },
  { name: 'K31', rating: 2 },
  { name: 'K32', rating: 2 },
  { name: 'K33', rating: 2 },
  { name: 'K34', rating: 2 },
  { name: 'K35', rating: 2 },
  { name: 'K36', rating: 2 },
  { name: 'K37', rating: 2 },
  { name: 'K38', rating: 2 },
  { name: 'K39', rating: 2 },
  { name: 'K40', rating: 2 },
  { name: 'K41', rating: 2 },
  { name: 'K42', rating: 2 },
];

// Sort the elements by `rating` in descending order.
// (This updates `array` in place.)
array.sort((a, b) => b.rating - a.rating);

const reduced = array.reduce((acc, element) => {
	const letter = element.name.slice(0, 1);
	const previousLetter = acc.slice(-1);
	if (previousLetter === letter) {
		return acc;
	}
	return acc + letter;
}, '');
print(reduced);
const isProbablyStable = reduced === 'DGBEFHACIJK';
print(isProbablyStable);

Results

$ spidermonkey sort-stability.js
DGBEFHACIJK
true

$ javascriptcore sort-stability.js
DGBEFHACIJK
true

$ xs -s sort-stability.js
DGBEFHACIJK
true

$ v8 sort-stability.js
DGBEFHACIJK
true

$ chakra sort-stability.js
GDGDGDGDGBEHEFEHFHFHFHFHFHFHACAIJK
false

$ chakra -v
ch version 1.11.3.0

@rhuanjl
Copy link
Collaborator Author

rhuanjl commented Nov 29, 2018

@mathiasbynens this is still only in the master branch of chakra - it's not in 1.11

(Note, I'm an external contributor and do not know if or when the CC team are planning to include this in a release)

@mathiasbynens
Copy link
Contributor

@rhuanjl Ah, great! I saw the release being cut days after this PR was merged, and assumed the patch was part of the release.

@fatcerberus
Copy link
Contributor

Semver-patch releases don’t cut from master - they are bug fixes only.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bytecode-Update This PR updates bytecode and will cause merge conflicts with other PRs with this label External Contributor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: Array prototype values copied to non-array when using sort Stable sort for Array#sort.