Cách nhanh nhất để sắp xếp 10 số? (số là 32 bit)


211

Tôi đang giải quyết một vấn đề và nó liên quan đến việc sắp xếp 10 số (int32) rất nhanh. Ứng dụng của tôi cần sắp xếp 10 số nhanh gấp hàng triệu lần. Tôi đang lấy mẫu một tập hợp dữ liệu gồm hàng tỷ phần tử và mỗi lần tôi cần chọn 10 số trong số đó (đơn giản hóa) và sắp xếp chúng (và đưa ra kết luận từ danh sách 10 phần tử được sắp xếp).

Hiện tại tôi đang sử dụng sắp xếp chèn nhưng tôi tưởng tượng rằng tôi có thể thực hiện thuật toán sắp xếp tùy chỉnh rất nhanh cho bài toán cụ thể gồm 10 số sẽ đánh bại sắp xếp chèn.

Có ai có bất kỳ ý tưởng về cách tiếp cận vấn đề này?


14
Nghe có vẻ thô thiển, một loạt các iftuyên bố lồng nhau sẽ hoạt động tốt nhất. Tránh các vòng lặp.
ja72

8
Bạn có mong đợi rằng các con số sẽ được trao cho bạn với bất kỳ sai lệch nào trong tập hợp hoán vị, hoặc chúng sẽ được phân phối đồng đều? Sẽ có bất kỳ mối quan hệ giữa thứ tự của một danh sách và tiếp theo?
Douglas Zare

4
Toàn bộ tập dữ liệu (với hàng tỷ số) được phân phối theo luật của Benford nhưng khi tôi chọn các yếu tố ngẫu nhiên trong bộ này, chúng không còn nữa (tôi nghĩ).
Bodacydo

13
Bạn có thể muốn đọc stackoverflow.com/q/2786899/995714
phuclv

11
Nếu bạn chọn ngẫu nhiên từ hàng tỷ phần tử thì có thể độ trễ để kéo dữ liệu đó có thể có nhiều tác động hơn thời gian cần thiết để sắp xếp các phần tử được chọn ngay cả khi toàn bộ tập dữ liệu nằm trong RAM. Bạn có thể kiểm tra tác động bằng cách đánh giá hiệu suất chọn dữ liệu theo tuần tự so với ngẫu nhiên.
Steve S.

Câu trả lời:


213

(Theo dõi gợi ý của HelloWorld để xem xét các mạng sắp xếp.)

Có vẻ như một mạng so sánh / trao đổi 29 là cách nhanh nhất để thực hiện sắp xếp 10 đầu vào. Tôi đã sử dụng mạng được Waksman phát hiện vào năm 1969 cho ví dụ này trong Javascript, dịch này sẽ dịch trực tiếp sang C, vì nó chỉ là một danh sách các ifcâu lệnh, so sánh và hoán đổi.

function sortNet10(data) {	// ten-input sorting network by Waksman, 1969
    var swap;
    if (data[0] > data[5]) { swap = data[0]; data[0] = data[5]; data[5] = swap; }
    if (data[1] > data[6]) { swap = data[1]; data[1] = data[6]; data[6] = swap; }
    if (data[2] > data[7]) { swap = data[2]; data[2] = data[7]; data[7] = swap; }
    if (data[3] > data[8]) { swap = data[3]; data[3] = data[8]; data[8] = swap; }
    if (data[4] > data[9]) { swap = data[4]; data[4] = data[9]; data[9] = swap; }
    if (data[0] > data[3]) { swap = data[0]; data[0] = data[3]; data[3] = swap; }
    if (data[5] > data[8]) { swap = data[5]; data[5] = data[8]; data[8] = swap; }
    if (data[1] > data[4]) { swap = data[1]; data[1] = data[4]; data[4] = swap; }
    if (data[6] > data[9]) { swap = data[6]; data[6] = data[9]; data[9] = swap; }
    if (data[0] > data[2]) { swap = data[0]; data[0] = data[2]; data[2] = swap; }
    if (data[3] > data[6]) { swap = data[3]; data[3] = data[6]; data[6] = swap; }
    if (data[7] > data[9]) { swap = data[7]; data[7] = data[9]; data[9] = swap; }
    if (data[0] > data[1]) { swap = data[0]; data[0] = data[1]; data[1] = swap; }
    if (data[2] > data[4]) { swap = data[2]; data[2] = data[4]; data[4] = swap; }
    if (data[5] > data[7]) { swap = data[5]; data[5] = data[7]; data[7] = swap; }
    if (data[8] > data[9]) { swap = data[8]; data[8] = data[9]; data[9] = swap; }
    if (data[1] > data[2]) { swap = data[1]; data[1] = data[2]; data[2] = swap; }
    if (data[3] > data[5]) { swap = data[3]; data[3] = data[5]; data[5] = swap; }
    if (data[4] > data[6]) { swap = data[4]; data[4] = data[6]; data[6] = swap; }
    if (data[7] > data[8]) { swap = data[7]; data[7] = data[8]; data[8] = swap; }
    if (data[1] > data[3]) { swap = data[1]; data[1] = data[3]; data[3] = swap; }
    if (data[4] > data[7]) { swap = data[4]; data[4] = data[7]; data[7] = swap; }
    if (data[2] > data[5]) { swap = data[2]; data[2] = data[5]; data[5] = swap; }
    if (data[6] > data[8]) { swap = data[6]; data[6] = data[8]; data[8] = swap; }
    if (data[2] > data[3]) { swap = data[2]; data[2] = data[3]; data[3] = swap; }
    if (data[4] > data[5]) { swap = data[4]; data[4] = data[5]; data[5] = swap; }
    if (data[6] > data[7]) { swap = data[6]; data[6] = data[7]; data[7] = swap; }
    if (data[3] > data[4]) { swap = data[3]; data[3] = data[4]; data[4] = swap; }
    if (data[5] > data[6]) { swap = data[5]; data[5] = data[6]; data[6] = swap; }
    return(data);
}

alert(sortNet10([5,7,1,8,4,3,6,9,2,0]));

Đây là một đại diện đồ họa của mạng, được chia thành các giai đoạn độc lập. Để tận dụng quá trình xử lý song song, việc phân nhóm 5-4-3-4-4-4-3-2 có thể được thay đổi thành một nhóm 4-4-4-4-4-4-3-2.
Mạng phân loại 10 đầu vào (Waksman, 1969)

Mạng sắp xếp 10 đầu vào (Waksman, 1969) được nhóm lại


69
gợi ý; sử dụng macro hoán đổi. thích#define SORTPAIR(data, i1, i2) if (data[i1] > data[i2]) { int swap = data[i1]... }
Peter Cordes

9
Nó có thể được chỉ ra một cách hợp lý rằng đây là mức tối thiểu?
corsiKa

8
@corsiKa Vâng, các mạng phân loại đã là một lĩnh vực nghiên cứu từ những ngày đầu của khoa học máy tính. Trong nhiều trường hợp, các giải pháp tối ưu đã được biết đến trong nhiều thập kỷ. Xem en.wikipedia.org/wiki/Sorting_network
m69 '' lén lút và không mong muốn ''

8
Tôi đã tạo một Jsperf để kiểm tra và tôi có thể xác nhận rằng Network Sort nhanh hơn 20 lần so với sắp xếp riêng của trình duyệt. jsperf.com/fastest-10-number-sort
Daniel

9
@Katai Điều này sẽ phá hủy mọi tối ưu hóa mà trình biên dịch của bạn có thể tạo ra. Ý kiến ​​tồi. Đọc phần này để biết thêm thông tin en.wikipedia.org/wiki/
Kẻ

88

Khi bạn đối phó với kích thước cố định này, hãy xem Sắp xếp Mạng . Các thuật toán này có thời gian chạy cố định và độc lập với đầu vào của chúng. Đối với trường hợp sử dụng của bạn, bạn không có chi phí như vậy mà một số thuật toán sắp xếp có.

Bitonic sort là một triển khai của mạng như vậy. Cái này hoạt động tốt nhất với len (n) <= 32 trên CPU. Trên các đầu vào lớn hơn, bạn có thể nghĩ đến việc chuyển sang GPU. https://en.wikipedia.org/wiki/Sorting_network

Btw, một trang tốt để so sánh các thuật toán sắp xếp là cái này ở đây (mặc dù nó thiếu bitonic sort.

http://www.sorting-alerskyms.com


3
@ ErickG.Hagstrom Có nhiều giải pháp; miễn là họ sử dụng 29 so sánh, chúng đều hiệu quả như nhau. Tôi đã sử dụng giải pháp của Waksman từ năm 1969; ông rõ ràng là người đầu tiên phát hiện ra một phiên bản so sánh 29.
m69 '' lén lút và không mong muốn ''

1
Vâng, @ m69. Có hơn một triệu. Giải pháp của Waksman có chiều dài 29 và độ sâu 9. Giải pháp tôi liên kết là một cải tiến so với chiều sâu: length = 29, height = 8. Tất nhiên, khi được thực hiện trong C, độ sâu không thành vấn đề.
Erick G. Hagstrom

4
@ ErickG.Hagstrom Rõ ràng có 87 giải pháp với độ sâu 7, giải pháp đầu tiên được Knuth tìm thấy vào năm 1973, nhưng tôi không thể tìm thấy bất kỳ giải pháp nào trong số đó bằng Google nhanh chóng. larc.unt.edu/ian/pub/9-input.pdf (xem Kết luận, trang 14)
m69 '' lén lút và không mong muốn ''

4
@ ErickG.Hagstrom: độ sâu có thể không có sự khác biệt "ở cấp độ C", nhưng có lẽ một khi trình biên dịch và CPU đã kết thúc với nó, có khả năng nó sẽ được song song một phần trong CPU và do đó độ sâu nhỏ hơn có thể giúp ích. Tất nhiên, tùy thuộc vào CPU: một số CPU tương đối đơn giản và thực hiện hết lần này đến lần khác, trong khi một số CPU có thể có nhiều hoạt động trong chuyến bay, đặc biệt bạn có thể có hiệu suất rất khác nhau cho bất kỳ tải và lưu trữ nào vào ngăn xếp cần thiết trong để thao tác 10 biến, tùy thuộc vào cách chúng được thực hiện.
Steve Jessop

1
@ ErickG.Hagstrom Không rõ ràng ngay lập tức từ bài báo của Ian Parberry, nhưng các mạng sâu 7 có độ dài lớn hơn 29. Xem Knuth, "Nghệ thuật lập trình máy tính Vol.III", §5.3.4, fig . 49 và 51.
m69 '' lén lút và không mong muốn ''

33

Sử dụng một mạng sắp xếp có so sánh trong các nhóm 4, vì vậy bạn có thể làm điều đó trong các thanh ghi SIMD. Một cặp các lệnh min / max được đóng gói thực hiện chức năng so sánh được đóng gói. Xin lỗi, tôi không có thời gian ngay bây giờ để tìm một trang mà tôi nhớ đã thấy về điều này, nhưng hy vọng việc tìm kiếm trên các mạng sắp xếp SIMD hoặc SSE sẽ bật lên một cái gì đó.

x86 SSE không có các lệnh tối thiểu 32 bit và số nguyên tối thiểu cho các vectơ của bốn int 32 bit. AVX2 (Haswell trở lên) có cùng vectơ 256b của 8 ints. Ngoài ra còn có hướng dẫn xáo trộn hiệu quả.

Nếu bạn có nhiều loại nhỏ độc lập, có thể thực hiện song song 4 hoặc 8 loại bằng vectơ. Đặc biệt nếu bạn chọn ngẫu nhiên các yếu tố (vì vậy dữ liệu sẽ được sắp xếp sẽ không liền kề trong bộ nhớ), bạn có thể tránh xáo trộn và chỉ cần so sánh theo thứ tự bạn cần. 10 thanh ghi để giữ tất cả dữ liệu từ 4 (AVX2: 8) danh sách 10 ints vẫn để lại 6 regs cho không gian đầu.

Mạng sắp xếp vector kém hiệu quả hơn nếu bạn cũng cần sắp xếp dữ liệu liên quan. Trong trường hợp đó, cách hiệu quả nhất dường như là sử dụng phép so sánh đóng gói để lấy mặt nạ có các phần tử thay đổi và sử dụng mặt nạ đó để trộn các vectơ của (tham chiếu đến) dữ liệu liên quan.


26

Điều gì về một loại lựa chọn không kiểm soát, chi nhánh ít?

#include <iostream>
#include <algorithm>
#include <random>

//return the index of the minimum element in array a
int min(const int * const a) {
  int m = a[0];
  int indx = 0;
  #define TEST(i) (m > a[i]) && (m = a[i], indx = i ); 
  //see http://stackoverflow.com/a/7074042/2140449
  TEST(1);
  TEST(2);
  TEST(3);
  TEST(4);
  TEST(5);
  TEST(6);
  TEST(7);
  TEST(8);
  TEST(9);
  #undef TEST
  return indx;
}

void sort( int * const a ){
  int work[10];
  int indx;
  #define GET(i) indx = min(a); work[i] = a[indx]; a[indx] = 2147483647; 
  //get the minimum, copy it to work and set it at max_int in a
  GET(0);
  GET(1);
  GET(2);
  GET(3);
  GET(4);
  GET(5);
  GET(6);
  GET(7);
  GET(8);
  GET(9);
  #undef GET
  #define COPY(i) a[i] = work[i];
  //copy back to a
  COPY(0);
  COPY(1);
  COPY(2);
  COPY(3);
  COPY(4);
  COPY(5);
  COPY(6);
  COPY(7);
  COPY(8);
  COPY(9);
  #undef COPY
}

int main() {
  //generating and printing a random array
  int a[10] = { 1,2,3,4,5,6,7,8,9,10 };
  std::random_device rd;
  std::mt19937 g(rd());
  std::shuffle( a, a+10, g);
  for (int i = 0; i < 10; i++) {
    std::cout << a[i] << ' ';
  }
  std::cout << std::endl;

  //sorting and printing again
  sort(a);
  for (int i = 0; i < 10; i++) {
    std::cout << a[i] << ' ';
  } 

  return 0;
}

http://coliru.stacked-crooking.com/a/71e18bc4f7fa18c6

Các dòng liên quan duy nhất là hai đầu tiên #define.

Nó sử dụng hai danh sách và kiểm tra lại hoàn toàn danh sách đầu tiên trong mười lần, đây sẽ là một loại lựa chọn được triển khai kém, tuy nhiên nó tránh được các nhánh và các vòng lặp có chiều dài thay đổi, có thể bù cho các bộ xử lý hiện đại và một bộ dữ liệu nhỏ như vậy.


Điểm chuẩn

Tôi đã so sánh với mạng sắp xếp và mã của tôi dường như chậm hơn. Tuy nhiên tôi đã cố gắng để loại bỏ unrolling và bản sao. Chạy mã này:

#include <iostream>
#include <algorithm>
#include <random>
#include <chrono>

int min(const int * const a, int i) {
  int m = a[i];
  int indx = i++;
  for ( ; i<10; i++) 
    //see http://stackoverflow.com/a/7074042/2140449
    (m > a[i]) && (m = a[i], indx = i ); 
  return indx;
}

void sort( int * const a ){
  for (int i = 0; i<9; i++)
    std::swap(a[i], a[min(a,i)]); //search only forward
}


void sortNet10(int * const data) {  // ten-input sorting network by Waksman, 1969
    int swap;
    if (data[0] > data[5]) { swap = data[0]; data[0] = data[5]; data[5] = swap; }
    if (data[1] > data[6]) { swap = data[1]; data[1] = data[6]; data[6] = swap; }
    if (data[2] > data[7]) { swap = data[2]; data[2] = data[7]; data[7] = swap; }
    if (data[3] > data[8]) { swap = data[3]; data[3] = data[8]; data[8] = swap; }
    if (data[4] > data[9]) { swap = data[4]; data[4] = data[9]; data[9] = swap; }
    if (data[0] > data[3]) { swap = data[0]; data[0] = data[3]; data[3] = swap; }
    if (data[5] > data[8]) { swap = data[5]; data[5] = data[8]; data[8] = swap; }
    if (data[1] > data[4]) { swap = data[1]; data[1] = data[4]; data[4] = swap; }
    if (data[6] > data[9]) { swap = data[6]; data[6] = data[9]; data[9] = swap; }
    if (data[0] > data[2]) { swap = data[0]; data[0] = data[2]; data[2] = swap; }
    if (data[3] > data[6]) { swap = data[3]; data[3] = data[6]; data[6] = swap; }
    if (data[7] > data[9]) { swap = data[7]; data[7] = data[9]; data[9] = swap; }
    if (data[0] > data[1]) { swap = data[0]; data[0] = data[1]; data[1] = swap; }
    if (data[2] > data[4]) { swap = data[2]; data[2] = data[4]; data[4] = swap; }
    if (data[5] > data[7]) { swap = data[5]; data[5] = data[7]; data[7] = swap; }
    if (data[8] > data[9]) { swap = data[8]; data[8] = data[9]; data[9] = swap; }
    if (data[1] > data[2]) { swap = data[1]; data[1] = data[2]; data[2] = swap; }
    if (data[3] > data[5]) { swap = data[3]; data[3] = data[5]; data[5] = swap; }
    if (data[4] > data[6]) { swap = data[4]; data[4] = data[6]; data[6] = swap; }
    if (data[7] > data[8]) { swap = data[7]; data[7] = data[8]; data[8] = swap; }
    if (data[1] > data[3]) { swap = data[1]; data[1] = data[3]; data[3] = swap; }
    if (data[4] > data[7]) { swap = data[4]; data[4] = data[7]; data[7] = swap; }
    if (data[2] > data[5]) { swap = data[2]; data[2] = data[5]; data[5] = swap; }
    if (data[6] > data[8]) { swap = data[6]; data[6] = data[8]; data[8] = swap; }
    if (data[2] > data[3]) { swap = data[2]; data[2] = data[3]; data[3] = swap; }
    if (data[4] > data[5]) { swap = data[4]; data[4] = data[5]; data[5] = swap; }
    if (data[6] > data[7]) { swap = data[6]; data[6] = data[7]; data[7] = swap; }
    if (data[3] > data[4]) { swap = data[3]; data[3] = data[4]; data[4] = swap; }
    if (data[5] > data[6]) { swap = data[5]; data[5] = data[6]; data[6] = swap; }
}


std::chrono::duration<double> benchmark( void(*func)(int * const), const int seed ) {
  std::mt19937 g(seed);
  int a[10] = {10,11,12,13,14,15,16,17,18,19};
  std::chrono::high_resolution_clock::time_point t1, t2; 
  t1 = std::chrono::high_resolution_clock::now();
  for (long i = 0; i < 1e7; i++) {
    std::shuffle( a, a+10, g);
    func(a);
  }
  t2 = std::chrono::high_resolution_clock::now();
  return std::chrono::duration_cast<std::chrono::duration<double>>(t2 - t1);
}

int main() {
  std::random_device rd;
  for (int i = 0; i < 10; i++) {
    const int seed = rd();
    std::cout << "seed = " << seed << std::endl;
    std::cout << "sortNet10: " << benchmark(sortNet10, seed).count() << std::endl;
    std::cout << "sort:      " << benchmark(sort,      seed).count() << std::endl;
  }
  return 0;
}

Tôi liên tục nhận được kết quả tốt hơn cho loại sắp xếp không có chi nhánh so với mạng sắp xếp.

$ gcc -v
gcc version 5.2.0 (GCC) 
$ g++ -std=c++11 -Ofast sort.cpp && ./a.out
seed = -1727396418
sortNet10: 2.24137
sort:      2.21828
seed = 2003959850
sortNet10: 2.23914
sort:      2.21641
seed = 1994540383
sortNet10: 2.23782
sort:      2.21778
seed = 1258259982
sortNet10: 2.25199
sort:      2.21801
seed = 1821086932
sortNet10: 2.25535
sort:      2.2173
seed = 412262735
sortNet10: 2.24489
sort:      2.21776
seed = 1059795817
sortNet10: 2.29226
sort:      2.21777
seed = -188551272
sortNet10: 2.23803
sort:      2.22996
seed = 1043757247
sortNet10: 2.2503
sort:      2.23604
seed = -268332483
sortNet10: 2.24455
sort:      2.24304

4
Kết quả không ấn tượng lắm, nhưng thực sự là những gì tôi mong đợi. Mạng phân loại giảm thiểu so sánh, không hoán đổi. Khi tất cả các giá trị đã có trong các so sánh bộ đệm rẻ hơn nhiều so với các giao dịch hoán đổi, do đó, một loại lựa chọn (giảm thiểu số lượng giao dịch hoán đổi) chiếm ưu thế. (và không có nhiều sự so sánh hơn: mạng với 29 compasions, lên đến lựa chọn 29 giao dịch hoán đổi ?; vs loại với 45 so sánh và tối đa là 9 giao dịch hoán đổi)
ví dụ

7
Oh và nó có các nhánh - trừ khi dòng for ( ; i<10; i++) (m > a[i]) && (m = a[i], indx = i ); được tối ưu hóa đặc biệt tốt. (ngắn mạch thường là một hình thức phân nhánh)
ví dụ

1
@EugeneRyabtsev cũng vậy, nhưng nó được cho ăn với chính xác các chuỗi ngẫu nhiên giống nhau mọi lúc nên sẽ hủy. Tôi đã cố gắng thay đổi std::shufflevới for (int n = 0; n<10; n++) a[n]=g();. Thời gian thực hiện giảm một nửa và mạng nhanh hơn bây giờ.
DarioP

Làm thế nào để so sánh với libc ++ std::sort?
gnzlbg

1
@gnzlbg Tôi cũng đã thử std::sortnhưng nó hoạt động rất tệ đến nỗi tôi thậm chí không đưa nó vào điểm chuẩn. Tôi đoán rằng với các tập dữ liệu nhỏ có khá nhiều chi phí.
DarioP

20

Câu hỏi không nói rằng đây là một loại ứng dụng dựa trên web. Một điều làm tôi chú ý là:

Tôi đang lấy mẫu một tập hợp dữ liệu gồm hàng tỷ phần tử và mỗi lần tôi cần chọn 10 số trong số đó (đơn giản hóa) và sắp xếp chúng (và đưa ra kết luận từ danh sách 10 phần tử được sắp xếp).

Là một kỹ sư phần mềm và phần cứng, điều này hoàn toàn hét lên "FPGA" với tôi. Tôi không biết loại kết luận nào bạn cần rút ra từ bộ số được sắp xếp hoặc dữ liệu đến từ đâu nhưng tôi biết sẽ rất khó để xử lý ở đâu đó giữa một trăm triệu và một tỷ trong số "sắp xếp và" phân tích "hoạt động mỗi giây . Trước đây, tôi đã thực hiện công việc giải trình tự DNA được hỗ trợ bằng công nghệ đồ họa. Gần như không thể đánh bại sức mạnh xử lý khổng lồ của FPGA khi vấn đề rất phù hợp với loại giải pháp đó.

Ở một mức độ nào đó, yếu tố giới hạn duy nhất trở thành việc bạn có thể chuyển dữ liệu vào một đồ họa nhanh như thế nào và bạn có thể lấy nó ra nhanh như thế nào.

Để tham khảo, tôi đã thiết kế bộ xử lý hình ảnh thời gian thực hiệu suất cao, nhận dữ liệu hình ảnh RGB 32 bit với tốc độ khoảng 300 triệu pixel mỗi giây. Dữ liệu được truyền qua các bộ lọc FIR, hệ số nhân ma trận, bảng tra cứu, khối phát hiện cạnh không gian và một số hoạt động khác trước khi đi ra đầu kia. Tất cả những điều này trên một Xilinx Virtex2 tương đối nhỏ với xung nhịp bên trong kéo dài từ khoảng 33 MHz đến, nếu tôi nhớ chính xác, 400 MHz. Ồ, vâng, nó cũng đã thực hiện bộ điều khiển DDR2 và chạy hai ngân hàng bộ nhớ DDR2.

Một FPGA có thể xuất ra một loại mười số 32 bit trên mỗi lần chuyển đổi xung nhịp trong khi hoạt động ở hàng trăm MHz. Sẽ có độ trễ ngắn khi bắt đầu vận hành khi dữ liệu lấp đầy đường ống xử lý / s. Sau đó, bạn sẽ có thể nhận được một kết quả trên mỗi đồng hồ. Hoặc nhiều hơn nếu việc xử lý có thể được song song thông qua việc sao chép đường ống phân loại và phân tích. Các giải pháp, về nguyên tắc, là gần như tầm thường.

Vấn đề là: Nếu ứng dụng không bị ràng buộc bởi PC và luồng dữ liệu và xử lý là "tương thích" với giải pháp FPGA (độc lập hoặc là thẻ đồng xử lý trong máy), bạn sẽ không có cách nào khác để có thể đánh bại mức hiệu suất có thể đạt được với phần mềm được viết bằng bất kỳ ngôn ngữ nào, bất kể thuật toán.

BIÊN TẬP:

Chỉ cần chạy nhanh tìm kiếm và tìm thấy một bài báo có thể được sử dụng cho bạn. Có vẻ như nó bắt đầu từ năm 2012. Bạn có thể làm tốt hơn rất nhiều trong hoạt động ngày hôm nay (và thậm chí là sau đó). Đây là:

Sắp xếp mạng trên các đồ họa


10

Gần đây tôi đã viết một lớp học nhỏ sử dụng thuật toán Bose-Nelson để tạo ra một mạng sắp xếp theo thời gian biên dịch.

Nó có thể được sử dụng để tạo ra một loại rất nhanh cho 10 số.

/**
 * A Functor class to create a sort for fixed sized arrays/containers with a
 * compile time generated Bose-Nelson sorting network.
 * \tparam NumElements  The number of elements in the array or container to sort.
 * \tparam T            The element type.
 * \tparam Compare      A comparator functor class that returns true if lhs < rhs.
 */
template <unsigned NumElements, class Compare = void> class StaticSort
{
    template <class A, class C> struct Swap
    {
        template <class T> inline void s(T &v0, T &v1)
        {
            T t = Compare()(v0, v1) ? v0 : v1; // Min
            v1 = Compare()(v0, v1) ? v1 : v0; // Max
            v0 = t;
        }

        inline Swap(A &a, const int &i0, const int &i1) { s(a[i0], a[i1]); }
    };

    template <class A> struct Swap <A, void>
    {
        template <class T> inline void s(T &v0, T &v1)
        {
            // Explicitly code out the Min and Max to nudge the compiler
            // to generate branchless code.
            T t = v0 < v1 ? v0 : v1; // Min
            v1 = v0 < v1 ? v1 : v0; // Max
            v0 = t;
        }

        inline Swap(A &a, const int &i0, const int &i1) { s(a[i0], a[i1]); }
    };

    template <class A, class C, int I, int J, int X, int Y> struct PB
    {
        inline PB(A &a)
        {
            enum { L = X >> 1, M = (X & 1 ? Y : Y + 1) >> 1, IAddL = I + L, XSubL = X - L };
            PB<A, C, I, J, L, M> p0(a);
            PB<A, C, IAddL, J + M, XSubL, Y - M> p1(a);
            PB<A, C, IAddL, J, XSubL, M> p2(a);
        }
    };

    template <class A, class C, int I, int J> struct PB <A, C, I, J, 1, 1>
    {
        inline PB(A &a) { Swap<A, C> s(a, I - 1, J - 1); }
    };

    template <class A, class C, int I, int J> struct PB <A, C, I, J, 1, 2>
    {
        inline PB(A &a) { Swap<A, C> s0(a, I - 1, J); Swap<A, C> s1(a, I - 1, J - 1); }
    };

    template <class A, class C, int I, int J> struct PB <A, C, I, J, 2, 1>
    {
        inline PB(A &a) { Swap<A, C> s0(a, I - 1, J - 1); Swap<A, C> s1(a, I, J - 1); }
    };

    template <class A, class C, int I, int M, bool Stop = false> struct PS
    {
        inline PS(A &a)
        {
            enum { L = M >> 1, IAddL = I + L, MSubL = M - L};
            PS<A, C, I, L, (L <= 1)> ps0(a);
            PS<A, C, IAddL, MSubL, (MSubL <= 1)> ps1(a);
            PB<A, C, I, IAddL, L, MSubL> pb(a);
        }
    };

    template <class A, class C, int I, int M> struct PS <A, C, I, M, true>
    {
        inline PS(A &a) {}
    };

public:
    /**
     * Sorts the array/container arr.
     * \param  arr  The array/container to be sorted.
     */
    template <class Container> inline void operator() (Container &arr) const
    {
        PS<Container, Compare, 1, NumElements, (NumElements <= 1)> ps(arr);
    };

    /**
     * Sorts the array arr.
     * \param  arr  The array to be sorted.
     */
    template <class T> inline void operator() (T *arr) const
    {
        PS<T*, Compare, 1, NumElements, (NumElements <= 1)> ps(arr);
    };
};

#include <iostream>
#include <vector>

int main(int argc, const char * argv[])
{
    enum { NumValues = 10 };

    // Arrays
    {
        int rands[NumValues];
        for (int i = 0; i < NumValues; ++i) rands[i] = rand() % 100;
        std::cout << "Before Sort: \t";
        for (int i = 0; i < NumValues; ++i) std::cout << rands[i] << " ";
        std::cout << "\n";
        StaticSort<NumValues> staticSort;
        staticSort(rands);
        std::cout << "After Sort: \t";
        for (int i = 0; i < NumValues; ++i) std::cout << rands[i] << " ";
        std::cout << "\n";
    }

    std::cout << "\n";

    // STL Vector
    {
        std::vector<int> rands(NumValues);
        for (int i = 0; i < NumValues; ++i) rands[i] = rand() % 100;
        std::cout << "Before Sort: \t";
        for (int i = 0; i < NumValues; ++i) std::cout << rands[i] << " ";
        std::cout << "\n";
        StaticSort<NumValues> staticSort;
        staticSort(rands);
        std::cout << "After Sort: \t";
        for (int i = 0; i < NumValues; ++i) std::cout << rands[i] << " ";
        std::cout << "\n";
    }

    return 0;
}

Lưu ý rằng thay vì một if (compare) swap câu lệnh, chúng tôi mã hóa rõ ràng các toán tử ternary cho min và max. Điều này là để giúp thúc đẩy trình biên dịch sử dụng mã không phân nhánh.

Điểm chuẩn

Các điểm chuẩn sau đây được tổng hợp bằng clang -O3 và chạy trên macbook air giữa năm 2012 của tôi.

Sắp xếp dữ liệu ngẫu nhiên

So sánh nó với mã của DarioP, đây là số mili giây được thực hiện để sắp xếp 1 triệu mảng int 32 bit có kích thước 10:

Hardcoding Sort Net 10: 88,774 ms
Templated Bose-Nelson sort 10: 27.815 ms

Sử dụng phương pháp tạo khuôn mẫu này, chúng tôi cũng có thể tạo các mạng sắp xếp theo thời gian biên dịch cho số lượng phần tử khác.

Thời gian (tính bằng mili giây) để sắp xếp 1 triệu mảng với các kích cỡ khác nhau.
Số mili giây cho các mảng có kích thước 2, 4, 8 lần lượt là 1.943, 8.655, 20.246.
C ++ Định thời gian sắp xếp tĩnh Bose-Nelson

Tín dụng cho Glenn Teitelbaum cho loại chèn không được kiểm soát.

Dưới đây là các đồng hồ trung bình trên mỗi loại cho các mảng nhỏ gồm 6 yếu tố. Mã điểm chuẩn và ví dụ có thể được tìm thấy tại câu hỏi này:
Loại nhanh nhất có độ dài cố định 6 mảng int

Direct call to qsort library function       : 326.81
Naive implementation (insertion sort)       : 132.98
Insertion Sort (Daniel Stutzbach)           : 104.04
Insertion Sort Unrolled                     : 99.64
Insertion Sort Unrolled (Glenn Teitelbaum)  : 81.55
Rank Order                                  : 44.01
Rank Order with registers                   : 42.40
Sorting Networks (Daniel Stutzbach)         : 88.06
Sorting Networks (Paul R)                   : 31.64
Sorting Networks 12 with Fast Swap          : 29.68
Sorting Networks 12 reordered Swap          : 28.61
Reordered Sorting Network w/ fast swap      : 24.63
Templated Sorting Network (this class)      : 25.37

Nó thực hiện nhanh như ví dụ nhanh nhất trong câu hỏi cho 6 yếu tố.

Hiệu suất để sắp xếp dữ liệu được sắp xếp

Thông thường, các mảng đầu vào có thể đã được sắp xếp hoặc chủ yếu được sắp xếp.
Trong những trường hợp như vậy, sắp xếp chèn có thể là sự lựa chọn tốt hơn.

nhập mô tả hình ảnh ở đây

Bạn có thể muốn chọn một thuật toán sắp xếp phù hợp tùy thuộc vào dữ liệu.

Mã được sử dụng cho điểm chuẩn có thể được tìm thấy ở đây .


Bất kỳ cơ hội nào bạn có thể thêm một so sánh cho thuật toán của tôi dưới đây?
Glenn Teitelbaum

@GlennTeitelbaum bất kỳ cơ hội nào bạn đã thêm điều này vào điểm chuẩn của bạn và phương tiện và kết quả được tiết lộ?
greybeard

Kudos để thêm dữ liệu về sắp xếp đầu vào được sắp xếp.
greybeard

Trên một số hệ thống v1 = v0 < v1 ? v1 : v0; // Maxvẫn có thể chi nhánh, trong trường hợp đó nó có thể được thay thế bằng v1 += v0 - tbởi vì nếu tv0sau đó v1 + v0 -t == v1 + v0 - v0 == v1khác tv1v1 + v0 -t == v1 + v0 - v1 == v0
Glenn Teitelbaum

Ternary thường biên dịch thành một maxsshoặc minsshướng dẫn về trình biên dịch hiện đại. Nhưng trong trường hợp nó không hoạt động, có thể sử dụng các cách hoán đổi khác. :)
Vectorized

5

Mặc dù một loại mạng có tỷ lệ tốt là nhanh trên các mảng nhỏ, đôi khi bạn không thể đánh bại sắp xếp chèn nếu được tối ưu hóa đúng cách. Ví dụ, chèn hàng loạt với 2 phần tử:

{
    final int a=in[0]<in[1]?in[0]:in[1];
    final int b=in[0]<in[1]?in[1]:in[0];
    in[0]=a;
    in[1]=b;
}
for(int x=2;x<10;x+=2)
{
    final int a=in[x]<in[x+1]?in[x]:in[x+1];
    final int b=in[x]<in[x+1]?in[x+1]:in[x];
    int y= x-1;

    while(y>=0&&in[y]>b)
    {
        in[y+2]= in[y];
        --y;
    }
    in[y+2]=b;
    while(y>=0&&in[y]>a)
    {
        in[y+1]= in[y];
        --y;
    }
    in[y+1]=a;
}

Không chắc chắn tại sao bạn lặp lại in[y+2]= in[y];, lỗi đánh máy?
Glenn Teitelbaum

Wow, tôi đã làm điều đó như thế nào? Và làm thế nào mà nó mất quá lâu để ai đó chú ý? Câu trả lời: Đó không phải là một lỗi đánh máy: Tôi đã điều chỉnh một thuật toán khác có cả khóa và mảng giá trị.
warren

3

Bạn hoàn toàn có thể hủy đăng ký insertion sort

Để làm cho điều đó dễ dàng hơn, templates đệ quy có thể được sử dụng mà không có chi phí hoạt động. Vì nó đã là một template, intcó thể là một templatetham số là tốt. Điều này cũng làm cho kích thước mảng mã hóa khác hơn 10 tầm thường để tạo.

Lưu ý rằng để sắp xếp int x[10]cuộc gọi là insert_sort<int, 9>::sort(x);do lớp sử dụng chỉ mục của mục cuối cùng. Điều này có thể được gói, nhưng đó sẽ là nhiều mã hơn để đọc qua.

template <class T, int NUM>
class insert_sort;

template <class T>
class insert_sort<T,0>
// stop template recursion
// sorting 1 item is a no-op
{
public:
    static void place(T *x) {}
    static void sort(T * x) {}
};

template <class T, int NUM>
class insert_sort
// use template recursion to do insertion sort
// NUM is the index of the last item, eg. for x[10] call <9>
{
public:
    static void place(T *x)
    {
        T t1=x[NUM-1];
        T t2=x[NUM];
        if (t1 > t2)
        {
            x[NUM-1]=t2;
            x[NUM]=t1;
            insert_sort<T,NUM-1>::place(x);
        }
    }
    static void sort(T * x)
    {
        insert_sort<T,NUM-1>::sort(x); // sort everything before
        place(x);                    // put this item in
    }
};

Trong thử nghiệm của tôi, điều này nhanh hơn các ví dụ mạng sắp xếp.


0

Vì những lý do tương tự như những lý do mà tôi đã mô tả ở đây , các chức năng sắp xếp sau đây sort6_iterator()sort10_iterator_local()sẽ hoạt động tốt, trong đó mạng phân loại được lấy từ đây :

template<class IterType> 
inline void sort10_iterator(IterType it) 
{
#define SORT2(x,y) {if(data##x>data##y)std::swap(data##x,data##y);}
#define DD1(a)   auto data##a=*(data+a);
#define DD2(a,b) auto data##a=*(data+a), data##b=*(data+b);
#define CB1(a)   *(data+a)=data##a;
#define CB2(a,b) *(data+a)=data##a;*(data+b)=data##b;
  DD2(1,4) SORT2(1,4) DD2(7,8) SORT2(7,8) DD2(2,3) SORT2(2,3) DD2(5,6) SORT2(5,6) DD2(0,9) SORT2(0,9) 
  SORT2(2,5) SORT2(0,7) SORT2(8,9) SORT2(3,6) 
  SORT2(4,9) SORT2(0,1) 
  SORT2(0,2) CB1(0) SORT2(6,9) CB1(9) SORT2(3,5) SORT2(4,7) SORT2(1,8) 
  SORT2(3,4) SORT2(5,8) SORT2(6,7) SORT2(1,2) 
  SORT2(7,8) CB1(8) SORT2(1,3) CB1(1) SORT2(2,5) SORT2(4,6) 
  SORT2(2,3) CB1(2) SORT2(6,7) CB1(7) SORT2(4,5) 
  SORT2(3,4) CB2(3,4) SORT2(5,6) CB2(5,6) 
#undef CB1
#undef CB2
#undef DD1
#undef DD2
#undef SORT2
}

Để gọi hàm này, tôi đã truyền cho nó một std::vectoriterator.


0

Một loại sắp xếp chèn yêu cầu trung bình 29,6 so sánh để sắp xếp 10 đầu vào với trường hợp tốt nhất là 9 và tồi tệ nhất là 45 (đầu vào đã cho theo thứ tự ngược lại).

Một shellsort {9,6,1} sẽ yêu cầu trung bình 25,5 so sánh để sắp xếp 10 đầu vào. Trường hợp tốt nhất là 14 so sánh, tệ nhất là 34 và sắp xếp một đầu vào đảo ngược cần 22.

Vì vậy, sử dụng shellsort thay vì sắp xếp chèn giúp giảm 14% trường hợp trung bình. Mặc dù trường hợp tốt nhất được tăng 56%, trường hợp xấu nhất giảm 24%, điều này rất có ý nghĩa trong các ứng dụng trong đó việc kiểm tra hiệu suất của trường hợp xấu nhất là quan trọng. Trường hợp ngược lại giảm 51%.

Vì bạn dường như đã quen thuộc với sắp xếp chèn, bạn có thể triển khai thuật toán dưới dạng mạng sắp xếp cho {9,6} và sau đó xử lý sắp xếp chèn ({1}) sau đó:

i[0] with i[9]    // {9}

i[0] with i[6]    // {6}
i[1] with i[7]    // {6}
i[2] with i[8]    // {6}
i[3] with i[9]    // {6}

i[0 ... 9]        // insertion sort
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.