Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Array.prototype.sort isn't stable #212

Closed
SinLucifer opened this issue Apr 14, 2020 · 32 comments
Closed

Array.prototype.sort isn't stable #212

SinLucifer opened this issue Apr 14, 2020 · 32 comments
Assignees
Labels
bug Something isn't working good first issue Good for newcomers help wanted Extra attention is needed

Comments

@SinLucifer
Copy link

Hermes version: 0.2.1 ~ 0.4.1

I noticed the issues about Infinite loop when sorting , and find two others problem about Array.sort with a comparator.

  1. The sort result will not base on input order.
let array = [
    { "index": 0, "key": 1 }, 
    { "index": 1, "key": 1 }, 
    { "index": 2, "key": 1 }, 
    { "index": 3, "key": 1 }, 
    { "index": 4, "key": 2 }, 
    { "index": 5, "key": 1 }, 
    { "index": 6, "key": 1 },
    { "index": 7, "key": 1 }];

array.sort((item1, item2) => {
    return item1.key - item2.key
})

console.log(array);

and get result in hermes:

[{"index":0,"key":1},
{"index":5,"key":1},
{"index":7,"key":1},
{"index":6,"key":1},
{"index":3,"key":1},
{"index":1,"key":1},
{"index":2,"key":1},
{"index":4,"key":2}]

which we will get in chrome:

0: {index: 0, key: 1}
1: {index: 1, key: 1}
2: {index: 2, key: 1}
3: {index: 3, key: 1}
4: {index: 5, key: 1}
5: {index: 6, key: 1}
6: {index: 7, key: 1}
7: {index: 4, key: 2}

The order of the objects has been changed by Array.sort, I am not sure if it is a bug.

  1. Array defined in global field and use Array.sort will give different result;
let array = [
    { "index": 0, "key": 1 }, 
    { "index": 1, "key": 1 }, 
    { "index": 2, "key": 1 }, 
    { "index": 3, "key": 1 }, 
    { "index": 4, "key": 2 }, 
    { "index": 5, "key": 1 }, 
    { "index": 6, "key": 1 },
    { "index": 7, "key": 1 }];

class TestSort extends Component {
    render() {
        return (
            <Button title="sort test" onPress={() => {
                array.sort((item1, item2) => {
                    return item1.key - item2.key
                })
                console.log(array);
            }} />
        );
    }
}

first sort result:

//index: 0, 5, 7, 6, 3, 1, 2, 4
[{"index":0,"key":1},
{"index":5,"key":1},
{"index":7,"key":1},
{"index":6,"key":1},
{"index":3,"key":1},
{"index":1,"key":1},
{"index":2,"key":1},
{"index":4,"key":2}]

second sort reslut:

//index: 0, 3, 2, 1, 6, 5, 7, 4
[{"index":0,"key":1},
{"index":3,"key":1},
{"index":2,"key":1},
{"index":1,"key":1},
{"index":6,"key":1},
{"index":5,"key":1},
{"index":7,"key":1},
{"index":4,"key":2}]

Every time I tap the button to get a sort result is different.

In our business code, key may be the weight of the object. The array will be displayed by ListView , because of the uncertain sort results, the display order will be changed and will confuse the user.

Of cause we can fix this by make the comparator more complex, but I don` t think it is a good idea.

BTW, this problem only happen when I define the array in global field, it is fine when I define the array into a function.

@SinLucifer
Copy link
Author

There is more information of second problem:

first sort result:

//index: 0, 5, 7, 6, 3, 1, 2, 4

second sort result:

//index: 0, 3, 2, 1, 6, 5, 7, 4

third sort result:

//index: 0, 6, 7, 5, 1, 3, 7, 4

fourth sort reuslt:

//index: 0, 1, 2, 3, 5, 6, 7, 4 

Next time it will go back to first result.

@dulinriley
Copy link
Contributor

dulinriley commented Apr 14, 2020

I think both of these observed effects are due to the same underlying issue (that was touched upon in the last issue you mentioned #95):

Our current Array.prototype.sort is not stable. A stable sort means that the order of equivalent elements is guaranteed to be preserved.

We have a variety of sorting strategies based on the array:

  • If the array has fewer than INSERTION_THRESHOLD (which is currently 6) elements, use insertion sort, which is a stable sort
  • Else, use quicksort which is unstable
  • If quicksort recurses too many times, use heap sort instead, which is also unstable

If you modify your examples to be 6 elements or fewer, you will notice they will produce the results you expect.

Your second example shows that an unstable sort can produce different results when called multiple times, since it doesn't guarantee that equal keys stay in the same position.

Since tc39 has made a stable Array.prototype.sort now part of the spec (as of tc39/ecma262#1340), we'll need to update our implementation. This is actually a great first issue for someone from the community to dive into, since it's an easily testable change and fairly isolated from more complex parts of the VM.

The only changes that need to be made are in lib/VM/JSLib/Sorting.cpp, and you can use our existing SortModel to handle all the comparator logic.

@dulinriley dulinriley added bug Something isn't working good first issue Good for newcomers help wanted Extra attention is needed labels Apr 14, 2020
@tinfoil-knight
Copy link

@dulinriley Can I try out this issue?

@dulinriley
Copy link
Contributor

dulinriley commented Apr 22, 2020

@tinfoil-knight Sure thing!

Here is some advice to get you started:

  • First, familiarize yourself with our Building and Running guide to run the existing tests
  • Next, choose a stable sorting algorithm to implement. I recommend Timsort, since it's used by both v8 and CPython (see this blog post from v8 on stable sorts)
  • Implement the sort in hermes/lib/VM/JSLib/Sorting.cpp. Use the SortModel to do comparisons between keys, as that will do the right logic for exceptions and other miscellaneous JS stuff
  • Add some tests for sort stability in the file hermes/test/hermes/array-functions.js
  • If test262 has any tests for stable sorts, enable them by removing relevant lines from hermes/utils/testsuite/testsuite_skiplist.py

@dulinriley
Copy link
Contributor

@tinfoil-knight Were you able to try out a stable sort implementation? Do you need any help?

@tinfoil-knight
Copy link

tinfoil-knight commented Jul 29, 2020

@dulinriley No. I tried to read and understand TimSort but I got nowhere. Sorry for not sending out an update. I still have no idea as to how to implement TimSort. I'll update you if I can solve the issue or not.

@tinfoil-knight
Copy link

@dulinriley Unfortunately, I don't think I've enough clarity on multiple things to make changes to the codebase. I find Timsort to be very complex. I'm freeing this issue up. I'm really sorry.

@dulinriley dulinriley changed the title Another problem about Array.prototype.sort Array.prototype.sort isn't stable Aug 10, 2020
@dulinriley
Copy link
Contributor

If Timsort is too difficult, we can use any stable sort for now (such as merge sort) to fix the immediate bug, and come back to optimize it with a more sophisticated algorithm later.

@tinfoil-knight do you want to try with a simpler sorting algorithm instead?

@tinfoil-knight
Copy link

tinfoil-knight commented Aug 11, 2020

No. I don't really know how the library is handling multiple things.

@nairboh
Copy link

nairboh commented Aug 11, 2020

@dulinriley would it be okay if I took a stab at this?

@dulinriley
Copy link
Contributor

Hi @nairboh, we just found someone internally who was interested in working on it.
We'll let you know how that turns out, and if we'll still need any help with this issue

@CoryWritesCode
Copy link

Glad this is being worked on, @dulinriley any word on the internal progress?

@CoryWritesCode
Copy link

For those looking for a fix in the meantime, I was able to make use of lodash.sortBy() to get the sorting I needed.

@dulinriley
Copy link
Contributor

We have a fix written up, and going through review. It'll auto-close this issue when it hits Github.

facebook-github-bot pushed a commit that referenced this issue Sep 1, 2020
Summary:
The implementation of a stable sort in Hermes had a bug in dealing with
comparators that modified the array being sorted.

Reopens issue #212

Reviewed By: tmikov

Differential Revision: D23437337

fbshipit-source-id: b7a0c26ee7e6b171b857561d628c809581b2ed61
@tmikov tmikov self-assigned this Sep 9, 2020
@mikunimaru
Copy link

This issue has recurred in version 0.7.2.

@tmikov
Copy link
Contributor

tmikov commented Dec 31, 2020

@mikunimaru What issue? Without more information, we cannot help.

@mikunimaru
Copy link

Array.prototype.sort is not yet stable sort.

let array = [
    { "index": 0, "key": 1 }, 
    { "index": 1, "key": 1 }, 
    { "index": 2, "key": 1 }, 
    { "index": 3, "key": 1 }, 
    { "index": 4, "key": 2 }, 
    { "index": 5, "key": 1 }, 
    { "index": 6, "key": 1 },
    { "index": 7, "key": 1 }];

array.sort((item1, item2) => {
    return item1.key - item2.key
})

console.log(array);

result (enableHermes: false)
[{"index":0,"key":1},{"index":1,"key":1},{"index":2,"key":1},{"index":3,"key":1},{"index":5,"key":1},{"index":6,"key":1},{"index":7,"key":1},{"index":4,"key":2}]

result (enableHermes: true)
[{"index":0,"key":1},{"index":5,"key":1},{"index":7,"key":1},{"index":6,"key":1},{"index":3,"key":1},{"index":1,"key":1},{"index":2,"key":1},{"index":4,"key":2}]

Since stable sorting is performed only when Hermes is disabled, there is a risk of causing a bug due to this difference in behavior.

Hermes version: 0.7.2
React Native version (if any): 0.64-rc2
Android version (if any): 10
Platform arm64-v8a

@tmikov
Copy link
Contributor

tmikov commented Jan 1, 2021

Ah, yes, Hermes does not yet fully support ES2019, which is the first version of the spec requiring a stable sort. I will reopen the issue. We don't have immediate plans to work on this (there are higher priority tasks to finish first), but contributions are welcome.

@tmikov tmikov reopened this Jan 1, 2021
@rlemasquerier
Copy link

Hello @tmikov, does anyone has any update about this issue ?

I believe it's quiet a major issue, since it prevents us from using the native sort function along with a comparison function as soon as Hermes is enabled, right ?

@mikunimaru Did you find any workaround ?

Would be glad to help but I'm unsure about what needs to be done exactly to make that function stable, any directions ?

Thanks a lot

@tmikov
Copy link
Contributor

tmikov commented Mar 1, 2021

@rlemasquerier someone needs to implement the stable sort in Hermes. Unfortunately it is not trivial, for a few reasons:

  • It needs to terminate even if the comparison function is incorrect
  • It needs to produce the same sequence of calls on all platforms
  • Implementing JS library functionality in C++ in Hermes requires understanding of implementation details like handles, etc.

The first two points mean that the standard C++ library is out. In practice an existing sort routine can't readily be reused, at least not without heavy customization.

The easiest solution probably is to find a stable sort implementation in JS and polyfill it. In fact, if it is suitably licensed, we can include it in Hermes. Though, of course, there will be a performance penalty.

@Prakhar-Srivastava
Copy link

Prakhar-Srivastava commented Apr 24, 2021

Hey guys!
I wanted to ask if the Array.prototype.sort needs to sort an array inplace...?
As in V8 we can do something like:

let x = [8, 4, 9, 1, 0, 4, 3, 6];
const s = Array.prototype.sort.call(x, (a, b) => a - b);

And this would sort the array x inplace while s also has the sorted values of x. I suppose that line 2 copies the elements of x after it is sorted inplace, but just wanted to make sure.

@ljharb
Copy link

ljharb commented Apr 24, 2021

It does not; s and x are the same exact array instance.

@vicky1999
Copy link

Hi, Is this issue available? I would like to work on this issue.

@tmikov
Copy link
Contributor

tmikov commented May 1, 2021

Yes, it is available.

@vicky1999
Copy link

can I work on it?

@Prakhar-Srivastava
Copy link

Prakhar-Srivastava commented May 1, 2021

Hey @vicky1999, I'm working on an implementation (working on my machine locally and will fork the repo when I am done and commit the changes there). I'm trying to implement Timsort (bufferless) taking inspiration from std::inplace_stable_sort. You can take this up if you have something better in mind.

@vicky1999
Copy link

vicky1999 commented May 2, 2021

Hey Guys,
I have implemented the selection sort algorithm. when using selection sort, the sorted array was stable. It was working fine.

Now, I'll try to implement in place merge sort.

@chakrihacker
Copy link

Just curious, timsort is built on top of insertion sort, then why can't we use stable insertion sort?

@Prakhar-Srivastava
Copy link

Just curious, timsort is built on top of insertion sort, then why can't we use stable insertion sort?

Hey!
It's based on merge sort and only uses insertion sort for small arrays and since both of these methods are stable, thus timsort is stable too, and in fact even faster than merge sort or insertion sort alone. And Hermes is about doing things efficiently using an algorithm that gives O(n²) in any case kinda criminal to me 😂

@andreialecu
Copy link

Ran into a quite nasty bug in a third party library after enabling Hermes due to this.

More details in: kylerjensen/react-native-font-faces#289

Was able to fix it in a PR, via the following JS based stable sorting function:

/**
 * The Hermes JS engine does not do a stable sort: https://github.com/facebook/hermes/issues/212
 */
export function stableSort<T>(array: T[], ...compareFns: Array<(a: T, b: T) => number>) {
  return compareFns.reduce((final, compareFn) => {
    return final
      .map((item, index) => ({ item, index }))
      .sort((a, b) => compareFn(a.item, b.item) || a.index - b.index)
      .map(({ item }) => item)
  }, array);
}

You can see the diff in: https://github.com/kylerjensen/react-native-font-faces/pull/290/files

Leaving it here in case it helps someone.

@pdpd123
Copy link

pdpd123 commented Jul 19, 2021

I don't understand why is this not a P0 for Hermes team to fix?
This could potentially hang up the whole application if such a fundamental incompatibility. My team burnt 3 days on this because some(ONLY SOME) of out customer has the whole app frozen due to this but we can't repro (Because you need > 6 items..... )

This is a very nasty low level bug. PLease prioritize. Array.sort is very frequently used

@kodafb
Copy link
Contributor

kodafb commented Oct 6, 2021

Array.prototype.sort is stable as of commit ec424a1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working good first issue Good for newcomers help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests