Mengikuti minat pada pertanyaan ini , saya pikir akan menarik untuk membuat jawaban yang sedikit lebih objektif dan kuantitatif dengan mengusulkan sebuah kontes.
Idenya sederhana: Saya telah menghasilkan file biner yang berisi 50 juta gaussian ganda yang didistribusikan (rata-rata: 0, stdev 1). Tujuannya adalah membuat program yang akan mengurutkan ini dalam memori secepat mungkin. Implementasi referensi yang sangat sederhana dalam python membutuhkan 1m4s untuk selesai. Seberapa rendah kita bisa pergi?
Aturannya adalah sebagai berikut: jawab dengan program yang membuka file "gaussian.dat" dan urutkan angka dalam memori (tidak perlu untuk menampilkannya), dan instruksi untuk membangun dan menjalankan program. Program ini harus dapat bekerja pada mesin Arch Linux saya (artinya Anda dapat menggunakan bahasa pemrograman atau pustaka yang mudah diinstal pada sistem ini).
Program ini harus mudah dibaca, sehingga saya bisa memastikannya aman untuk diluncurkan (tolong jangan ada solusi assembler saja!).
Saya akan menjalankan jawaban di mesin saya (quad core, 4 Gigabytes RAM). Solusi tercepat akan mendapatkan jawaban yang diterima dan hadiah 100 poin :)
Program yang digunakan untuk menghasilkan angka:
#!/usr/bin/env python
import random
from array import array
from sys import argv
count=int(argv[1])
a=array('d',(random.gauss(0,1) for x in xrange(count)))
f=open("gaussian.dat","wb")
a.tofile(f)
Implementasi referensi sederhana:
#!/usr/bin/env python
from array import array
from sys import argv
count=int(argv[1])
a=array('d')
a.fromfile(open("gaussian.dat"),count)
print "sorting..."
b=sorted(a)
EDIT: hanya 4 GB RAM, maaf
EDIT # 2: Perhatikan bahwa tujuan dari kontes ini adalah untuk melihat apakah kita dapat menggunakan informasi sebelumnya tentang data . itu tidak seharusnya menjadi pertandingan kencing antara implementasi bahasa pemrograman yang berbeda!
sumber
Jawaban:
Berikut ini adalah solusi dalam C ++ yang pertama-tama mempartisi angka-angka ke dalam ember dengan jumlah elemen yang diharapkan yang sama dan kemudian mengurutkan masing-masing ember secara terpisah. Itu precomputes tabel dari fungsi distribusi kumulatif berdasarkan pada beberapa formula dari Wikipedia dan kemudian interpolasi nilai dari tabel ini untuk mendapatkan perkiraan cepat.
Beberapa langkah dijalankan dalam beberapa utas untuk memanfaatkan keempat inti.
Untuk mengkompilasi dan menjalankannya, gunakan perintah ini:
EDIT: Semua ember sekarang ditempatkan ke dalam array yang sama untuk menghilangkan kebutuhan untuk menyalin kembali ember ke dalam array. Juga ukuran tabel dengan nilai yang dikomputasi berkurang, karena nilainya cukup akurat. Namun, jika saya mengubah jumlah ember di atas 256, program ini membutuhkan waktu lebih lama untuk dijalankan daripada dengan jumlah ember itu.
EDIT: Algoritma yang sama, bahasa pemrograman yang berbeda. Saya menggunakan C ++ sebagai ganti Java dan waktu operasi berkurang dari ~ 3.2s menjadi ~ 2.35s pada mesin saya. Jumlah bucket optimal masih sekitar 256 (sekali lagi, di komputer saya).
Omong-omong, tbb benar-benar hebat.
EDIT: Saya terinspirasi oleh solusi hebat Alexandru dan menggantikan std :: sort pada fase terakhir dengan versi modifikasi dari jenis radix-nya. Saya memang menggunakan metode yang berbeda untuk menangani angka positif / negatif, meskipun perlu melewati array lebih banyak. Saya juga memutuskan untuk mengurutkan array dengan tepat dan menghapus jenis penyisipan. Saya nantinya akan meluangkan waktu menguji bagaimana perubahan ini mempengaruhi kinerja dan mungkin mengembalikannya. Namun, dengan menggunakan jenis radix, waktu berkurang dari ~ 2,35 ke ~ 1,63.
sumber
Tanpa menjadi pintar, hanya untuk menyediakan penyortir naif yang jauh lebih cepat, inilah satu di C yang seharusnya cukup banyak setara dengan Python Anda:
Dikompilasi dengan
gcc -O3
, pada mesin saya ini membutuhkan waktu lebih dari satu menit kurang dari Python: sekitar 11 detik dibandingkan dengan 87 detik.sumber
Saya mempartisi ke dalam segmen berdasarkan standar deviasi yang harus dipecah menjadi 4s. Sunting: Ditulis ulang ke partisi berdasarkan nilai x di http://en.wikipedia.org/wiki/Error_function#Table_of_values
http://www.wolframalpha.com/input/?i=percentages+by++normal+distribution
Saya mencoba menggunakan ember yang lebih kecil, tetapi tampaknya memiliki sedikit efek 2 * melebihi jumlah core yang tersedia. Tanpa koleksi paralel, akan butuh 37 detik di kotak saya dan 24 dengan koleksi paralel. Jika mempartisi melalui distribusi, Anda tidak bisa hanya menggunakan array, jadi ada beberapa overhead lagi. Saya tidak jelas kapan nilai akan kotak / unboxed di scala.
Saya menggunakan scala 2.9, untuk koleksi paralel. Anda bisa mengunduh distribusi tar.gz itu.
Untuk mengkompilasi: scalac SortFile.scala (Saya baru saja menyalinnya langsung ke folder scala / bin.
Untuk menjalankan: JAVA_OPTS = "- Xmx4096M" ./scala SortFile (Saya menjalankannya dengan 2 pertunjukan ram dan mendapatkan waktu yang hampir bersamaan)
Sunting: Dihapus mengalokasikan Direct, lebih lambat dari hanya mengalokasikan. Priming yang dihapus dari ukuran awal untuk buffer array. Sebenarnya membuatnya membaca seluruh nilai 50000000. Menulis ulang untuk mudah-mudahan menghindari masalah autoboxing (masih lebih lambat dari naif c)
sumber
Masukkan saja ini ke dalam file cs dan kompilasi dengan teori csc: (Membutuhkan mono)
sumber
Karena Anda tahu apa distribusinya, Anda bisa menggunakan pengindeksan O (N) langsung. (Jika Anda bertanya-tanya apa itu, anggaplah Anda memiliki setumpuk 52 kartu dan Anda ingin menyortirnya. Hanya memiliki 52 nampan dan melemparkan setiap kartu ke nampan itu sendiri.)
Anda memiliki 5e7 ganda. Alokasikan array hasil R dari 5e7 ganda. Ambil setiap nomor
x
dan dapatkani = phi(x) * 5e7
. Pada dasarnya lakukanR[i] = x
. Memiliki cara untuk menangani tabrakan, seperti memindahkan nomor yang mungkin bertabrakan dengannya (seperti dalam pengkodean hash sederhana). Atau, Anda bisa membuat R beberapa kali lebih besar, diisi dengan nilai kosong yang unik . Pada akhirnya, Anda hanya menyapu unsur-unsur R.phi
hanyalah fungsi distribusi kumulatif gaussian. Ini mengubah angka terdistribusi gaussian antara +/- tak terhingga menjadi angka terdistribusi seragam antara 0 dan 1. Cara sederhana untuk menghitungnya adalah dengan pencarian tabel dan interpolasi.sumber
Berikut ini adalah solusi berurutan lainnya:
Saya ragu itu mengalahkan solusi multi-threaded, tetapi timing pada laptop i7 saya (stdsort adalah solusi C ++ yang disediakan dalam jawaban lain):
Perhatikan bahwa solusi ini memiliki kompleksitas waktu linier (karena menggunakan representasi khusus ganda).
EDIT : Memperbaiki urutan elemen yang akan meningkat.
EDIT : Peningkatan kecepatan hampir setengah detik.
EDIT : Peningkatan kecepatan oleh 0,7 detik lainnya. Jadikan algoritma lebih ramah cache.
EDIT : Peningkatan kecepatan 1 detik lagi. Karena hanya ada 50.000.000 elemen yang saya dapat mengurutkan sebagian mantissa dan menggunakan menyortir jenis (yang ramah cache) untuk memperbaiki elemen out-of-place. Ide ini menghilangkan sekitar dua iterasi dari loop penyortiran radix terakhir.
EDIT : 0,16 lebih sedikit detik. Pertama std :: membalikkan bisa dihilangkan jika urutan penyortiran terbalik.
sumber
Mengambil solusi Christian Ammer dan memparalelkannya dengan Blok Bangunan Berulir Intel
Jika Anda memiliki akses ke perpustakaan Intel Performance Primitives (IPP), Anda dapat menggunakan jenis radix-nya. Ganti saja
dengan
dan
dengan
Pada laptop dual core saya, waktunya adalah
sumber
Bagaimana dengan penerapan quicksort paralel yang memilih nilai pivot berdasarkan statistik distribusi, sehingga memastikan partisi berukuran sama? Poros pertama akan berada pada rata-rata (nol dalam kasus ini), pasangan berikutnya akan berada pada persentil ke-25 dan ke-75 (+/- -0,67449 standar deviasi), dan seterusnya, dengan masing-masing partisi mengurangi separuh dari set data yang tersisa lebih atau kurang sempurna.
sumber
Sangat jelek (mengapa menggunakan array ketika saya bisa menggunakan variabel yang diakhiri dengan angka), tetapi kode cepat (percobaan pertama saya ke std :: threads), sepanjang waktu (real time) pada sistem saya 1,8 s (dibandingkan dengan std :: sort () 4,8 dtk), kompilasi dengan g ++ -std = c ++ 0x -O3 -march = asli -pthread Cukup lewat data melalui stdin (hanya berfungsi untuk 50M).
// Edit diubah untuk membaca file gaussian.dat.
sumber
Solusi C ++ menggunakan
std::sort
(akhirnya lebih cepat dari qsort, mengenai Kinerja qsort vs std :: sort )Saya tidak dapat diandalkan untuk mengatakan berapa lama karena saya hanya memiliki 1GB di mesin saya dan dengan kode Python yang diberikan saya hanya bisa membuat
gaussian.dat
file dengan hanya 25mio ganda (tanpa mendapatkan kesalahan memori). Tapi saya sangat tertarik berapa lama algoritma std :: sort berjalan.sumber
sort.h
file untuk mengkompilasinya dengan C ++. Itu sekitar dua kali lebih lambat daripadastd::sort
. Tidak tahu mengapa, mungkin karena optimisasi kompiler?Berikut adalah campuran dari jenis radix Alexandru dengan pivot cerdas berulir Zjarek. Kompilasi dengan
Anda dapat mengubah ukuran radix dengan mendefinisikan LANGKAH (misalnya, tambahkan -DSTEP = 11). Saya menemukan yang terbaik untuk laptop saya adalah 8 (default).
Secara default, ini membagi masalah menjadi 4 bagian dan menjalankannya pada beberapa utas. Anda dapat mengubahnya dengan mengirimkan parameter kedalaman ke baris perintah. Jadi, jika Anda memiliki dua inti, jalankan sebagai
dan jika Anda memiliki 16 core
Kedalaman maks sekarang adalah 6 (64 utas). Jika Anda memasukkan terlalu banyak level, Anda hanya akan memperlambat kodenya.
Satu hal yang juga saya coba adalah radix sort dari perpustakaan Intel Performance Primitives (IPP). Implementasi Alexandru benar-benar merusak IPP, dengan IPP sekitar 30% lebih lambat. Variasi itu juga termasuk di sini (dikomentari).
EDIT : Saya menerapkan perbaikan cache Alexandru, dan itu mencukur sekitar 30% dari waktu di mesin saya.
EDIT : Ini mengimplementasikan semacam rekursif, sehingga harus bekerja dengan baik pada mesin 16 core Alexandru. Itu juga menggunakan perbaikan terakhir Alexandru dan menghapus salah satu yang sebaliknya. Bagi saya, ini memberikan peningkatan 20%.
EDIT : Memperbaiki bug tanda yang menyebabkan inefisiensi ketika ada lebih dari 2 core.
EDIT : Menghapus lambda, sehingga akan dikompilasi dengan versi gcc yang lebih lama. Ini termasuk variasi kode IPP yang dikomentari. Saya juga memperbaiki dokumentasi untuk berjalan pada 16 core. Sejauh yang saya tahu, ini adalah implementasi tercepat.
EDIT : Memperbaiki bug ketika LANGKAH tidak 8. Meningkatkan jumlah maksimum utas menjadi 64. Menambahkan beberapa info waktu.
sumber
step
(11 sudah optimal di laptop saya).int cnt[mask]
seharusnyaint cnt[mask + 1]
. Untuk hasil yang lebih baik gunakan nilai tetapint cnt[1 << 16]
.Saya kira ini sangat tergantung pada apa yang ingin Anda lakukan. Jika Anda ingin mengurutkan sekelompok orang Gaussi, maka ini tidak akan membantu Anda. Tetapi jika Anda ingin sekelompok orang Gaussias yang diurutkan, ini akan. Bahkan jika ini sedikit merindukan masalahnya, saya pikir akan menarik untuk membandingkan vs rutinitas penyortiran yang sebenarnya.
Jika Anda ingin sesuatu menjadi cepat, lakukan lebih sedikit.
Alih-alih menghasilkan banyak sampel acak dari distribusi normal dan kemudian menyortir, Anda dapat menghasilkan banyak sampel dari distribusi normal dalam urutan diurutkan.
Anda dapat menggunakan solusi di sini untuk menghasilkan n nomor acak yang seragam dalam urutan diurutkan. Kemudian Anda dapat menggunakan invers cdf (scipy.stats.norm.ppf) dari distribusi normal untuk mengubah angka acak seragam menjadi angka dari distribusi normal melalui inverse transform sampling .
Jika Anda ingin mendapatkan tangan Anda lebih kotor, saya kira Anda mungkin bisa mempercepat banyak perhitungan cdf terbalik dengan menggunakan beberapa jenis metode berulang, dan menggunakan hasil sebelumnya sebagai tebakan awal Anda. Karena tebakannya akan sangat dekat, mungkin satu iterasi tunggal akan memberi Anda akurasi tinggi.
sumber
Coba ubah solusi Guvante ini dengan Main ini (), ia mulai mengurutkan begitu 1/4 IO selesai, lebih cepat dalam pengujian saya:
sumber
Karena Anda tahu distribusinya, ide saya adalah membuat k ember, masing-masing dengan jumlah elemen yang diharapkan (karena Anda tahu distribusinya, Anda dapat menghitung ini). Kemudian dalam waktu O (n), sapu array dan masukkan elemen ke dalam ember mereka.
Kemudian secara bersamaan menyortir ember. Misalkan Anda memiliki k k, dan n elemen. Sebuah ember akan mengambil (n / k) lg (n / k) waktu untuk menyortir. Sekarang anggaplah Anda memiliki prosesor p yang dapat Anda gunakan. Karena bucket dapat disortir secara independen, Anda memiliki pengali langit-langit (k / p) untuk ditangani. Ini memberikan runtime akhir n + ceil (k / p) * (n / k) lg (n / k), yang seharusnya jauh lebih cepat daripada n lg n jika Anda memilih k dengan baik.
sumber
std::sort()
, tapi itu jauh lebih lambat daripada solusi radixsort Alexandru.Satu ide optimasi tingkat rendah adalah mencocokkan dua ganda dalam register SSE, sehingga setiap utas akan bekerja dengan dua item sekaligus. Ini mungkin rumit untuk dilakukan untuk beberapa algoritma.
Hal lain yang harus dilakukan adalah mengurutkan array dalam potongan cache-friendly, lalu menggabungkan hasilnya. Dua level harus digunakan: misalnya pertama 4 KB untuk L1 kemudian 64 KB untuk L2.
Ini harus sangat ramah-cache, karena jenis bucket tidak akan keluar dari cache, dan penggabungan akhir akan memunculkan memori secara berurutan.
Perhitungan hari ini jauh lebih murah daripada akses memori. Namun kami memiliki sejumlah besar item, jadi sulit untuk mengetahui mana ukuran array ketika jenis cache-aware lebih lambat daripada versi non-cache-aware dengan kompleksitas rendah.
Tapi saya tidak akan memberikan implementasi di atas karena saya akan melakukannya di Windows (VC ++).
sumber
Berikut adalah implementasi sortir ember pemindaian linier. Saya pikir ini lebih cepat dari semua implementasi single-threaded saat ini kecuali untuk jenis radix. Seharusnya linear waktu yang diharapkan berjalan jika saya memperkirakan cdf cukup akurat (Saya menggunakan interpolasi linear dari nilai yang saya temukan di web) dan tidak membuat kesalahan yang akan menyebabkan pemindaian berlebihan:
sumber
Saya tidak tahu, mengapa saya tidak bisa mengedit posting saya sebelumnya, jadi ini versi baru, 0,2 detik lebih cepat (tetapi sekitar 1,5 detik lebih cepat dalam waktu CPU (pengguna)). Solusi ini memiliki 2 program, pertama menghitung kuantil untuk distribusi normal untuk jenis bucket, dan menyimpannya dalam tabel, t [skala ganda *] = indeks bucket, di mana skala adalah beberapa angka acak yang memungkinkan casting untuk menggandakan kemungkinan. Kemudian program utama dapat menggunakan data ini untuk menempatkan ganda di ember yang benar. Ini memiliki satu kelemahan, jika data tidak gaussian itu tidak akan berfungsi dengan benar (dan juga hampir tidak ada peluang untuk bekerja secara salah untuk distribusi normal), tetapi modifikasi untuk case khusus mudah dan cepat (hanya jumlah bucket yang diperiksa dan jatuh ke std ::menyortir()).
Kompilasi: g ++ => http://pastebin.com/WG7pZEzH program bantuan
g ++ -std = c ++ 0x -O3 -march = native -pthread => http://pastebin.com/T3yzViZP program penyortiran utama
sumber
Berikut ini adalah solusi berurutan lainnya. Yang ini menggunakan fakta bahwa elemen-elemennya terdistribusi normal, dan saya pikir idenya secara umum berlaku untuk menyortir mendekati waktu linier.
Algoritmanya seperti ini:
phi()
fungsi dalam implementasi)size * phi(x)
Sayangnya, konstanta tersembunyi cukup besar dan solusi ini dua kali lebih lambat dari algoritma sortir radix.
sumber
Favorit pribadi saya menggunakan Blok Bangunan Berulir Intel telah diposting, tetapi berikut ini adalah solusi paralel paralel menggunakan JDK 7 dan fork / join API baru:
Penafian penting : Saya melakukan adaptasi cepat untuk fork / gabung dari: https://github.com/pmbauer/parallel/tree/master/src/main/java/pmbauer/parallel
Untuk menjalankan ini, Anda memerlukan versi beta JDK 7 (http://jdk7.java.net/download.html).
Pada core i7 2.93Ghz Quad core saya (OS X):
Referensi Python
Fork JDK 7 Java / gabung
Saya juga mencoba melakukan beberapa percobaan dengan membaca paralel dan mengubah byte menjadi dua kali lipat, tetapi saya tidak melihat perbedaan di sana.
Memperbarui:
Jika ada yang ingin bereksperimen dengan pemuatan paralel data, versi pemuatan paralel di bawah. Secara teori ini bisa membuatnya sedikit lebih cepat, jika perangkat IO Anda memiliki kapasitas paralel yang cukup (SSD biasanya melakukannya). Ada juga beberapa overhead dalam membuat Doubles from bytes, sehingga berpotensi menjadi lebih cepat secara paralel juga. Pada sistem saya (SSD Ubuntu 10.10 / Nehalem Quad / Intel X25M, dan OS X 10.6 / i7 Quad / Samsung SSD) saya tidak melihat perbedaan nyata.
Pembaruan2:
Saya mengeksekusi kode pada salah satu dari 12 mesin dev inti kami dengan sedikit modifikasi untuk menetapkan jumlah inti yang tetap. Ini memberikan hasil sebagai berikut:
Pada sistem ini saya juga mencoba versi Python yang mengambil 1m2.994s dan versi C ++ Zjarek yang mengambil 1.925s (untuk beberapa alasan versi C ++ Zjarek tampaknya berjalan relatif lebih cepat di komputer static_rtti).
Saya juga mencoba apa yang terjadi jika saya menggandakan ukuran file menjadi 100.000.000 ganda:
Dalam hal ini, versi C ++ Zjarek mengambil 3,968s. Python terlalu lama di sini.
150.000.000 ganda:
Dalam hal ini, versi C ++ Zjarek adalah 6.044. Saya bahkan tidak mencoba Python.
Versi C ++ sangat konsisten dengan hasilnya, di mana Java sedikit berayun. Pertama itu menjadi sedikit lebih efisien ketika masalahnya menjadi lebih besar, tetapi kemudian kurang efisien lagi.
sumber
Versi menggunakan pthreads tradisional. Kode untuk menggabungkan disalin dari jawaban Guvante. Kompilasi dengan
g++ -O3 -pthread
.Di laptop saya, saya mendapatkan hasil berikut:
sumber
Berikut ini adalah implementasi C99 berurutan yang mencoba untuk benar-benar memanfaatkan distribusi yang dikenal. Ini pada dasarnya melakukan satu putaran bucket sortir menggunakan informasi distribusi, kemudian beberapa putaran quicksort pada setiap bucket dengan asumsi distribusi seragam dalam batas bucket dan akhirnya sortir seleksi yang dimodifikasi untuk menyalin data kembali ke buffer asli. Quicksort menghafal titik perpecahan, jadi pemilihan semacam hanya perlu beroperasi pada potongan kecil. Dan terlepas dari (karena?) Semua kerumitan itu, itu bahkan tidak terlalu cepat.
Untuk membuat evaluasi Φ cepat, nilai sampel dalam beberapa poin dan kemudian hanya interpolasi linier yang digunakan. Sebenarnya tidak masalah jika Φ dievaluasi dengan tepat, asalkan perkiraannya benar-benar monoton.
Ukuran nampan dipilih sedemikian rupa sehingga kemungkinan nampan bin diabaikan. Lebih tepatnya, dengan parameter saat ini, kemungkinan dataset 50000000 elemen akan menyebabkan overflow bin adalah 3,65e-09. (Ini dapat dihitung menggunakan fungsi survival dari distribusi Poisson .)
Untuk mengkompilasi, silakan gunakan
Karena ada perhitungan yang jauh lebih banyak daripada solusi lain, flag-flag compiler ini diperlukan untuk membuatnya setidaknya cukup cepat. Tanpa
-msse3
konversi daridouble
keint
menjadi sangat lambat. Jika arsitektur Anda tidak mendukung SSE3, konversi ini juga dapat dilakukan menggunakanlrint()
fungsi.Kode ini agak jelek - tidak yakin apakah ini memenuhi persyaratan "cukup mudah dibaca" ...
sumber
Ini menggunakan erf () untuk menempatkan setiap elemen secara tepat ke dalam nampan, lalu mengurutkan masing-masing nampan. Itu membuat array sepenuhnya di tempat.
Pass pertama: docensus () menghitung jumlah elemen dalam setiap bin.
Lulus kedua: partisi () memungkinkan array, menempatkan setiap elemen ke dalam bin yang tepat
Lulus ketiga: sortbins () melakukan qsort pada setiap nampan.
Agak naif, dan memanggil fungsi erf () mahal dua kali untuk setiap nilai. Pass pertama dan ketiga berpotensi diparalelkan. Yang kedua sangat serial dan mungkin diperlambat oleh pola akses memori yang sangat acak. Mungkin juga bermanfaat untuk men-cache nomor bin setiap dobel, tergantung pada rasio kecepatan CPU terhadap memori.
Program ini memungkinkan Anda memilih jumlah sampah untuk digunakan. Tambahkan saja angka kedua ke baris perintah. Saya mengkompilasinya dengan gcc -O3, tetapi mesin saya sangat lemah sehingga saya tidak bisa memberi tahu Anda angka kinerja yang baik.
Edit: Poof! Program C saya secara ajaib berubah menjadi program C ++ menggunakan std :: sort!
sumber
Lihatlah implementasi radix sort oleh Michael Herf ( Radix Tricks ). Di mesin saya penyortiran 5 kali lebih cepat dibandingkan dengan
std::sort
algoritma dalam jawaban pertama saya. Nama fungsi penyortiran adalahRadixSort11
.sumber