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

Add intro sort to Array #3514

Merged
merged 3 commits into from Nov 17, 2016
Merged

Add intro sort to Array #3514

merged 3 commits into from Nov 17, 2016

Conversation

c910335
Copy link
Contributor

@c910335 c910335 commented Nov 7, 2016

For most of the developers, performance is one of the most reasons why they choose a programming language.
But how do they know the performance of a programming language?
Usually, they test it by sorting an array because sorting is the most basic, classic and the most important algorithm in the world.
That is what I did these days.
After several tests, I found that Array#sort is usually faster than qsort in C but sometimes slower than std::sort in C++.
However, I believe that Crystal can do better.

Therefore, let me introduce "introspective sort" a.k.a. introsort, the fastest sorting algorithm as far as I know.
Introsort - Wikipedia
According to Wikipedia, Introsort is a hybrid sorting algorithm that consists of quicksort, heapsort and insertion sort.
It combines the good parts of the three algorithms, with fast average performance (quicksort), worst-case O(n log n) runtime (heapsort) and fastest practical performance on an almost sorted array (insertion sort).
Benchmarks

@asterite
Copy link
Member

asterite commented Nov 7, 2016

@c910335 WOW! Thank you so much for this! This is really useful. It will take some time for me or others to review this, but this will most probably be merged.

We originally wrote a naive quicksort implementation in Crystal because we needed ir in order to implement the compiler. We knew we would eventually need to improve its performance, so your pull request is a really nice gift! I didn't know about introsort so I also learned something new today. Thank you!

And, as always, it's really nice to see that idiomatic Crystal code can be competitive and even beat some C/C++ implementations :-)

@sdogruyol
Copy link
Member

@c910335 This is awesome 🎉 ありがとうございます!

@akitaonrails
Copy link

This is nice. Java and many languages use a hybrid approach for sort to avoid the worst cases. Didn't know Crystal used qsort only so this is really a super improvement.

@kostya
Copy link
Contributor

kostya commented Nov 7, 2016

i little modified your bench, there is also some worst cases:
https://gist.github.com/kostya/71342b0718cfa5d5414dde688929d000

======== reversed  16777216 =============
(0.413) 16777216 elements with original quick sort
(1.871) 16777216 elements with intro sort
(0.181) 16777216 elements with std::sort in cpp
(0.923) 16777216 elements with qsort in c

@lbguilherme
Copy link
Contributor

Awesome work!

An improvement suggestion: instead of having a duplicate with or without comp better user a block and pass it along always. The default version would use Array.intra_sort! {|a, b| a < b } and all others could pass it with method! {|a, b| yield(a, b) }. This is as efficient as the current duplication because yields are always inlined

@c910335
Copy link
Contributor Author

c910335 commented Nov 7, 2016

@kostya
Thank you for finding those worst cases.
I have no idea why this implement may be slower than the others in this moment.
However, I have to go to sleep or I'll late for class (I am a college student).
I will looking for the reason of slowness this afternoon of my timezone (+0800).
Thank you again.

@c910335
Copy link
Contributor Author

c910335 commented Nov 7, 2016

I found that the method "shift_median!" breaks the monotonicity of a decreasing array since it shifts the pivot for quick sort to the front while the other sorts take advantage of the monotonicity to do faster.
Therefore, I improved "shift_median!" and renamed it to "center_median!", which moves the pivot to the middle.
Thanks, @kostya.
Benchmarks

@c910335
Copy link
Contributor Author

c910335 commented Nov 10, 2016

I am so sorry.
Since I misunderstood Benchmark.ips, the first benchmark I gave was wrong.
Here is the new result with Benchmark.ips.
Benchmarks
BTW, I think it is good to add some methods like "before" to Benchmark::IPS::Job

@ysbaddaden
Copy link
Contributor

@c910335 impressive results. Fastest of the implementations most of the time, and when it's not, it's almost on par with C++?

@firejox
Copy link
Contributor

firejox commented Nov 16, 2016

@c910335
I found the problem that first value and last value would not exchange in center_median! method in reverse test case.
And now it can beat std::sort!
https://gist.github.com/firejox/4d300495811c1dda65fefc1b76fc57b6

@asterite
Copy link
Member

@c910335 I've reviewed the code and also did some experiments to see if it was working correct and everything's good. Thank you so much for this!! ❤️

@asterite asterite merged commit 53be65d into crystal-lang:master Nov 17, 2016
@asterite asterite added this to the 0.20.0 milestone Nov 17, 2016
@chenkovsky
Copy link

I test it on mac, the benchmark shows that the clang's sort function has better performance.

@lbguilherme
Copy link
Contributor

@chenkovsky By how much?

@chenkovsky
Copy link

@lbguilherme

======== random 2097152 =============
with intro sort 4.94 (202.55ms) (±11.17%) 1.07× slower
with std::sort in cpp 5.29 ( 189.0ms) (± 8.38%) fastest
with qsort in c 2.54 (393.48ms) (± 1.81%) 2.08× slower
======== seq 2097152 =============
with intro sort 47.45 ( 21.07ms) (±12.62%) 5.56× slower
with std::sort in cpp 263.91 ( 3.79ms) (±24.78%) fastest
with qsort in c 81.07 ( 12.33ms) (±11.10%) 3.26× slower
======== rotated_seq 2097152 =============
with intro sort 35.91 ( 27.85ms) (±11.48%) 2.41× slower
with std::sort in cpp 86.42 ( 11.57ms) (±10.04%) fastest
with qsort in c 12.26 ( 81.54ms) (± 4.81%) 7.05× slower
======== reversed 2097152 =============
with intro sort 47.28 ( 21.15ms) (± 7.88%) 5.76× slower
with std::sort in cpp 272.38 ( 3.67ms) (± 9.02%) fastest
with qsort in c 12.81 ( 78.05ms) (± 7.22%) 21.26× slower
======== rotated_reversed 2097152 =============
with intro sort 34.69 ( 28.83ms) (± 8.14%) 1.54× slower
with std::sort in cpp 53.51 ( 18.69ms) (±16.25%) fastest
with qsort in c 16.49 ( 60.64ms) (± 6.32%) 3.24× slower

firejox pushed a commit to firejox/crystal that referenced this pull request Dec 12, 2016
@ysbaddaden
Copy link
Contributor

Rust landed Introsort last week, too: rust-lang/rust#38192

@asterite
Copy link
Member

@ysbaddaden Cool! Now someone should send us a pull request changing Hash's implementation to use open addressing, like Ruby 2.4.0 does :-)

@notriddle
Copy link

Just letting you know that Rust's not a traditional introsort. It's closer to TimSort.

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

Successfully merging this pull request may close these issues.

None yet

10 participants