Loại nhanh nhất có độ dài cố định 6 int int


401

Trả lời một câu hỏi khác về Stack Overflow (câu hỏi này ) Tôi tình cờ gặp một vấn đề phụ thú vị. Cách nhanh nhất để sắp xếp một mảng gồm 6 số nguyên là gì?

Vì câu hỏi ở mức rất thấp:

  • chúng tôi không thể cho rằng các thư viện có sẵn (và bản thân cuộc gọi có chi phí), chỉ đơn giản là C
  • để tránh làm trống đường dẫn lệnh (có chi phí rất cao), có lẽ chúng ta nên giảm thiểu các nhánh, nhảy và mọi loại phá vỡ dòng điều khiển khác (như các ẩn sau các điểm chuỗi trong &&hoặc ||).
  • phòng bị hạn chế và giảm thiểu các thanh ghi và sử dụng bộ nhớ là một vấn đề, lý tưởng nhất là sắp xếp vị trí có lẽ là tốt nhất.

Thực sự câu hỏi này là một loại Golf trong đó mục tiêu không phải là giảm thiểu chiều dài nguồn mà là thời gian thực hiện. Tôi gọi mã nó Zening 'như được sử dụng trong tiêu đề của cuốn sách Thiền của Bộ luật tối ưu hóa bởi Michael Abrash và nó phần tiếp theo .

Về lý do tại sao nó thú vị, có một số lớp:

  • ví dụ đơn giản và dễ hiểu và đo lường, không có nhiều kỹ năng C liên quan
  • nó cho thấy các hiệu ứng của việc lựa chọn một thuật toán tốt cho vấn đề, nhưng cũng có tác dụng của trình biên dịch và phần cứng cơ bản.

Dưới đây là tham chiếu của tôi (ngây thơ, không được tối ưu hóa) và bộ thử nghiệm của tôi.

#include <stdio.h>

static __inline__ int sort6(int * d){

    char j, i, imin;
    int tmp;
    for (j = 0 ; j < 5 ; j++){
        imin = j;
        for (i = j + 1; i < 6 ; i++){
            if (d[i] < d[imin]){
                imin = i;
            }
        }
        tmp = d[j];
        d[j] = d[imin];
        d[imin] = tmp;
    }
}

static __inline__ unsigned long long rdtsc(void)
{
  unsigned long long int x;
     __asm__ volatile (".byte 0x0f, 0x31" : "=A" (x));
     return x;
}

int main(int argc, char ** argv){
    int i;
    int d[6][5] = {
        {1, 2, 3, 4, 5, 6},
        {6, 5, 4, 3, 2, 1},
        {100, 2, 300, 4, 500, 6},
        {100, 2, 3, 4, 500, 6},
        {1, 200, 3, 4, 5, 600},
        {1, 1, 2, 1, 2, 1}
    };

    unsigned long long cycles = rdtsc();
    for (i = 0; i < 6 ; i++){
        sort6(d[i]);
        /*
         * printf("d%d : %d %d %d %d %d %d\n", i,
         *  d[i][0], d[i][6], d[i][7],
         *  d[i][8], d[i][9], d[i][10]);
        */
    }
    cycles = rdtsc() - cycles;
    printf("Time is %d\n", (unsigned)cycles);
}

Kết quả thô

Khi số lượng biến thể đang trở nên lớn, tôi đã tập hợp tất cả chúng trong một bộ thử nghiệm có thể tìm thấy ở đây . Các thử nghiệm thực tế được sử dụng là một chút ngây thơ hơn so với các thử nghiệm ở trên, nhờ Kevin Stock. Bạn có thể biên dịch và thực hiện nó trong môi trường của riêng bạn. Tôi khá thích thú với hành vi trên các trình biên dịch / kiến ​​trúc đích khác nhau. (Các bạn ơi, hãy đặt nó vào câu trả lời, tôi sẽ +1 mỗi người đóng góp cho một kết quả mới).

Tôi đã đưa ra câu trả lời cho Daniel Stutzbach (vì chơi golf) một năm trước khi anh ấy là nguồn giải pháp nhanh nhất tại thời điểm đó (sắp xếp mạng lưới).

Linux 64 bit, gcc 4.6.1 64 bit, Intel Core 2 Duo E8400, -O2

  • Gọi trực tiếp đến chức năng thư viện qsort: 689,38
  • Thực hiện ngây thơ (sắp xếp chèn): 285,70
  • Sắp xếp chèn (Daniel Stutzbach): 142.12
  • Sắp xếp chèn Không được kiểm soát: 125,47
  • Thứ tự xếp hạng: 102,26
  • Thứ tự xếp hạng với các thanh ghi: 58,03
  • Mạng sắp xếp (Daniel Stutzbach): 111,68
  • Mạng sắp xếp (Paul R): 66,36
  • Sắp xếp mạng 12 với Hoán đổi nhanh: 58,86
  • Sắp xếp mạng 12 Hoán đổi được sắp xếp lại: 53,74
  • Sắp xếp mạng 12 được sắp xếp lại Hoán đổi đơn giản: 31,54
  • Mạng sắp xếp được sắp xếp lại với trao đổi nhanh: 31,54
  • Mạng sắp xếp được sắp xếp lại với trao đổi nhanh V2: 33.63
  • Sắp xếp bong bóng nội tuyến (Paolo Bonzini): 48,85
  • Sắp xếp chèn không được kiểm soát (Paolo Bonzini): 75.30

Linux 64 bit, gcc 4.6.1 64 bit, Intel Core 2 Duo E8400, -O1

  • Gọi trực tiếp đến chức năng thư viện qsort: 705.93
  • Thực hiện ngây thơ (sắp xếp chèn): 135,60
  • Sắp xếp chèn (Daniel Stutzbach): 142.11
  • Sắp xếp chèn Không được kiểm soát: 126,75
  • Thứ tự xếp hạng: 46,42
  • Thứ tự xếp hạng với các thanh ghi: 43,58
  • Mạng sắp xếp (Daniel Stutzbach): 115,57
  • Mạng sắp xếp (Paul R): 64,44
  • Sắp xếp mạng 12 với Hoán đổi nhanh: 61,98
  • Mạng sắp xếp 12 Hoán đổi được sắp xếp lại: 54,67
  • Sắp xếp mạng 12 được sắp xếp lại Hoán đổi đơn giản: 31,54
  • Mạng sắp xếp được sắp xếp lại w / trao đổi nhanh: 31.24
  • Mạng sắp xếp được sắp xếp lại w / trao đổi nhanh V2: 33,07
  • Sắp xếp bong bóng nội tuyến (Paolo Bonzini): 45,79
  • Sắp xếp chèn không được kiểm soát (Paolo Bonzini): 80,15

Tôi đã bao gồm cả kết quả -O1 và -O2 vì đáng ngạc nhiên đối với một số chương trình O2 kém hiệu quả hơn O1. Tôi tự hỏi những gì tối ưu hóa cụ thể có hiệu ứng này?

Nhận xét về các giải pháp được đề xuất

Sắp xếp chèn (Daniel Stutzbach)

Như dự kiến ​​giảm thiểu các chi nhánh thực sự là một ý tưởng tốt.

Mạng sắp xếp (Daniel Stutzbach)

Tốt hơn so với sắp xếp chèn. Tôi tự hỏi nếu hiệu ứng chính không nhận được từ việc tránh vòng lặp bên ngoài. Tôi đã thử nó bằng cách sắp xếp chèn không kiểm soát để kiểm tra và thực sự chúng tôi nhận được các số liệu tương tự (mã ở đây ).

Mạng sắp xếp (Paul R)

Tốt nhất cho đến nay. Mã thực tế tôi sử dụng để kiểm tra là ở đây . Vẫn chưa biết tại sao nó lại nhanh gấp gần hai lần so với việc triển khai mạng sắp xếp khác. Thông số đi qua? Tối đa nhanh?

Sắp xếp mạng 12 SWAP với hoán đổi nhanh

Theo đề xuất của Daniel Stutzbach, tôi đã kết hợp mạng phân loại trao đổi 12 của anh ấy với trao đổi nhanh không phân nhánh (mã ở đây ). Nó thực sự nhanh hơn, tốt nhất cho đến nay với một mức lãi nhỏ (khoảng 5%) như có thể được dự kiến ​​bằng cách sử dụng 1 trao đổi ít hơn.

Cũng rất thú vị khi nhận thấy rằng hoán đổi không phân nhánh dường như kém hiệu quả hơn (4 lần) so với cách đơn giản sử dụng nếu trên kiến ​​trúc PPC.

Gọi thư viện qsort

Để đưa ra một điểm tham chiếu khác, tôi cũng đã thử như đề xuất chỉ gọi thư viện qsort (mã ở đây ). Như mong đợi, nó chậm hơn nhiều: chậm hơn 10 đến 30 lần ... vì nó trở nên rõ ràng với bộ thử nghiệm mới, vấn đề chính dường như là tải ban đầu của thư viện sau cuộc gọi đầu tiên và nó không quá kém so với các cuộc gọi khác phiên bản. Nó chỉ chậm hơn từ 3 đến 20 lần trên Linux của tôi. Trên một số kiến ​​trúc được sử dụng để kiểm tra bởi những người khác, nó dường như còn nhanh hơn (tôi thực sự ngạc nhiên về điều đó, vì thư viện qsort sử dụng API phức tạp hơn).

Thứ tự xếp hạng

Rex Kerr đã đề xuất một phương pháp hoàn toàn khác: cho mỗi mục của mảng tính trực tiếp vị trí cuối cùng của nó. Điều này là hiệu quả vì thứ tự tính toán thứ hạng không cần chi nhánh. Hạn chế của phương pháp này là phải mất gấp ba lần dung lượng bộ nhớ của mảng (một bản sao của mảng và biến để lưu trữ thứ tự xếp hạng). Kết quả thực hiện rất đáng ngạc nhiên (và thú vị). Trên kiến ​​trúc tham chiếu của tôi với HĐH 32 bit và Intel Core2 Quad E8300, số chu kỳ hơi thấp hơn 1000 (như sắp xếp các mạng với trao đổi phân nhánh). Nhưng khi được biên dịch và thực thi trên hộp 64 bit của tôi (Intel Core2 Duo), nó hoạt động tốt hơn nhiều: nó trở thành nhanh nhất cho đến nay. Cuối cùng tôi đã tìm ra lý do thực sự. Hộp 32 bit của tôi sử dụng gcc 4.4.1 và hộp 64 bit của tôi gcc 4.4.

cập nhật :

Như các số liệu được công bố ở trên cho thấy hiệu ứng này vẫn được tăng cường bởi các phiên bản sau của gcc và Thứ tự xếp hạng trở nên nhanh gấp đôi so với bất kỳ giải pháp thay thế nào khác.

Sắp xếp Mạng 12 với Hoán đổi được sắp xếp lại

Hiệu quả đáng kinh ngạc của đề xuất Rex Kerr với gcc 4.4.3 khiến tôi tự hỏi: làm thế nào một chương trình có mức sử dụng bộ nhớ gấp 3 lần có thể nhanh hơn các mạng sắp xếp không phân nhánh? Giả thuyết của tôi là nó có ít sự phụ thuộc của kiểu đọc sau khi viết, cho phép sử dụng tốt hơn bộ lập lịch hướng dẫn superscalar của x86. Điều đó đã cho tôi một ý tưởng: sắp xếp lại các giao dịch hoán đổi để giảm thiểu đọc sau khi viết phụ thuộc. Nói một cách đơn giản hơn: khi bạn thực hiện, SWAP(1, 2); SWAP(0, 2);bạn phải đợi cho phép hoán đổi đầu tiên kết thúc trước khi thực hiện lần thứ hai vì cả hai đều truy cập vào một ô nhớ chung. Khi bạn làm SWAP(1, 2); SWAP(4, 5);bộ xử lý có thể thực hiện cả hai song song. Tôi đã thử nó và nó hoạt động như mong đợi, các mạng sắp xếp đang chạy nhanh hơn khoảng 10%.

Sắp xếp mạng 12 với hoán đổi đơn giản

Một năm sau bài viết gốc Steinar H. Gunderson đề xuất, chúng ta không nên cố gắng vượt qua trình biên dịch và giữ cho mã hoán đổi đơn giản. Đó thực sự là một ý tưởng tốt vì mã kết quả nhanh hơn khoảng 40%! Ông cũng đề xuất một trao đổi được tối ưu hóa bằng tay bằng cách sử dụng mã lắp ráp nội tuyến x86 mà vẫn có thể dự phòng thêm một số chu kỳ. Điều đáng ngạc nhiên nhất (nó nói về khối lượng tâm lý của lập trình viên) là một năm trước, không ai từng sử dụng phiên bản hoán đổi đó. Mã tôi đã sử dụng để kiểm tra là ở đây . Những người khác đề xuất các cách khác để viết một trao đổi nhanh C, nhưng nó mang lại hiệu suất tương tự như cách đơn giản với một trình biên dịch hợp lý.

Mã "tốt nhất" hiện tại như sau:

static inline void sort6_sorting_network_simple_swap(int * d){
#define min(x, y) (x<y?x:y)
#define max(x, y) (x<y?y:x) 
#define SWAP(x,y) { const int a = min(d[x], d[y]); \
                    const int b = max(d[x], d[y]); \
                    d[x] = a; d[y] = b; }
    SWAP(1, 2);
    SWAP(4, 5);
    SWAP(0, 2);
    SWAP(3, 5);
    SWAP(0, 1);
    SWAP(3, 4);
    SWAP(1, 4);
    SWAP(0, 3);
    SWAP(2, 5);
    SWAP(1, 3);
    SWAP(2, 4);
    SWAP(2, 3);
#undef SWAP
#undef min
#undef max
}

Nếu chúng tôi tin rằng bộ thử nghiệm của chúng tôi (và, vâng, nó khá kém, đó chỉ là lợi ích ngắn gọn, đơn giản và dễ hiểu những gì chúng tôi đang đo), số chu kỳ trung bình của mã kết quả cho một loại là dưới 40 chu kỳ ( 6 bài kiểm tra được thực hiện). Điều đó đặt mỗi trao đổi ở trung bình 4 chu kỳ. Tôi gọi đó là nhanh đáng kinh ngạc. Bất kỳ cải tiến khác có thể?


2
Bạn có một số hạn chế trên ints? Ví dụ: chúng ta có thể giả sử rằng với bất kỳ 2 x, y x-yx+ysẽ không gây ra tràn hoặc tràn?
Matthieu M.

3
Bạn nên thử kết hợp mạng sắp xếp 12 trao đổi của tôi với chức năng hoán đổi không nhánh của Paul. Giải pháp của anh ta chuyển tất cả các tham số dưới dạng các phần tử riêng biệt trên ngăn xếp thay vì một con trỏ đơn lẻ vào một mảng. Điều đó cũng có thể làm cho một sự khác biệt.
Daniel Stutzbach

2
Lưu ý rằng việc triển khai chính xác của ndtsc trên 64 bit là __asm__ volatile (".byte 0x0f, 0x31; shlq $32, %%rdx; orq %%rdx, %0" : "=a" (x) : : "rdx");bởi vì ndtsc đặt câu trả lời trong EDX: EAX trong khi GCC mong đợi nó trong một thanh ghi 64 bit duy nhất. Bạn có thể thấy lỗi bằng cách biên dịch tại -O3. Cũng xem bên dưới nhận xét của tôi cho Paul R về SWAP nhanh hơn.
Paolo Bonzini

3
@Tyler: Làm thế nào để bạn thực hiện nó ở cấp độ lắp ráp mà không có chi nhánh?
Loren Pechtel

4
@Loren: CMP EAX, EBX; SBB EAX, EAXsẽ đặt 0 hoặc 0xFFFFFFFF EAXtùy thuộc vào việc EAXlớn hơn hoặc nhỏ hơn EBXtương ứng. SBBlà "trừ bằng vay", đối tác của ADC("thêm bằng mang"); bit trạng thái bạn đề cập đến bit mang. Sau đó, một lần nữa, tôi nhớ rằng ADCSBBcó độ trễ khủng khiếp & thông trên Pentium 4 vs ADDSUB, và vẫn còn gấp đôi chậm trên CPU Core. Vì 80386 cũng có hướng dẫn SETcclưu trữ có CMOVccđiều kiện và di chuyển có điều kiện, nhưng chúng cũng chậm.
j_random_hacker

Câu trả lời:


162

Đối với bất kỳ tối ưu hóa, luôn luôn tốt nhất để kiểm tra, thử nghiệm, kiểm tra. Tôi sẽ thử ít nhất là sắp xếp các mạng và sắp xếp chèn. Nếu tôi đang đặt cược, tôi sẽ đặt tiền của mình vào loại chèn dựa trên kinh nghiệm trong quá khứ.

Bạn có biết gì về dữ liệu đầu vào không? Một số thuật toán sẽ hoạt động tốt hơn với một số loại dữ liệu nhất định. Ví dụ, sắp xếp chèn thực hiện tốt hơn trên dữ liệu được sắp xếp hoặc sắp xếp gần đúng, vì vậy nó sẽ là lựa chọn tốt hơn nếu có cơ hội dữ liệu gần như sắp xếp trên trung bình.

Thuật toán bạn đã đăng tương tự như một loại chèn, nhưng có vẻ như bạn đã giảm thiểu số lượng giao dịch hoán đổi với chi phí so sánh nhiều hơn. Tuy nhiên, so sánh đắt hơn nhiều so với giao dịch hoán đổi, bởi vì các nhánh có thể khiến đường ống dẫn bị đình trệ.

Đây là một triển khai sắp xếp chèn:

static __inline__ int sort6(int *d){
        int i, j;
        for (i = 1; i < 6; i++) {
                int tmp = d[i];
                for (j = i; j >= 1 && tmp < d[j-1]; j--)
                        d[j] = d[j-1];
                d[j] = tmp;
        }
}

Đây là cách tôi xây dựng một mạng sắp xếp. Đầu tiên, sử dụng trang web này để tạo một bộ macro SWAP tối thiểu cho một mạng có độ dài phù hợp. Gói nó trong một chức năng cho tôi:

static __inline__ int sort6(int * d){
#define SWAP(x,y) if (d[y] < d[x]) { int tmp = d[x]; d[x] = d[y]; d[y] = tmp; }
    SWAP(1, 2);
    SWAP(0, 2);
    SWAP(0, 1);
    SWAP(4, 5);
    SWAP(3, 5);
    SWAP(3, 4);
    SWAP(0, 3);
    SWAP(1, 4);
    SWAP(2, 5);
    SWAP(2, 4);
    SWAP(1, 3);
    SWAP(2, 3);
#undef SWAP
}

9
+1: thật tuyệt, bạn đã thực hiện nó với 12 trao đổi thay vì 13 trao đổi trong mạng được mã hóa bằng tay và theo kinh nghiệm của tôi ở trên. Tôi sẽ cung cấp cho bạn +1 khác nếu tôi có thể cho liên kết đến trang web tạo mạng cho bạn - hiện được đánh dấu.
Paul R

9
Đây là một ý tưởng tuyệt vời cho một chức năng sắp xếp mục đích chung nếu bạn mong đợi phần lớn các yêu cầu là các mảng có kích thước nhỏ. Sử dụng câu lệnh chuyển đổi cho các trường hợp bạn muốn tối ưu hóa, sử dụng quy trình này; hãy để trường hợp mặc định sử dụng hàm sắp xếp thư viện.
Đánh dấu tiền chuộc

5
@Mark Một chức năng sắp xếp thư viện tốt sẽ có đường dẫn nhanh cho các mảng nhỏ. Nhiều thư viện hiện đại sẽ sử dụng QuickSort đệ quy hoặc MergeSort chuyển sang InsertsSort sau khi đệ quy xuống n < SMALL_CONSTANT.
Daniel Stutzbach

3
@Mark Vâng, một chức năng sắp xếp thư viện C yêu cầu bạn chỉ định thao tác so sánh thông qua một porter chức năng. Chi phí chung của việc gọi một chức năng cho mọi so sánh là rất lớn. Thông thường, đó vẫn là cách sạch nhất để đi, bởi vì đây hiếm khi là một con đường quan trọng trong chương trình. Tuy nhiên, nếu đó là con đường quan trọng, chúng tôi thực sự có thể sắp xếp nhanh hơn nhiều nếu chúng tôi biết chúng tôi đang sắp xếp các số nguyên và chính xác là 6 số. :)
Daniel Stutzbach

7
@tgwh: Trao đổi XOR hầu như luôn là một ý tưởng tồi.
Paul R

63

Đây là một triển khai sử dụng các mạng sắp xếp :

inline void Sort2(int *p0, int *p1)
{
    const int temp = min(*p0, *p1);
    *p1 = max(*p0, *p1);
    *p0 = temp;
}

inline void Sort3(int *p0, int *p1, int *p2)
{
    Sort2(p0, p1);
    Sort2(p1, p2);
    Sort2(p0, p1);
}

inline void Sort4(int *p0, int *p1, int *p2, int *p3)
{
    Sort2(p0, p1);
    Sort2(p2, p3);
    Sort2(p0, p2);  
    Sort2(p1, p3);  
    Sort2(p1, p2);  
}

inline void Sort6(int *p0, int *p1, int *p2, int *p3, int *p4, int *p5)
{
    Sort3(p0, p1, p2);
    Sort3(p3, p4, p5);
    Sort2(p0, p3);  
    Sort2(p2, p5);  
    Sort4(p1, p2, p3, p4);  
}

Bạn thực sự cần cành rất hiệu quả minmaxtriển khai cho điều này, vì đó là một cách hiệu quả những gì mã này sôi xuống - một chuỗi các minmaxcác hoạt động (13 của mỗi, trong tổng số). Tôi để điều này như một bài tập cho người đọc.

Lưu ý rằng việc triển khai này dễ dàng cho phép vector hóa (ví dụ SIMD - hầu hết các SIMD SIMD đều có hướng dẫn tối thiểu / tối đa vectơ) và cả việc triển khai GPU (ví dụ CUDA - không phân nhánh không có vấn đề với phân kỳ dọc v.v.).

Xem thêm: Thực hiện thuật toán nhanh để sắp xếp danh sách rất nhỏ


1
Đối với một số hack cho min / max: Graphics.stanford.edu/~seander/bithacks.html#IntegerMinOrMax
Rubys

1
@Paul: trong bối cảnh sử dụng CUDA thực, đó chắc chắn là câu trả lời tốt nhất. Tôi sẽ kiểm tra xem nó cũng là (và bao nhiêu) trong bối cảnh golf x64 và kết quả công bố.
kriss

1
Sort3sẽ nhanh hơn (trên hầu hết các kiến ​​trúc, dù sao) nếu bạn lưu ý rằng đó (a+b+c)-(min+max)là số trung tâm.
Rex Kerr

1
@Rex: Tôi thấy - có vẻ tốt. Đối với các kiến ​​trúc SIMD như AltiVec và SSE, nó sẽ có cùng số chu kỳ hướng dẫn (tối đa và tối thiểu là các hướng dẫn chu trình đơn như cộng / trừ), nhưng đối với CPU vô hướng bình thường, phương pháp của bạn trông tốt hơn.
Paul R

2
Nếu tôi để GCC tối ưu hóa tối thiểu với các hướng dẫn di chuyển có điều kiện, tôi sẽ tăng tốc 33% : #define SWAP(x,y) { int dx = d[x], dy = d[y], tmp; tmp = d[x] = dx < dy ? dx : dy; d[y] ^= dx ^ tmp; }. Ở đây tôi không sử dụng ?: Cho d [y] vì nó cho hiệu suất kém hơn một chút, nhưng nó gần như bị nhiễu.
Paolo Bonzini

45

Vì đây là các số nguyên và so sánh là nhanh, tại sao không tính thứ tự xếp hạng của từng trực tiếp:

inline void sort6(int *d) {
  int e[6];
  memcpy(e,d,6*sizeof(int));
  int o0 = (d[0]>d[1])+(d[0]>d[2])+(d[0]>d[3])+(d[0]>d[4])+(d[0]>d[5]);
  int o1 = (d[1]>=d[0])+(d[1]>d[2])+(d[1]>d[3])+(d[1]>d[4])+(d[1]>d[5]);
  int o2 = (d[2]>=d[0])+(d[2]>=d[1])+(d[2]>d[3])+(d[2]>d[4])+(d[2]>d[5]);
  int o3 = (d[3]>=d[0])+(d[3]>=d[1])+(d[3]>=d[2])+(d[3]>d[4])+(d[3]>d[5]);
  int o4 = (d[4]>=d[0])+(d[4]>=d[1])+(d[4]>=d[2])+(d[4]>=d[3])+(d[4]>d[5]);
  int o5 = 15-(o0+o1+o2+o3+o4);
  d[o0]=e[0]; d[o1]=e[1]; d[o2]=e[2]; d[o3]=e[3]; d[o4]=e[4]; d[o5]=e[5];
}

@Rex: với gcc -O1, nó dưới 1000 chu kỳ, khá nhanh nhưng chậm hơn so với sắp xếp mạng. Bất kỳ ý tưởng để cải thiện mã? Có lẽ nếu chúng ta có thể tránh được việc sao chép mảng ...
kriss

@kriss: Nó nhanh hơn mạng sắp xếp đối với tôi với -O2. Có một số lý do tại sao -O2 không ổn, hoặc nó cũng chậm hơn đối với -O2? Có lẽ đó là một sự khác biệt trong kiến ​​trúc máy?
Rex Kerr

1
@Rex: xin lỗi, tôi đã bỏ lỡ mẫu> vs> = từ cái nhìn đầu tiên. Nó hoạt động trong mọi trường hợp.
kriss

3
@kriss: Aha. Điều đó không hoàn toàn đáng ngạc nhiên - có rất nhiều biến số trôi nổi xung quanh, và chúng phải được sắp xếp cẩn thận và lưu vào bộ nhớ cache trong sổ đăng ký, v.v.
Rex Kerr

2
@SSpoke 0+1+2+3+4+5=15Vì một trong số chúng bị thiếu, 15 trừ đi tổng số phần còn lại mang lại thiếu một
Glenn Teitelbaum

35

Có vẻ như tôi đã đến bữa tiệc muộn một năm, nhưng ở đây chúng tôi đi ...

Nhìn vào tổ hợp được tạo bởi gcc 4.5.2, tôi nhận thấy rằng tải và lưu trữ đang được thực hiện cho mỗi lần hoán đổi, điều này thực sự không cần thiết. Sẽ tốt hơn nếu tải 6 giá trị vào các thanh ghi, sắp xếp chúng và lưu chúng trở lại vào bộ nhớ. Tôi đã ra lệnh cho các tải tại các cửa hàng càng gần càng tốt để các thanh ghi được sử dụng trước tiên và được sử dụng lần cuối. Tôi cũng đã sử dụng macro SWAP của Steinar H. Gunderson. Cập nhật: Tôi đã chuyển sang macro SWAP của Paolo Bonzini mà gcc chuyển đổi thành một thứ tương tự như của Arnolderson, nhưng gcc có thể đặt hàng tốt hơn các hướng dẫn vì chúng không được cung cấp dưới dạng lắp ráp rõ ràng.

Tôi đã sử dụng thứ tự trao đổi tương tự như mạng trao đổi được sắp xếp lại được đưa ra là hoạt động tốt nhất, mặc dù có thể có thứ tự tốt hơn. Nếu tôi tìm thấy thêm thời gian tôi sẽ tạo và kiểm tra một loạt các hoán vị.

Tôi đã thay đổi mã kiểm tra để xem xét hơn 4000 mảng và hiển thị số chu kỳ trung bình cần thiết để sắp xếp từng mảng. Trên i5-650, tôi nhận được ~ 34,1 chu kỳ / sắp xếp (sử dụng -O3), so với mạng sắp xếp được sắp xếp lại ban đầu nhận được ~ 65,3 chu kỳ / sắp xếp (sử dụng -O1, nhịp -O2 và -O3).

#include <stdio.h>

static inline void sort6_fast(int * d) {
#define SWAP(x,y) { int dx = x, dy = y, tmp; tmp = x = dx < dy ? dx : dy; y ^= dx ^ tmp; }
    register int x0,x1,x2,x3,x4,x5;
    x1 = d[1];
    x2 = d[2];
    SWAP(x1, x2);
    x4 = d[4];
    x5 = d[5];
    SWAP(x4, x5);
    x0 = d[0];
    SWAP(x0, x2);
    x3 = d[3];
    SWAP(x3, x5);
    SWAP(x0, x1);
    SWAP(x3, x4);
    SWAP(x1, x4);
    SWAP(x0, x3);
    d[0] = x0;
    SWAP(x2, x5);
    d[5] = x5;
    SWAP(x1, x3);
    d[1] = x1;
    SWAP(x2, x4);
    d[4] = x4;
    SWAP(x2, x3);
    d[2] = x2;
    d[3] = x3;

#undef SWAP
#undef min
#undef max
}

static __inline__ unsigned long long rdtsc(void)
{
    unsigned long long int x;
    __asm__ volatile ("rdtsc; shlq $32, %%rdx; orq %%rdx, %0" : "=a" (x) : : "rdx");
    return x;
}

void ran_fill(int n, int *a) {
    static int seed = 76521;
    while (n--) *a++ = (seed = seed *1812433253 + 12345);
}

#define NTESTS 4096
int main() {
    int i;
    int d[6*NTESTS];
    ran_fill(6*NTESTS, d);

    unsigned long long cycles = rdtsc();
    for (i = 0; i < 6*NTESTS ; i+=6) {
        sort6_fast(d+i);
    }
    cycles = rdtsc() - cycles;
    printf("Time is %.2lf\n", (double)cycles/(double)NTESTS);

    for (i = 0; i < 6*NTESTS ; i+=6) {
        if (d[i+0] > d[i+1] || d[i+1] > d[i+2] || d[i+2] > d[i+3] || d[i+3] > d[i+4] || d[i+4] > d[i+5])
            printf("d%d : %d %d %d %d %d %d\n", i,
                    d[i+0], d[i+1], d[i+2],
                    d[i+3], d[i+4], d[i+5]);
    }
    return 0;
}

Tôi đã thay đổi bộ kiểm thử để báo cáo đồng hồ theo từng loại và chạy thêm các bài kiểm tra (chức năng cmp đã được cập nhật để xử lý tràn số nguyên), đây là kết quả trên một số kiến ​​trúc khác nhau. Tôi đã thử kiểm tra trên cpu AMD nhưng không có độ tin cậy trên X6 1100T.

Clarkdale (i5-650)
==================
Direct call to qsort library function      635.14   575.65   581.61   577.76   521.12
Naive implementation (insertion sort)      538.30   135.36   134.89   240.62   101.23
Insertion Sort (Daniel Stutzbach)          424.48   159.85   160.76   152.01   151.92
Insertion Sort Unrolled                    339.16   125.16   125.81   129.93   123.16
Rank Order                                 184.34   106.58   54.74    93.24    94.09
Rank Order with registers                  127.45   104.65   53.79    98.05    97.95
Sorting Networks (Daniel Stutzbach)        269.77   130.56   128.15   126.70   127.30
Sorting Networks (Paul R)                  551.64   103.20   64.57    73.68    73.51
Sorting Networks 12 with Fast Swap         321.74   61.61    63.90    67.92    67.76
Sorting Networks 12 reordered Swap         318.75   60.69    65.90    70.25    70.06
Reordered Sorting Network w/ fast swap     145.91   34.17    32.66    32.22    32.18

Kentsfield (Core 2 Quad)
========================
Direct call to qsort library function      870.01   736.39   723.39   725.48   721.85
Naive implementation (insertion sort)      503.67   174.09   182.13   284.41   191.10
Insertion Sort (Daniel Stutzbach)          345.32   152.84   157.67   151.23   150.96
Insertion Sort Unrolled                    316.20   133.03   129.86   118.96   105.06
Rank Order                                 164.37   138.32   46.29    99.87    99.81
Rank Order with registers                  115.44   116.02   44.04    116.04   116.03
Sorting Networks (Daniel Stutzbach)        230.35   114.31   119.15   110.51   111.45
Sorting Networks (Paul R)                  498.94   77.24    63.98    62.17    65.67
Sorting Networks 12 with Fast Swap         315.98   59.41    58.36    60.29    55.15
Sorting Networks 12 reordered Swap         307.67   55.78    51.48    51.67    50.74
Reordered Sorting Network w/ fast swap     149.68   31.46    30.91    31.54    31.58

Sandy Bridge (i7-2600k)
=======================
Direct call to qsort library function      559.97   451.88   464.84   491.35   458.11
Naive implementation (insertion sort)      341.15   160.26   160.45   154.40   106.54
Insertion Sort (Daniel Stutzbach)          284.17   136.74   132.69   123.85   121.77
Insertion Sort Unrolled                    239.40   110.49   114.81   110.79   117.30
Rank Order                                 114.24   76.42    45.31    36.96    36.73
Rank Order with registers                  105.09   32.31    48.54    32.51    33.29
Sorting Networks (Daniel Stutzbach)        210.56   115.68   116.69   107.05   124.08
Sorting Networks (Paul R)                  364.03   66.02    61.64    45.70    44.19
Sorting Networks 12 with Fast Swap         246.97   41.36    59.03    41.66    38.98
Sorting Networks 12 reordered Swap         235.39   38.84    47.36    38.61    37.29
Reordered Sorting Network w/ fast swap     115.58   27.23    27.75    27.25    26.54

Nehalem (Xeon E5640)
====================
Direct call to qsort library function      911.62   890.88   681.80   876.03   872.89
Naive implementation (insertion sort)      457.69   236.87   127.68   388.74   175.28
Insertion Sort (Daniel Stutzbach)          317.89   279.74   147.78   247.97   245.09
Insertion Sort Unrolled                    259.63   220.60   116.55   221.66   212.93
Rank Order                                 140.62   197.04   52.10    163.66   153.63
Rank Order with registers                  84.83    96.78    50.93    109.96   54.73
Sorting Networks (Daniel Stutzbach)        214.59   220.94   118.68   120.60   116.09
Sorting Networks (Paul R)                  459.17   163.76   56.40    61.83    58.69
Sorting Networks 12 with Fast Swap         284.58   95.01    50.66    53.19    55.47
Sorting Networks 12 reordered Swap         281.20   96.72    44.15    56.38    54.57
Reordered Sorting Network w/ fast swap     128.34   50.87    26.87    27.91    28.02

Ý tưởng của bạn về các biến đăng ký nên được áp dụng cho giải pháp "Thứ tự xếp hạng" của Rex Kerr. Đó phải là nhanh nhất, và có lẽ sau đó -O3tối ưu hóa sẽ không phản tác dụng.
cdunn2001

1
@ cdunn2001 Tôi vừa thử nghiệm nó, tôi không thấy sự cải thiện (ngoại trừ một vài chu kỳ tại -O0 và -Os). Nhìn vào asm, có vẻ như gcc đã tìm cách sử dụng các thanh ghi và loại bỏ lệnh gọi tới memcpy.
Kevin Stock

Bạn có phiền khi thêm phiên bản hoán đổi đơn giản vào bộ thử nghiệm của mình không, tôi đoán sẽ rất thú vị khi so sánh nó với việc hoán đổi nhanh lắp ráp được tối ưu hóa bằng tay.
kriss

1
Mã của bạn vẫn sử dụng trao đổi của Gunderson, của tôi sẽ là #define SWAP(x,y) { int oldx = x; x = x < y ? x : y; y ^= oldx ^ x; }.
Paolo Bonzini

@Paolo Bonzini: Vâng, tôi dự định thêm một trường hợp thử nghiệm với bạn, chỉ là chưa có thời gian. Nhưng tôi sẽ tránh lắp ráp nội tuyến.
kriss

15

Tôi đã vấp phải câu hỏi này từ Google vài ngày trước bởi vì tôi cũng có nhu cầu sắp xếp nhanh chóng một mảng có độ dài cố định gồm 6 số nguyên. Tuy nhiên, trong trường hợp của tôi, số nguyên của tôi chỉ có 8 bit (thay vì 32) và tôi không có yêu cầu khắt khe chỉ sử dụng C. Tôi nghĩ rằng dù sao tôi cũng sẽ chia sẻ những phát hiện của mình, trong trường hợp chúng có thể hữu ích cho ai đó ...

Tôi đã triển khai một biến thể của một loại mạng lắp ráp sử dụng SSE để vector hóa các hoạt động so sánh và trao đổi, trong phạm vi có thể. Phải mất sáu "lượt" để sắp xếp hoàn toàn mảng. Tôi đã sử dụng một cơ chế mới để chuyển đổi trực tiếp kết quả của PCMPGTB (so sánh véc tơ) sang các tham số xáo trộn cho PSHUFB (hoán đổi véc tơ), chỉ sử dụng PADDB (thêm vectơ) và trong một số trường hợp cũng là lệnh PAND (bitwise AND).

Cách tiếp cận này cũng có tác dụng phụ của việc mang lại một chức năng thực sự không phân nhánh. Không có hướng dẫn nhảy nào.

Có vẻ như việc thực hiện này nhanh hơn khoảng 38% so với việc triển khai hiện được đánh dấu là tùy chọn nhanh nhất trong câu hỏi ("Sắp xếp mạng 12 bằng cách hoán đổi đơn giản"). Tôi đã sửa đổi việc triển khai đó để sử dụng charcác phần tử mảng trong quá trình thử nghiệm của mình, để làm cho sự so sánh công bằng.

Tôi nên lưu ý rằng phương pháp này có thể được áp dụng cho bất kỳ kích thước mảng nào lên tới 16 phần tử. Tôi hy vọng lợi thế tốc độ tương đối so với các lựa chọn thay thế sẽ phát triển lớn hơn cho các mảng lớn hơn.

Mã được viết bằng MASM cho bộ xử lý x86_64 với SSSE3. Hàm sử dụng quy ước gọi Windows x64 "mới". Đây là ...

PUBLIC simd_sort_6

.DATA

ALIGN 16

pass1_shuffle   OWORD   0F0E0D0C0B0A09080706040503010200h
pass1_add       OWORD   0F0E0D0C0B0A09080706050503020200h
pass2_shuffle   OWORD   0F0E0D0C0B0A09080706030405000102h
pass2_and       OWORD   00000000000000000000FE00FEFE00FEh
pass2_add       OWORD   0F0E0D0C0B0A09080706050405020102h
pass3_shuffle   OWORD   0F0E0D0C0B0A09080706020304050001h
pass3_and       OWORD   00000000000000000000FDFFFFFDFFFFh
pass3_add       OWORD   0F0E0D0C0B0A09080706050404050101h
pass4_shuffle   OWORD   0F0E0D0C0B0A09080706050100020403h
pass4_and       OWORD   0000000000000000000000FDFD00FDFDh
pass4_add       OWORD   0F0E0D0C0B0A09080706050403020403h
pass5_shuffle   OWORD   0F0E0D0C0B0A09080706050201040300h
pass5_and       OWORD 0000000000000000000000FEFEFEFE00h
pass5_add       OWORD   0F0E0D0C0B0A09080706050403040300h
pass6_shuffle   OWORD   0F0E0D0C0B0A09080706050402030100h
pass6_add       OWORD   0F0E0D0C0B0A09080706050403030100h

.CODE

simd_sort_6 PROC FRAME

    .endprolog

    ; pxor xmm4, xmm4
    ; pinsrd xmm4, dword ptr [rcx], 0
    ; pinsrb xmm4, byte ptr [rcx + 4], 4
    ; pinsrb xmm4, byte ptr [rcx + 5], 5
    ; The benchmarked 38% faster mentioned in the text was with the above slower sequence that tied up the shuffle port longer.  Same on extract
    ; avoiding pins/extrb also means we don't need SSE 4.1, but SSSE3 CPUs without SSE4.1 (e.g. Conroe/Merom) have slow pshufb.
    movd    xmm4, dword ptr [rcx]
    pinsrw  xmm4,  word ptr [rcx + 4], 2  ; word 2 = bytes 4 and 5


    movdqa xmm5, xmm4
    pshufb xmm5, oword ptr [pass1_shuffle]
    pcmpgtb xmm5, xmm4
    paddb xmm5, oword ptr [pass1_add]
    pshufb xmm4, xmm5

    movdqa xmm5, xmm4
    pshufb xmm5, oword ptr [pass2_shuffle]
    pcmpgtb xmm5, xmm4
    pand xmm5, oword ptr [pass2_and]
    paddb xmm5, oword ptr [pass2_add]
    pshufb xmm4, xmm5

    movdqa xmm5, xmm4
    pshufb xmm5, oword ptr [pass3_shuffle]
    pcmpgtb xmm5, xmm4
    pand xmm5, oword ptr [pass3_and]
    paddb xmm5, oword ptr [pass3_add]
    pshufb xmm4, xmm5

    movdqa xmm5, xmm4
    pshufb xmm5, oword ptr [pass4_shuffle]
    pcmpgtb xmm5, xmm4
    pand xmm5, oword ptr [pass4_and]
    paddb xmm5, oword ptr [pass4_add]
    pshufb xmm4, xmm5

    movdqa xmm5, xmm4
    pshufb xmm5, oword ptr [pass5_shuffle]
    pcmpgtb xmm5, xmm4
    pand xmm5, oword ptr [pass5_and]
    paddb xmm5, oword ptr [pass5_add]
    pshufb xmm4, xmm5

    movdqa xmm5, xmm4
    pshufb xmm5, oword ptr [pass6_shuffle]
    pcmpgtb xmm5, xmm4
    paddb xmm5, oword ptr [pass6_add]
    pshufb xmm4, xmm5

    ;pextrd dword ptr [rcx], xmm4, 0    ; benchmarked with this
    ;pextrb byte ptr [rcx + 4], xmm4, 4 ; slower version
    ;pextrb byte ptr [rcx + 5], xmm4, 5
    movd   dword ptr [rcx], xmm4
    pextrw  word ptr [rcx + 4], xmm4, 2  ; x86 is little-endian, so this is the right order

    ret

simd_sort_6 ENDP

END

Bạn có thể biên dịch nó thành một đối tượng thực thi và liên kết nó vào dự án C của bạn. Để được hướng dẫn về cách thực hiện việc này trong Visual Studio, bạn có thể đọc bài viết này . Bạn có thể sử dụng nguyên mẫu C sau để gọi hàm từ mã C của mình:

void simd_sort_6(char *values);

Nó sẽ được can thiệp để so sánh của bạn với các đề xuất cấp độ lắp ráp khác. Các màn trình diễn được so sánh thực hiện không bao gồm chúng. Sử dụng SSE nghe có vẻ tốt.
kriss

Một lĩnh vực nghiên cứu khác trong tương lai sẽ là ứng dụng các hướng dẫn Intel AVX mới cho vấn đề này. Các vectơ 256 bit lớn hơn đủ lớn để phù hợp với 8 DWORD.
Joe Crivello

1
Thay vì pxor / pinsrd xmm4, mem, 0, chỉ cần sử dụng movd!
Peter Cordes

14

Mã kiểm tra là khá xấu; nó tràn ra mảng ban đầu (không phải mọi người ở đây có đọc cảnh báo trình biên dịch không?), printf đang in sai các phần tử, nó sử dụng .byte cho rdtsc không có lý do chính đáng, chỉ có một lần chạy (!), không có gì kiểm tra rằng kết quả cuối cùng thực sự chính xác (vì vậy rất dễ dàng để tối ưu hóa điều này thành một cái gì đó sai một cách tinh vi), các thử nghiệm đi kèm rất thô sơ (không có số âm?) và không có gì ngăn trình biên dịch loại bỏ toàn bộ chức năng thành mã chết.

Điều đó đang được nói, nó cũng khá dễ dàng để cải thiện giải pháp mạng bitonic; chỉ cần thay đổi công cụ min / max / SWAP thành

#define SWAP(x,y) { int tmp; asm("mov %0, %2 ; cmp %1, %0 ; cmovg %1, %0 ; cmovg %2, %1" : "=r" (d[x]), "=r" (d[y]), "=r" (tmp) : "0" (d[x]), "1" (d[y]) : "cc"); }

và nó phát ra nhanh hơn khoảng 65% đối với tôi (Debian gcc 4.4.5 với -O2, amd64, Core i7).


OK, mã kiểm tra là kém. Hãy cải thiện nó. Và vâng, bạn có thể sử dụng mã lắp ráp. Tại sao không đi tất cả các cách và mã hóa đầy đủ bằng cách sử dụng trình biên dịch x86? Nó có thể là một chút ít di động nhưng tại sao phải bận tâm?
kriss

Cảm ơn vì đã nhận thấy mảng tràn, tôi đã sửa nó. Những người khác có thể không nhận thấy điều đó vì nhấp vào liên kết để sao chép / dán mã, nơi không có tràn.
kriss

4
Bạn thậm chí không cần trình biên dịch chương trình; nếu bạn bỏ tất cả các mánh khóe thông minh, GCC sẽ nhận ra chuỗi và chèn các động tác có điều kiện cho bạn: #define min (a, b) ((a <b)? a: b) #define max (a, b) ( (a <b)? b: a) #define SWAP (x, y) {int a = min (d [x], d [y]); int b = max (d [x], d [y]); d [x] = a; d [y] = b; } Nó xuất hiện có thể chậm hơn một vài phần trăm so với biến thể asm nội tuyến, nhưng điều đó khó có thể nói do thiếu điểm chuẩn thích hợp.
Steinar H. Gunderson

3
Cuối cùng, nếu số của bạn là số float và bạn không phải lo lắng về NaN, v.v., GCC có thể chuyển đổi này thành hướng dẫn SSE minss / maxss, nhanh hơn ~ 25%. Tinh thần: Bỏ các thủ thuật bitfiddling thông minh và để trình biên dịch thực hiện công việc của nó. :-)
Steinar H. Gunderson

13

Trong khi tôi thực sự thích macro hoán đổi được cung cấp:

#define min(x, y) (y ^ ((x ^ y) & -(x < y)))
#define max(x, y) (x ^ ((x ^ y) & -(x < y)))
#define SWAP(x,y) { int tmp = min(d[x], d[y]); d[y] = max(d[x], d[y]); d[x] = tmp; }

Tôi thấy một sự cải tiến (mà một trình biên dịch tốt có thể thực hiện):

#define SWAP(x,y) { int tmp = ((x ^ y) & -(y < x)); y ^= tmp; x ^= tmp; }

Chúng tôi lưu ý về cách thức hoạt động của min và max và kéo biểu thức con chung một cách rõ ràng. Điều này giúp loại bỏ hoàn toàn các macro tối thiểu và tối đa.


Điều đó khiến họ bị ngược, chú ý rằng d [y] đạt mức tối đa, đó là x ^ (biểu hiện phụ chung).
Kevin Stock

Tôi đa ghi chu điêu tương đương; Tôi nghĩ rằng việc triển khai của bạn là chính xác mà bạn muốn d[x]thay vì x(tương tự cho y) và d[y] < d[x]cho sự bất bình đẳng ở đây (vâng, khác với mã tối thiểu / tối đa).
Tyler

Tôi đã thử với trao đổi của bạn, nhưng tối ưu hóa cục bộ có tác động tiêu cực ở cấp độ lớn hơn (tôi đoán nó giới thiệu phụ thuộc). Và kết quả là chậm hơn so với các trao đổi khác. Nhưng như bạn có thể thấy với giải pháp mới được đề xuất, thực sự có nhiều hiệu suất để đạt được tối ưu hóa trao đổi.
kriss

12

Không bao giờ tối ưu hóa tối thiểu / tối đa mà không có điểm chuẩn và nhìn vào trình biên dịch thực tế lắp ráp. Nếu tôi để GCC tối ưu hóa tối thiểu với các hướng dẫn di chuyển có điều kiện, tôi sẽ tăng tốc 33%:

#define SWAP(x,y) { int dx = d[x], dy = d[y], tmp; tmp = d[x] = dx < dy ? dx : dy; d[y] ^= dx ^ tmp; }

(280 so với 420 chu kỳ trong mã kiểm tra). Làm tối đa với ?: Ít nhiều giống nhau, gần như bị mất trong tiếng ồn, nhưng ở trên nhanh hơn một chút. SWAP này nhanh hơn với cả GCC và Clang.

Trình biên dịch cũng đang thực hiện một công việc đặc biệt khi phân bổ đăng ký và phân tích bí danh, chuyển hiệu quả d [x] thành các biến cục bộ trả trước và chỉ sao chép trở lại bộ nhớ vào cuối. Trên thực tế, chúng làm như vậy thậm chí còn tốt hơn so với việc bạn làm việc hoàn toàn với các biến cục bộ (nhưd0 = d[0], d1 = d[1], d2 = d[2], d3 = d[3], d4 = d[4], d5 = d[5] ). Tôi đang viết điều này bởi vì bạn đang giả định tối ưu hóa mạnh mẽ và đang cố gắng vượt qua trình biên dịch trên min / max. :)

Nhân tiện, tôi đã thử Clang và GCC. Họ thực hiện cùng một tối ưu hóa, nhưng do sự khác biệt về lịch trình, cả hai có một số thay đổi trong kết quả, không thể nói thực sự nhanh hơn hay chậm hơn. GCC nhanh hơn trên các mạng sắp xếp, Clang trên các loại bậc hai.

Chỉ để hoàn thiện, sắp xếp bong bóng không được kiểm soát và sắp xếp chèn cũng có thể. Đây là loại bong bóng:

SWAP(0,1); SWAP(1,2); SWAP(2,3); SWAP(3,4); SWAP(4,5);
SWAP(0,1); SWAP(1,2); SWAP(2,3); SWAP(3,4);
SWAP(0,1); SWAP(1,2); SWAP(2,3);
SWAP(0,1); SWAP(1,2);
SWAP(0,1);

và đây là loại chèn:

//#define ITER(x) { if (t < d[x]) { d[x+1] = d[x]; d[x] = t; } }
//Faster on x86, probably slower on ARM or similar:
#define ITER(x) { d[x+1] ^= t < d[x] ? d[x] ^ d[x+1] : 0; d[x] = t < d[x] ? t : d[x]; }
static inline void sort6_insertion_sort_unrolled_v2(int * d){
    int t;
    t = d[1]; ITER(0);
    t = d[2]; ITER(1); ITER(0);
    t = d[3]; ITER(2); ITER(1); ITER(0);
    t = d[4]; ITER(3); ITER(2); ITER(1); ITER(0);
    t = d[5]; ITER(4); ITER(3); ITER(2); ITER(1); ITER(0);

Kiểu chèn này nhanh hơn Daniel Stutzbach và đặc biệt tốt trên GPU hoặc máy tính có tính dự đoán vì ITER có thể được thực hiện chỉ với 3 hướng dẫn (so với 4 cho SWAP). Ví dụ, đây là t = d[2]; ITER(1); ITER(0);dòng trong lắp ráp ARM:

    MOV    r6, r2
    CMP    r6, r1
    MOVLT  r2, r1
    MOVLT  r1, r6
    CMP    r6, r0
    MOVLT  r1, r0
    MOVLT  r0, r6

Đối với sáu yếu tố, sắp xếp chèn có thể cạnh tranh với mạng sắp xếp (12 lần hoán đổi so với 15 lần lặp cân bằng 4 hướng dẫn / hoán đổi so với 3 hướng dẫn / lần lặp); Tất nhiên là loại bong bóng chậm hơn. Nhưng điều đó sẽ không đúng khi kích thước tăng lên, vì sắp xếp chèn là O (n ^ 2) trong khi các mạng sắp xếp là O (n log n).


1
Liên quan nhiều hơn hoặc ít hơn: Tôi đã gửi báo cáo cho GCC để nó có thể thực hiện tối ưu hóa trực tiếp trong trình biên dịch. Không chắc chắn rằng nó sẽ được thực hiện, nhưng ít nhất bạn có thể làm theo cách nó phát triển.
Morwenn

11

Tôi đã chuyển bộ kiểm thử sang máy kiến ​​trúc PPC mà tôi không thể xác định được (không phải chạm vào mã, chỉ cần tăng số lần lặp lại của bài kiểm tra, sử dụng 8 trường hợp kiểm tra để tránh gây ô nhiễm kết quả bằng mod và thay thế lệnh ndtsc cụ thể x86):

Gọi trực tiếp đến chức năng thư viện qsort : 101

Thực hiện ngây thơ (sắp xếp chèn) : 299

Sắp xếp chèn (Daniel Stutzbach) : 108

Sắp xếp chèn Không được kiểm soát : 51

Mạng sắp xếp (Daniel Stutzbach) : 26

Mạng sắp xếp (Paul R) : 85

Sắp xếp mạng 12 với Hoán đổi nhanh : 117

Sắp xếp mạng 12 Hoán đổi được sắp xếp lại : 116

Thứ tự xếp hạng : 56


1
Thật sự thú vị. Có vẻ như hoán đổi không phân nhánh là một ý tưởng tồi trên PPC. Nó cũng có thể là một hiệu ứng liên quan đến trình biên dịch. Cái nào đã được sử dụng?
kriss

Đây là một nhánh của trình biên dịch gcc - logic tối thiểu, tối đa có lẽ không phải là không có nhánh - tôi sẽ kiểm tra việc tháo gỡ và cho bạn biết, nhưng trừ khi trình biên dịch đủ thông minh bao gồm cả thứ gì đó như x <y mà không trở thành nhánh - trên x86 / x64 lệnh CMOV có thể tránh điều này, nhưng không có lệnh nào cho các giá trị điểm cố định trên PPC, chỉ có các float. Tôi có thể tìm hiểu điều này vào ngày mai và cho bạn biết - Tôi nhớ rằng có một min / max không phân nhánh đơn giản hơn nhiều trong nguồn Winamp AVS, nhưng iirc nó chỉ dành cho phao - nhưng có thể là một khởi đầu tốt cho cách tiếp cận thực sự không phân nhánh.
jheriko

4
Đây là một min / max không phân nhánh cho PPC với các đầu vào không dấu : subfc r5,r4,r3; subfe r6,r6,r6; andc r6,r5,r6; add r4,r6,r4; subf r3,r6,r3. r3 / r4 là đầu vào, r5 / r6 là các thanh ghi cào, trên đầu ra r3 lấy min và r4 lấy max. Nó nên được lên lịch trình bằng tay. Tôi đã tìm thấy nó với trình siêu phân tích GNU, bắt đầu từ các chuỗi 4 lệnh tối thiểu và tối đa và tìm kiếm thủ công hai chuỗi có thể được kết hợp. Đối với các đầu vào đã ký, tất nhiên bạn có thể thêm 0x80000000 vào tất cả các phần tử ở đầu và trừ lại ở cuối, và sau đó hoạt động như thể chúng không được ký.
Paolo Bonzini

7

Trao đổi XOR có thể hữu ích trong các chức năng hoán đổi của bạn.

void xorSwap (int *x, int *y) {
     if (*x != *y) {
         *x ^= *y;
         *y ^= *x;
         *x ^= *y;
     }
 }

Nếu if có thể gây ra quá nhiều phân kỳ trong mã của bạn, nhưng nếu bạn có đảm bảo rằng tất cả các số nguyên của bạn là duy nhất thì điều này có thể hữu ích.


1
trao đổi xor cũng hoạt động với các giá trị bằng nhau ... x ^ = y đặt x thành 0, y ^ = x để y là y (== x), x ^ = y đặt x thành y
jheriko

11
Khi nó không hoạt động là khi xytrỏ đến cùng một vị trí.
hobbs

Dù sao, khi được sử dụng với các mạng sắp xếp, chúng tôi không bao giờ gọi với cả x và y trỏ đến cùng một vị trí. Vẫn còn phải tìm một cách để tránh thử nghiệm lớn hơn để có được hiệu quả tương tự như hoán đổi không phân nhánh. Tôi có một ý tưởng để đạt được điều đó.
kriss

5

Mong muốn được thử điều này và học hỏi từ những ví dụ này, nhưng trước tiên, một số thời gian từ PPC Powerbook G4 1,5 GHz của tôi với RAM DDR 1 GB. (Tôi đã mượn một bộ đếm thời gian giống như ndtsc cho PPC từ http://www.mcs.anl.gov/~kazutomo/rdtsc.html để định thời gian.) Tôi đã chạy chương trình một vài lần và kết quả tuyệt đối thay đổi nhưng luôn nhất quán thử nghiệm nhanh nhất là "Sắp xếp chèn (Daniel Stutzbach)", với "Sắp xếp chèn không kiểm soát" một giây.

Đây là tập hợp thời gian cuối cùng:

**Direct call to qsort library function** : 164
**Naive implementation (insertion sort)** : 138
**Insertion Sort (Daniel Stutzbach)**     : 85
**Insertion Sort Unrolled**               : 97
**Sorting Networks (Daniel Stutzbach)**   : 457
**Sorting Networks (Paul R)**             : 179
**Sorting Networks 12 with Fast Swap**    : 238
**Sorting Networks 12 reordered Swap**    : 236
**Rank Order**                            : 116

4

Đây là đóng góp của tôi cho chủ đề này: shellsort 1, 4 gap được tối ưu hóa cho một vectơ int 6 thành viên (valp) chứa các giá trị duy nhất.

void shellsort (int *valp)
{      
  int c,a,*cp,*ip=valp,*ep=valp+5;

  c=*valp;    a=*(valp+4);if (c>a) {*valp=    a;*(valp+4)=c;}
  c=*(valp+1);a=*(valp+5);if (c>a) {*(valp+1)=a;*(valp+5)=c;}

  cp=ip;    
  do
  {
    c=*cp;
    a=*(cp+1);
    do
    {
      if (c<a) break;

      *cp=a;
      *(cp+1)=c;
      cp-=1;
      c=*cp;
    } while (cp>=valp);
    ip+=1;
    cp=ip;
  } while (ip<ep);
}

Trên máy tính xách tay HP dv7-3010so của tôi với lõi kép Athlon M300 @ 2 Ghz (bộ nhớ DDR2), nó thực thi trong 165 chu kỳ xung nhịp. Đây là mức trung bình được tính từ thời gian mỗi chuỗi duy nhất (6! / 720). Được biên dịch thành Win32 bằng OpenWatcom 1.8. Vòng lặp về cơ bản là một kiểu sắp xếp chèn và dài 16 lệnh / 37 byte.

Tôi không có môi trường 64 bit để biên dịch.


đẹp. Tôi sẽ thêm nó vào testsuite dài hơn
kriss

3

Nếu sắp xếp chèn là cạnh tranh hợp lý ở đây, tôi khuyên bạn nên thử shellsort. Tôi e rằng 6 yếu tố có lẽ chỉ là quá ít để nó nằm trong số tốt nhất, nhưng nó có thể đáng để thử.

Mã ví dụ, chưa được kiểm tra, chưa được mã hóa, v.v. Bạn muốn điều chỉnh chuỗi inc = 4 và inc - = 3 để tìm tối ưu (ví dụ: thử inc = 2, inc - = 1).

static __inline__ int sort6(int * d) {
    char j, i;
    int tmp;
    for (inc = 4; inc > 0; inc -= 3) {
        for (i = inc; i < 5; i++) {
            tmp = a[i];
            j = i;
            while (j >= inc && a[j - inc] > tmp) {
                a[j] = a[j - inc];
                j -= inc;
            }
            a[j] = tmp;
        }
    }
}

Tôi không nghĩ rằng điều này sẽ thắng, nhưng nếu ai đó đăng câu hỏi về cách sắp xếp 10 yếu tố, ai biết ...

Theo Wikipedia, điều này thậm chí có thể được kết hợp với các mạng sắp xếp: Pratt, V (1979). Shellsort và các mạng phân loại (luận văn xuất sắc trong ngành khoa học máy tính). Vòng hoa. Sđt 0-824-04406-1


vui lòng đề xuất một số triển khai :-)
kriss

Đề xuất thêm. Thưởng thức các lỗi.
gcp

3

Tôi biết tôi siêu muộn, nhưng tôi thích thử nghiệm một số giải pháp khác nhau. Đầu tiên, tôi dọn sạch miếng dán đó, biên dịch nó và đưa nó vào một kho lưu trữ. Tôi giữ một số giải pháp không mong muốn là ngõ cụt để những người khác không thử. Trong số này là giải pháp đầu tiên của tôi, đã cố gắng đảm bảo rằng x1> x2 đã được tính toán một lần. Sau khi tối ưu hóa, nó không nhanh hơn các phiên bản đơn giản khác.

Tôi đã thêm một phiên bản lặp của sắp xếp thứ tự xếp hạng, vì ứng dụng của nghiên cứu này của tôi là để sắp xếp 2-8 mục, vì vậy có một số lượng đối số khác nhau, nên cần một vòng lặp. Đây cũng là lý do tại sao tôi bỏ qua các giải pháp mạng sắp xếp.

Mã kiểm tra đã không kiểm tra các bản sao được xử lý chính xác, vì vậy trong khi các giải pháp hiện có đều đúng, tôi đã thêm một trường hợp đặc biệt vào mã kiểm tra để đảm bảo rằng các bản sao được xử lý chính xác.

Sau đó, tôi đã viết một loại chèn hoàn toàn trong các thanh ghi AVX. Trên máy của tôi, nó nhanh hơn 25% so với các loại chèn khác, nhưng chậm hơn 100% so với thứ tự xếp hạng. Tôi đã làm điều này hoàn toàn để thử nghiệm và không có hy vọng điều này sẽ tốt hơn do sự phân nhánh trong sắp xếp chèn.

static inline void sort6_insertion_sort_avx(int* d) {
    __m256i src = _mm256_setr_epi32(d[0], d[1], d[2], d[3], d[4], d[5], 0, 0);
    __m256i index = _mm256_setr_epi32(0, 1, 2, 3, 4, 5, 6, 7);
    __m256i shlpermute = _mm256_setr_epi32(7, 0, 1, 2, 3, 4, 5, 6);
    __m256i sorted = _mm256_setr_epi32(d[0], INT_MAX, INT_MAX, INT_MAX,
            INT_MAX, INT_MAX, INT_MAX, INT_MAX);
    __m256i val, gt, permute;
    unsigned j;
     // 8 / 32 = 2^-2
#define ITER(I) \
        val = _mm256_permutevar8x32_epi32(src, _mm256_set1_epi32(I));\
        gt =  _mm256_cmpgt_epi32(sorted, val);\
        permute =  _mm256_blendv_epi8(index, shlpermute, gt);\
        j = ffs( _mm256_movemask_epi8(gt)) >> 2;\
        sorted = _mm256_blendv_epi8(_mm256_permutevar8x32_epi32(sorted, permute),\
                val, _mm256_cmpeq_epi32(index, _mm256_set1_epi32(j)))
    ITER(1);
    ITER(2);
    ITER(3);
    ITER(4);
    ITER(5);
    int x[8];
    _mm256_storeu_si256((__m256i*)x, sorted);
    d[0] = x[0]; d[1] = x[1]; d[2] = x[2]; d[3] = x[3]; d[4] = x[4]; d[5] = x[5];
#undef ITER
}

Sau đó, tôi đã viết một thứ tự sắp xếp thứ hạng bằng AVX. Điều này phù hợp với tốc độ của các giải pháp xếp hạng khác, nhưng không nhanh hơn. Vấn đề ở đây là tôi chỉ có thể tính toán các chỉ số bằng AVX, và sau đó tôi phải tạo một bảng chỉ số. Điều này là do tính toán dựa trên đích chứ không phải dựa trên nguồn. Xem Chuyển đổi từ các chỉ số dựa trên nguồn sang các chỉ mục dựa trên đích

static inline void sort6_rank_order_avx(int* d) {
    __m256i ror = _mm256_setr_epi32(5, 0, 1, 2, 3, 4, 6, 7);
    __m256i one = _mm256_set1_epi32(1);
    __m256i src = _mm256_setr_epi32(d[0], d[1], d[2], d[3], d[4], d[5], INT_MAX, INT_MAX);
    __m256i rot = src;
    __m256i index = _mm256_setzero_si256();
    __m256i gt, permute;
    __m256i shl = _mm256_setr_epi32(1, 2, 3, 4, 5, 6, 6, 6);
    __m256i dstIx = _mm256_setr_epi32(0,1,2,3,4,5,6,7);
    __m256i srcIx = dstIx;
    __m256i eq = one;
    __m256i rotIx = _mm256_setzero_si256();
#define INC(I)\
    rot = _mm256_permutevar8x32_epi32(rot, ror);\
    gt = _mm256_cmpgt_epi32(src, rot);\
    index = _mm256_add_epi32(index, _mm256_and_si256(gt, one));\
    index = _mm256_add_epi32(index, _mm256_and_si256(eq,\
                _mm256_cmpeq_epi32(src, rot)));\
    eq = _mm256_insert_epi32(eq, 0, I)
    INC(0);
    INC(1);
    INC(2);
    INC(3);
    INC(4);
    int e[6];
    e[0] = d[0]; e[1] = d[1]; e[2] = d[2]; e[3] = d[3]; e[4] = d[4]; e[5] = d[5];
    int i[8];
    _mm256_storeu_si256((__m256i*)i, index);
    d[i[0]] = e[0]; d[i[1]] = e[1]; d[i[2]] = e[2]; d[i[3]] = e[3]; d[i[4]] = e[4]; d[i[5]] = e[5];
}

Repo có thể được tìm thấy ở đây: https://github.com/eyepatchParrot/sort6/


1
Bạn có thể sử dụng vmovmskpstrên các vectơ số nguyên (với một biểu tượng để giữ cho nội tại hạnh phúc), tránh việc phải dịch chuyển sang phải ffskết quả bitcan ( ).
Peter Cordes

1
Bạn có thể thêm 1 điều kiện dựa trên cmpgtkết quả bằng cách trừ nó, thay vì che giấu nó set1(1). ví dụ như index = _mm256_sub_epi32(index, gt)khôngindex -= -1 or 0;
Peter Cordes

1
eq = _mm256_insert_epi32(eq, 0, I)không phải là một cách hiệu quả để bằng không một phần tử nếu nó biên dịch thành văn bản (đặc biệt là đối với các phần tử nằm ngoài mức 4 thấp, vì vpinsrdchỉ có sẵn với đích XMM; chỉ số cao hơn 3 phải được mô phỏng). Thay vào đó, _mm256_blend_epi32( vpblendd) với một vectơ bằng không. vpblenddlà một lệnh đơn uop chạy trên bất kỳ cổng nào, so với shuffle cần cổng 5 trên CPU Intel. ( agner.org/optizes ).
Peter Cordes

1
Ngoài ra, bạn có thể xem xét việc tạo các rotvectơ với các xáo trộn khác nhau từ cùng một nguồn hoặc ít nhất là chạy song song 2 chuỗi dep mà bạn sử dụng xen kẽ, thay vì một chuỗi dep duy nhất thông qua xáo trộn làn đường (độ trễ 3 chu kỳ). Điều đó sẽ tăng ILP trong một loại duy nhất. Chuỗi 2 dep giới hạn số lượng hằng số vectơ ở một số hợp lý, chỉ 2: 1 cho một lần xoay và một cho 2 bước xoay kết hợp.
Peter Cordes

2

Câu hỏi này đang trở nên khá cũ, nhưng tôi thực sự phải giải quyết vấn đề tương tự ngày nay: các câu hỏi nhanh để sắp xếp các mảng nhỏ. Tôi nghĩ rằng nó sẽ là một ý tưởng tốt để chia sẻ kiến ​​thức của tôi. Trong lần đầu tiên tôi bắt đầu bằng cách sử dụng các mạng sắp xếp, cuối cùng tôi đã tìm được các thuật toán khác trong đó tổng số phép so sánh được thực hiện để sắp xếp mọi hoán vị của 6 giá trị nhỏ hơn so với các mạng sắp xếp và nhỏ hơn so với sắp xếp chèn. Tôi đã không đếm số lần hoán đổi; Tôi hy vọng nó sẽ tương đương (đôi khi có thể cao hơn một chút).

Thuật toán sort6sử dụng thuật toán sort4sử dụng thuật toán sort3. Dưới đây là cách triển khai trong một số dạng C ++ nhẹ (bản gốc nặng mẫu để nó có thể hoạt động với bất kỳ trình lặp truy cập ngẫu nhiên và bất kỳ chức năng so sánh phù hợp nào).

Sắp xếp 3 giá trị

Các thuật toán sau đây là một loại chèn không được kiểm soát. Khi hai lần hoán đổi (6 bài tập) phải được thực hiện, nó sử dụng 4 bài tập thay thế:

void sort3(int* array)
{
    if (array[1] < array[0]) {
        if (array[2] < array[0]) {
            if (array[2] < array[1]) {
                std::swap(array[0], array[2]);
            } else {
                int tmp = array[0];
                array[0] = array[1];
                array[1] = array[2];
                array[2] = tmp;
            }
        } else {
            std::swap(array[0], array[1]);
        }
    } else {
        if (array[2] < array[1]) {
            if (array[2] < array[0]) {
                int tmp = array[2];
                array[2] = array[1];
                array[1] = array[0];
                array[0] = tmp;
            } else {
                std::swap(array[1], array[2]);
            }
        }
    }
}

Có vẻ hơi phức tạp vì sắp xếp có ít nhất một nhánh cho mỗi hoán vị có thể có của mảng, sử dụng 2 ~ 3 phép so sánh và nhiều nhất là 4 phép gán để sắp xếp ba giá trị.

Sắp xếp 4 giá trị

Cuộc gọi này sort3sau đó thực hiện sắp xếp chèn không được kiểm soát với phần tử cuối cùng của mảng:

void sort4(int* array)
{
    // Sort the first 3 elements
    sort3(array);

    // Insert the 4th element with insertion sort 
    if (array[3] < array[2]) {
        std::swap(array[2], array[3]);
        if (array[2] < array[1]) {
            std::swap(array[1], array[2]);
            if (array[1] < array[0]) {
                std::swap(array[0], array[1]);
            }
        }
    }
}

Thuật toán này thực hiện 3 đến 6 so sánh và nhiều nhất là 5 lần hoán đổi. Thật dễ dàng để hủy đăng ký một loại chèn, nhưng chúng ta sẽ sử dụng một thuật toán khác cho loại cuối cùng ...

Sắp xếp 6 giá trị

Cái này sử dụng một phiên bản không được kiểm soát của cái mà tôi gọi là sắp xếp chèn kép . Cái tên không hay lắm, nhưng nó khá mô tả, đây là cách nó hoạt động:

  • Sắp xếp mọi thứ trừ phần tử đầu tiên và cuối cùng của mảng.
  • Hoán đổi phần tử đầu tiên và các phần tử của mảng nếu phần đầu tiên lớn hơn phần tử cuối cùng.
  • Chèn phần tử đầu tiên vào chuỗi được sắp xếp từ phía trước rồi phần tử cuối cùng từ phía sau.

Sau khi hoán đổi, phần tử đầu tiên luôn nhỏ hơn phần tử cuối cùng, điều đó có nghĩa là, khi chèn chúng vào chuỗi đã được sắp xếp, sẽ không có nhiều hơn N so sánh để chèn hai phần tử trong trường hợp xấu nhất: ví dụ: phần tử đầu tiên đã được chèn vào vị trí thứ 3, sau đó phần tử cuối cùng không thể được chèn thấp hơn vị trí thứ 4.

void sort6(int* array)
{
    // Sort everything but first and last elements
    sort4(array+1);

    // Switch first and last elements if needed
    if (array[5] < array[0]) {
        std::swap(array[0], array[5]);
    }

    // Insert first element from the front
    if (array[1] < array[0]) {
        std::swap(array[0], array[1]);
        if (array[2] < array[1]) {
            std::swap(array[1], array[2]);
            if (array[3] < array[2]) {
                std::swap(array[2], array[3]);
                if (array[4] < array[3]) {
                    std::swap(array[3], array[4]);
                }
            }
        }
    }

    // Insert last element from the back
    if (array[5] < array[4]) {
        std::swap(array[4], array[5]);
        if (array[4] < array[3]) {
            std::swap(array[3], array[4]);
            if (array[3] < array[2]) {
                std::swap(array[2], array[3]);
                if (array[2] < array[1]) {
                    std::swap(array[1], array[2]);
                }
            }
        }
    }
}

Các thử nghiệm của tôi về mọi hoán vị của 6 giá trị từng cho thấy thuật toán này luôn thực hiện từ 6 đến 13 so sánh. Tôi đã không tính toán số lần hoán đổi được thực hiện, nhưng tôi không hy vọng nó sẽ cao hơn 11 trong trường hợp xấu nhất.

Tôi hy vọng rằng điều này có ích, ngay cả khi câu hỏi này có thể không đại diện cho một vấn đề thực tế nữa :)

EDIT: sau khi đưa nó vào điểm chuẩn được cung cấp, rõ ràng là chậm hơn so với hầu hết các lựa chọn thay thế thú vị. Nó có xu hướng hoạt động tốt hơn một chút so với loại chèn không được kiểm soát, nhưng đó là khá nhiều. Về cơ bản, nó không phải là loại tốt nhất cho số nguyên nhưng có thể thú vị cho các loại có hoạt động so sánh đắt tiền.


Đây là tốt đẹp. Khi vấn đề được giải quyết đã nhiều thập kỷ, có lẽ là một chương trình C cũ, câu hỏi bây giờ đã gần 5 năm có vẻ không liên quan lắm.
kriss

Bạn nên có một cái nhìn về cách các câu trả lời khác được tính thời gian. Vấn đề là với các tập dữ liệu nhỏ như vậy, việc so sánh hoặc thậm chí so sánh và hoán đổi không thực sự nói rằng thuật toán nhanh như thế nào (về cơ bản sắp xếp 6 int luôn là O (1) vì O (6 * 6) là O (1)). Cách nhanh nhất hiện tại của các giải pháp được đề xuất trước đó là ngay lập tức tìm vị trí của từng giá trị bằng cách so sánh lớn (bởi RexKerr).
kriss

@kriss Bây giờ có nhanh nhất không? Từ việc tôi đọc kết quả, cách tiếp cận mạng sắp xếp là nhanh nhất, xấu của tôi. Cũng đúng là giải pháp của tôi xuất phát từ thư viện chung của tôi và tôi không phải lúc nào cũng so sánh các số nguyên, cũng không phải lúc nào cũng sử dụng operator<để so sánh. Bên cạnh số lượng mục tiêu so sánh và hoán đổi, tôi cũng định thời gian đúng cho các thuật toán của mình; giải pháp này là giải pháp chung nhanh nhất, nhưng tôi thực sự đã bỏ lỡ giải pháp của @ RexKerr. Hãy thử đi :)
Morwenn 7/10/2015

Giải pháp của RexKerr (Thứ hạng đơn hàng) trở thành nhanh nhất trên kiến ​​trúc X86 kể từ trình biên dịch gcc 4.2.3 (và kể từ gcc 4.9 trở nên nhanh hơn gần hai lần so với thứ hai tốt nhất). Nhưng nó phụ thuộc rất nhiều vào tối ưu hóa trình biên dịch và có thể không đúng với các kiến ​​trúc khác.
kriss

@kriss Thật thú vị khi biết. Và tôi thực sự có thể khác biệt nhiều hơn một lần nữa với -O3. Tôi đoán rằng tôi sẽ áp dụng một chiến lược khác cho thư viện sắp xếp của mình sau đó: cung cấp ba loại thuật toán để có số lượng so sánh thấp, số lần hoán đổi thấp hoặc có khả năng thực hiện tốt nhất. Ít nhất, những gì xảy ra sẽ minh bạch cho người đọc. Cảm ơn những hiểu biết của bạn :)
Morwenn 7/10/2015

1

Tôi tin rằng có hai phần cho câu hỏi của bạn.

  • Đầu tiên là xác định thuật toán tối ưu. Điều này được thực hiện - ít nhất là trong trường hợp này - bằng cách lặp qua mọi thứ tự có thể (không có nhiều) cho phép bạn tính chính xác độ lệch tối thiểu, tối đa, trung bình và chuẩn của các phép so sánh và hoán đổi. Có một Á hậu hoặc hai tiện dụng là tốt.
  • Thứ hai là tối ưu hóa thuật toán. Rất nhiều có thể được thực hiện để chuyển đổi các ví dụ mã sách giáo khoa thành các thuật toán thực tế có nghĩa và nạc. Nếu bạn nhận ra rằng một thuật toán không thể được tối ưu hóa đến mức cần thiết, hãy thử chạy lên.

Tôi sẽ không lo lắng quá nhiều về việc làm trống đường ống (giả sử x86 hiện tại): dự đoán nhánh đã đi một chặng đường dài. Điều tôi sẽ lo lắng là đảm bảo rằng mã và dữ liệu phù hợp với một dòng bộ đệm mỗi dòng (có thể là hai cho mã). Một khi có độ trễ tìm nạp thấp mới sẽ bù cho bất kỳ gian hàng nào. Điều đó cũng có nghĩa là vòng lặp bên trong của bạn sẽ có thể là mười hướng dẫn hoặc đúng như vậy (đúng là có hai vòng lặp bên trong khác nhau trong thuật toán sắp xếp của tôi, chúng lần lượt là 10 lệnh / 22 byte và dài 9/22). Giả sử mã không chứa bất kỳ div nào, bạn có thể chắc chắn rằng nó sẽ nhanh chóng.


Tôi không chắc làm thế nào để hiểu câu trả lời của bạn. Đầu tiên tôi không hiểu bạn đang đề xuất thuật toán gì? Và làm thế nào nó có thể là tối ưu nếu bạn phải lặp qua 720 thứ tự có thể (câu trả lời hiện có mất ít hơn 720 chu kỳ). Nếu bạn có đầu vào ngẫu nhiên, tôi không thể tưởng tượng được (thậm chí ở mức độ lý thuyết) làm thế nào dự đoán nhánh có thể hoạt động tốt hơn 50-50 trừ khi nó không quan tâm đến tất cả dữ liệu đầu vào. Ngoài ra, hầu hết các giải pháp tốt đã được đề xuất đều có khả năng hoạt động với cả dữ liệu và mã hoàn toàn trong bộ đệm. Nhưng có lẽ tôi hoàn toàn hiểu nhầm câu trả lời của bạn. Tâm hiển thị một số mã?
kriss

Ý tôi là chỉ có 720 (6!) Kết hợp 6 số nguyên khác nhau và bằng cách chạy tất cả chúng thông qua các thuật toán ứng cử viên, bạn có thể xác định được rất nhiều điều như tôi đã đề cập - đó là phần lý thuyết. Phần thực tế là tinh chỉnh thuật toán đó để chạy trong càng ít chu kỳ xung nhịp càng tốt. Điểm khởi đầu của tôi để sắp xếp 6 số nguyên là shellsort 1, 4 gap. Khoảng cách 4 mở đường cho dự đoán nhánh tốt trong khoảng cách 1.
Vỏ bọc của Olof

Các shellsort 1, 4 cho 6! kết hợp độc đáo (bắt đầu bằng 012345 và kết thúc bằng 543210) sẽ có trường hợp tốt nhất là 7 so sánh và 0 trao đổi và tồi tệ nhất là 14 so sánh và 10 trao đổi. Trường hợp trung bình là khoảng 11,14 so sánh và 6 trao đổi.
Olof Forshell

1
Tôi không nhận được "phân phối ngẫu nhiên thường xuyên" - những gì tôi đang làm là kiểm tra mọi kết hợp có thể và xác định số liệu thống kê tối thiểu / trung bình / tối đa. Shellsort là một loạt các loại tăng giảm dần sao cho mức tăng cuối cùng - 1 - thực hiện công việc ít hơn nhiều so với việc nó được thực hiện một mình như trong một loại chèn thuần túy. Để tính đồng hồ, thuật toán của tôi trung bình yêu cầu 406 chu kỳ đồng hồ và điều này bao gồm thu thập số liệu thống kê và thực hiện hai cuộc gọi đến thói quen sắp xếp thực tế - một cuộc gọi cho mỗi khoảng cách. Đây là trên điện thoại di động Athlon M300, trình biên dịch OpenWatcom.
Olof Forshell

1
"phân phối ngẫu nhiên thường xuyên" có nghĩa là mọi kết hợp dữ liệu thực tế được sắp xếp có thể không có xác suất như nhau. Nếu mọi kết hợp không có xác suất bằng nhau, số liệu thống kê của bạn sẽ bị phá vỡ vì trung bình cần phải tính đến số lần phân phối nhất định có khả năng xảy ra. Đối với số lượng đồng hồ, nếu bạn thử bất kỳ triển khai nào khác của loại này (các liên kết được cung cấp ở trên) và chạy nó trên hệ thống kiểm tra của bạn, chúng tôi sẽ có cơ sở để so sánh và xem hiệu suất mà bạn đã chọn thực hiện tốt như thế nào.
kriss

1

Tôi biết đây là một câu hỏi cũ.

Nhưng tôi chỉ viết một loại giải pháp khác mà tôi muốn chia sẻ.
Không sử dụng gì ngoài việc lồng tối thiểu MIN MAX,

Nó không nhanh vì nó sử dụng 114 mỗi cái,
có thể giảm xuống còn 75 khá đơn giản như vậy -> pastebin

Nhưng sau đó nó không hoàn toàn là tối đa tối đa nữa.

Những gì có thể hoạt động là làm tối thiểu / tối đa trên nhiều số nguyên cùng một lúc với AVX

Tham khảo PMINSW

#include <stdio.h>

static __inline__ int MIN(int a, int b){
int result =a;
__asm__ ("pminsw %1, %0" : "+x" (result) : "x" (b));
return result;
}
static __inline__ int MAX(int a, int b){
int result = a;
__asm__ ("pmaxsw %1, %0" : "+x" (result) : "x" (b));
return result;
}
static __inline__ unsigned long long rdtsc(void){
  unsigned long long int x;
__asm__ volatile (".byte 0x0f, 0x31" :
  "=A" (x));
  return x;
}

#define MIN3(a, b, c) (MIN(MIN(a,b),c))
#define MIN4(a, b, c, d) (MIN(MIN(a,b),MIN(c,d)))

static __inline__ void sort6(int * in) {
  const int A=in[0], B=in[1], C=in[2], D=in[3], E=in[4], F=in[5];

  in[0] = MIN( MIN4(A,B,C,D),MIN(E,F) );

  const int
  AB = MAX(A, B),
  AC = MAX(A, C),
  AD = MAX(A, D),
  AE = MAX(A, E),
  AF = MAX(A, F),
  BC = MAX(B, C),
  BD = MAX(B, D),
  BE = MAX(B, E),
  BF = MAX(B, F),
  CD = MAX(C, D),
  CE = MAX(C, E),
  CF = MAX(C, F),
  DE = MAX(D, E),
  DF = MAX(D, F),
  EF = MAX(E, F);

  in[1] = MIN4 (
  MIN4( AB, AC, AD, AE ),
  MIN4( AF, BC, BD, BE ),
  MIN4( BF, CD, CE, CF ),
  MIN3( DE, DF, EF)
  );

  const int
  ABC = MAX(AB,C),
  ABD = MAX(AB,D),
  ABE = MAX(AB,E),
  ABF = MAX(AB,F),
  ACD = MAX(AC,D),
  ACE = MAX(AC,E),
  ACF = MAX(AC,F),
  ADE = MAX(AD,E),
  ADF = MAX(AD,F),
  AEF = MAX(AE,F),
  BCD = MAX(BC,D),
  BCE = MAX(BC,E),
  BCF = MAX(BC,F),
  BDE = MAX(BD,E),
  BDF = MAX(BD,F),
  BEF = MAX(BE,F),
  CDE = MAX(CD,E),
  CDF = MAX(CD,F),
  CEF = MAX(CE,F),
  DEF = MAX(DE,F);

  in[2] = MIN( MIN4 (
  MIN4( ABC, ABD, ABE, ABF ),
  MIN4( ACD, ACE, ACF, ADE ),
  MIN4( ADF, AEF, BCD, BCE ),
  MIN4( BCF, BDE, BDF, BEF )),
  MIN4( CDE, CDF, CEF, DEF )
  );


  const int
  ABCD = MAX(ABC,D),
  ABCE = MAX(ABC,E),
  ABCF = MAX(ABC,F),
  ABDE = MAX(ABD,E),
  ABDF = MAX(ABD,F),
  ABEF = MAX(ABE,F),
  ACDE = MAX(ACD,E),
  ACDF = MAX(ACD,F),
  ACEF = MAX(ACE,F),
  ADEF = MAX(ADE,F),
  BCDE = MAX(BCD,E),
  BCDF = MAX(BCD,F),
  BCEF = MAX(BCE,F),
  BDEF = MAX(BDE,F),
  CDEF = MAX(CDE,F);

  in[3] = MIN4 (
  MIN4( ABCD, ABCE, ABCF, ABDE ),
  MIN4( ABDF, ABEF, ACDE, ACDF ),
  MIN4( ACEF, ADEF, BCDE, BCDF ),
  MIN3( BCEF, BDEF, CDEF )
  );

  const int
  ABCDE= MAX(ABCD,E),
  ABCDF= MAX(ABCD,F),
  ABCEF= MAX(ABCE,F),
  ABDEF= MAX(ABDE,F),
  ACDEF= MAX(ACDE,F),
  BCDEF= MAX(BCDE,F);

  in[4]= MIN (
  MIN4( ABCDE, ABCDF, ABCEF, ABDEF ),
  MIN ( ACDEF, BCDEF )
  );

  in[5] = MAX(ABCDE,F);
}

int main(int argc, char ** argv) {
  int d[6][6] = {
    {1, 2, 3, 4, 5, 6},
    {6, 5, 4, 3, 2, 1},
    {100, 2, 300, 4, 500, 6},
    {100, 2, 3, 4, 500, 6},
    {1, 200, 3, 4, 5, 600},
    {1, 1, 2, 1, 2, 1}
  };

  unsigned long long cycles = rdtsc();
  for (int i = 0; i < 6; i++) {
    sort6(d[i]);
  }
  cycles = rdtsc() - cycles;
  printf("Time is %d\n", (unsigned)cycles);

  for (int i = 0; i < 6; i++) {
    printf("d%d : %d %d %d %d %d %d\n", i,
     d[i][0], d[i][1], d[i][2],
     d[i][3], d[i][4], d[i][5]);
  }
}

EDIT:
Giải pháp thứ tự xếp hạng lấy cảm hứng từ Rex Kerr's, nhanh hơn nhiều so với mớ hỗn độn ở trên

static void sort6(int *o) {
const int 
A=o[0],B=o[1],C=o[2],D=o[3],E=o[4],F=o[5];
const unsigned char
AB = A>B, AC = A>C, AD = A>D, AE = A>E,
          BC = B>C, BD = B>D, BE = B>E,
                    CD = C>D, CE = C>E,
                              DE = D>E,
a =          AB + AC + AD + AE + (A>F),
b = 1 - AB      + BC + BD + BE + (B>F),
c = 2 - AC - BC      + CD + CE + (C>F),
d = 3 - AD - BD - CD      + DE + (D>F),
e = 4 - AE - BE - CE - DE      + (E>F);
o[a]=A; o[b]=B; o[c]=C; o[d]=D; o[e]=E;
o[15-a-b-c-d-e]=F;
}

1
luôn luôn tốt đẹp để xem các giải pháp mới. Có vẻ như một số tối ưu hóa dễ dàng là có thể. Cuối cùng, nó có thể không chứng minh quá khác biệt với Mạng sắp xếp.
kriss

Có, số lượng MIN và MAX có thể có thể giảm đi, ví dụ MIN (AB, CD) lặp đi lặp lại một vài lần, nhưng tôi giảm chúng rất nhiều sẽ khó khăn. Tôi đã thêm các trường hợp thử nghiệm của bạn.
PrincePolka

pmin / maxsw hoạt động trên các số nguyên có chữ ký 16 bit được đóng gói (int16_t ). Nhưng hàm C của bạn tuyên bố nó sắp xếp một mảng int(là 32 bit trong tất cả các cài đặt C hỗ trợ asmcú pháp đó ). Bạn đã kiểm tra nó chỉ với các số nguyên dương nhỏ chỉ có 0 trong nửa cao của chúng chưa? Điều đó sẽ hoạt động ... Đối với intbạn cần SSE4.1 pmin/maxsd(d = dword). felixcloutier.com/x86/pminsd:pminsq hoặc pminusdcho uint32_t.
Peter Cordes

1

Tôi thấy rằng ít nhất trên hệ thống của mình, các chức năng sort6_iterator()sort6_iterator_local()được xác định bên dưới đều chạy ít nhất là nhanh và thường nhanh hơn đáng kể so với người giữ bản ghi hiện tại ở trên:

#define MIN(x, y) (x<y?x:y)
#define MAX(x, y) (x<y?y:x)

template<class IterType> 
inline void sort6_iterator(IterType it) 
{
#define SWAP(x,y) { const auto a = MIN(*(it + x), *(it + y)); \
  const auto b = MAX(*(it + x), *(it + y)); \
  *(it + x) = a; *(it + y) = b; }

  SWAP(1, 2) SWAP(4, 5)
  SWAP(0, 2) SWAP(3, 5)
  SWAP(0, 1) SWAP(3, 4)
  SWAP(1, 4) SWAP(0, 3)
  SWAP(2, 5) SWAP(1, 3)
  SWAP(2, 4)
  SWAP(2, 3)
#undef SWAP
}

Tôi đã thông qua hàm này một std::vectortrình lặp trong mã thời gian của mình.

Tôi nghi ngờ (từ các bình luận như thế này và các nơi khác) rằng việc sử dụng các trình vòng lặp mang lại cho g ++ một số đảm bảo nhất định về những gì có thể và không thể xảy ra đối với bộ nhớ mà trình vòng lặp đề cập đến, điều mà nó không có và đó là những đảm bảo cho phép g ++ tối ưu hóa tốt hơn mã sắp xếp (ví dụ với con trỏ, trình biên dịch không thể chắc chắn rằng tất cả các con trỏ đang trỏ đến các vị trí bộ nhớ khác nhau). Nếu tôi nhớ chính xác, đây cũng là một phần lý do tại sao rất nhiều thuật toán STL, chẳng hạn nhưstd::sort() , thường có hiệu năng tốt đến mức khó hiểu.

Hơn nữa, sort6_iterator()một số lần (một lần nữa, tùy thuộc vào bối cảnh trong đó hàm được gọi) luôn vượt trội so với các chức năng sắp xếp sau, mà các bản sao dữ liệu vào các biến địa phương trước khi sắp xếp chúng. 1 Lưu ý rằng vì chỉ có 6 biến cục bộ được xác định, nếu các biến cục bộ này là nguyên thủy thì có khả năng chúng không bao giờ thực sự được lưu trữ trong RAM và thay vào đó chỉ được lưu trữ trong các thanh ghi của CPU cho đến khi kết thúc lệnh gọi, giúp thực hiện sắp xếp này Chức năng nhanh. (Nó cũng giúp trình biên dịch biết rằng các biến cục bộ riêng biệt có các vị trí riêng biệt trong bộ nhớ).

template<class IterType> 
inline void sort6_iterator_local(IterType it) 
{
#define SWAP(x,y) { const auto a = MIN(data##x, data##y); \
  const auto b = MAX(data##x, data##y); \
  data##x = a; data##y = b; }
//DD = Define Data
#define DD1(a)   auto data##a = *(it + a);
#define DD2(a,b) auto data##a = *(it + a), data##b = *(it + b);
//CB = Copy Back
#define CB(a) *(it + a) = data##a;

  DD2(1,2)    SWAP(1, 2)
  DD2(4,5)    SWAP(4, 5)
  DD1(0)      SWAP(0, 2)
  DD1(3)      SWAP(3, 5)
  SWAP(0, 1)  SWAP(3, 4)
  SWAP(1, 4)  SWAP(0, 3)   CB(0)
  SWAP(2, 5)  CB(5)
  SWAP(1, 3)  CB(1)
  SWAP(2, 4)  CB(4)
  SWAP(2, 3)  CB(2)        CB(3)
#undef CB
#undef DD2
#undef DD1
#undef SWAP
}

Lưu ý rằng việc xác định SWAP()như sau một số lần dẫn đến hiệu suất tốt hơn một chút mặc dù hầu hết thời gian nó dẫn đến hiệu suất kém hơn một chút hoặc chênh lệch không đáng kể về hiệu suất.

#define SWAP(x,y) { const auto a = MIN(data##x, data##y); \
  data##y = MAX(data##x, data##y); \
  data##x = a; }

Nếu bạn chỉ muốn một thuật toán sắp xếp mà trên các loại dữ liệu nguyên thủy, gcc -O3 luôn tối ưu hóa tốt cho dù cuộc gọi đến chức năng sắp xếp xuất hiện trong 1 thì tùy thuộc vào cách bạn vượt qua đầu vào, hãy thử một trong hai cách sau thuật toán:

template<class T> inline void sort6(T it) {
#define SORT2(x,y) {if(data##x>data##y){auto a=std::move(data##y);data##y=std::move(data##x);data##x=std::move(a);}}
#define DD1(a)   register auto data##a=*(it+a);
#define DD2(a,b) register auto data##a=*(it+a);register auto data##b=*(it+b);
#define CB1(a)   *(it+a)=data##a;
#define CB2(a,b) *(it+a)=data##a;*(it+b)=data##b;
  DD2(1,2) SORT2(1,2)
  DD2(4,5) SORT2(4,5)
  DD1(0)   SORT2(0,2)
  DD1(3)   SORT2(3,5)
  SORT2(0,1) SORT2(3,4) SORT2(2,5) CB1(5)
  SORT2(1,4) SORT2(0,3) CB1(0)
  SORT2(2,4) CB1(4)
  SORT2(1,3) CB1(1)
  SORT2(2,3) CB2(2,3)
#undef CB1
#undef CB2
#undef DD1
#undef DD2
#undef SORT2
}

Hoặc nếu bạn muốn truyền các biến bằng tham chiếu thì hãy sử dụng hàm này (hàm bên dưới khác với ở trên trong 5 dòng đầu tiên của nó):

template<class T> inline void sort6(T& e0, T& e1, T& e2, T& e3, T& e4, T& e5) {
#define SORT2(x,y) {if(data##x>data##y)std::swap(data##x,data##y);}
#define DD1(a)   register auto data##a=e##a;
#define DD2(a,b) register auto data##a=e##a;register auto data##b=e##b;
#define CB1(a)   e##a=data##a;
#define CB2(a,b) e##a=data##a;e##b=data##b;
  DD2(1,2) SORT2(1,2)
  DD2(4,5) SORT2(4,5)
  DD1(0)   SORT2(0,2)
  DD1(3)   SORT2(3,5)
  SORT2(0,1) SORT2(3,4) SORT2(2,5) CB1(5)
  SORT2(1,4) SORT2(0,3) CB1(0)
  SORT2(2,4) CB1(4)
  SORT2(1,3) CB1(1)
  SORT2(2,3) CB2(2,3)
#undef CB1
#undef CB2
#undef DD1
#undef DD2
#undef SORT2
}

Lý do sử dụng registertừ khóa là bởi vì đây là một trong số ít lần bạn biết rằng bạn muốn những giá trị này trong sổ đăng ký. Không có register, trình biên dịch sẽ tìm ra điều này hầu hết thời gian nhưng đôi khi không. Sử dụng registertừ khóa giúp giải quyết vấn đề này. Tuy nhiên, thông thường, không sử dụng registertừ khóa vì nó có khả năng làm chậm mã của bạn hơn là tăng tốc.

Ngoài ra, lưu ý việc sử dụng các mẫu. Điều này được thực hiện trên mục đích vì ngay cả với inlinetừ khóa, các hàm mẫu thường được tối ưu hóa mạnh mẽ hơn bởi gcc so với các hàm vanilla C (điều này phải làm với gcc cần xử lý các con trỏ hàm cho các hàm vanilla C nhưng không phải với các hàm mẫu).

  1. Trong khi định thời các hàm sắp xếp khác nhau, tôi nhận thấy rằng bối cảnh (tức là mã xung quanh) trong đó lệnh gọi hàm sắp xếp được thực hiện có tác động đáng kể đến hiệu suất, có thể là do hàm được nội tuyến và sau đó được tối ưu hóa. Ví dụ, nếu chương trình đủ đơn giản thì thường không có nhiều sự khác biệt về hiệu năng giữa việc truyền hàm sắp xếp một con trỏ so với chuyển nó qua một trình vòng lặp; mặt khác, sử dụng các trình vòng lặp thường mang lại hiệu suất tốt hơn rõ rệt và ít nhất (theo kinh nghiệm của tôi cho đến nay ít nhất) bất kỳ hiệu suất kém hơn đáng chú ý nào. Tôi nghi ngờ rằng điều này có thể là do g ++ có thể tối ưu hóa toàn bộ mã đơn giản.

0

Hãy thử 'sắp xếp danh sách sắp xếp'. :) Sử dụng hai mảng. Nhanh nhất cho mảng nhỏ và lớn.
Nếu bạn concating, bạn chỉ kiểm tra nơi chèn. Các giá trị lớn hơn khác mà bạn không cần so sánh (cmp = ab> 0).
Đối với 4 số, bạn có thể sử dụng hệ thống 4-5 cmp (~ 4.6) hoặc 3-6 cmp (~ 4.9). Sắp xếp bong bóng sử dụng 6 cmp (6). Rất nhiều cmp cho số lớn mã chậm hơn.
Mã này sử dụng 5 cmp (không phải loại MSL):
if (cmp(arr[n][i+0],arr[n][i+1])>0) {swap(n,i+0,i+1);} if (cmp(arr[n][i+2],arr[n][i+3])>0) {swap(n,i+2,i+3);} if (cmp(arr[n][i+0],arr[n][i+2])>0) {swap(n,i+0,i+2);} if (cmp(arr[n][i+1],arr[n][i+3])>0) {swap(n,i+1,i+3);} if (cmp(arr[n][i+1],arr[n][i+2])>0) {swap(n,i+1,i+2);}

Hiệu trưởng MSL 9 8 7 6 5 4 3 2 1 0 89 67 45 23 01 ... concat two sorted lists, list length = 1 6789 2345 01 ... concat two sorted lists, list length = 2 23456789 01 ... concat two sorted lists, list length = 4 0123456789 ... concat two sorted lists, list length = 8

mã js

function sortListMerge_2a(cmp)	
{
var step, stepmax, tmp, a,b,c, i,j,k, m,n, cycles;
var start = 0;
var end   = arr_count;
//var str = '';
cycles = 0;
if (end>3)
	{
	stepmax = ((end - start + 1) >> 1) << 1;
	m = 1;
	n = 2;
	for (step=1;step<stepmax;step<<=1)	//bounds 1-1, 2-2, 4-4, 8-8...
		{
		a = start;
		while (a<end)
			{
			b = a + step;
			c = a + step + step;
			b = b<end ? b : end;
			c = c<end ? c : end;
			i = a;
			j = b;
			k = i;
			while (i<b && j<c)
				{
				if (cmp(arr[m][i],arr[m][j])>0)
					{arr[n][k] = arr[m][j]; j++; k++;}
				else	{arr[n][k] = arr[m][i]; i++; k++;}
				}
			while (i<b)
				{arr[n][k] = arr[m][i]; i++; k++;
}
			while (j<c)
				{arr[n][k] = arr[m][j]; j++; k++;
}
			a = c;
			}
		tmp = m; m = n; n = tmp;
		}
	return m;
	}
else
	{
	// sort 3 items
	sort10(cmp);
	return m;
	}
}


0

Sắp xếp 4 mục với cách sử dụng cmp == 0. Số cmp là ~ 4,34 (FF gốc có ~ 4,52), nhưng mất gấp 3 lần so với danh sách hợp nhất. Nhưng tốt hơn ít hoạt động cmp, nếu bạn có số lượng lớn hoặc văn bản lớn. Chỉnh sửa: sửa lỗi

Kiểm tra trực tuyến http://mlich.zam.slu.cz/js-sort/x-sort-x2.htm

function sort4DG(cmp,start,end,n) // sort 4
{
var n     = typeof(n)    !=='undefined' ? n   : 1;
var cmp   = typeof(cmp)  !=='undefined' ? cmp   : sortCompare2;
var start = typeof(start)!=='undefined' ? start : 0;
var end   = typeof(end)  !=='undefined' ? end   : arr[n].length;
var count = end - start;
var pos = -1;
var i = start;
var cc = [];
// stabilni?
cc[01] = cmp(arr[n][i+0],arr[n][i+1]);
cc[23] = cmp(arr[n][i+2],arr[n][i+3]);
if (cc[01]>0) {swap(n,i+0,i+1);}
if (cc[23]>0) {swap(n,i+2,i+3);}
cc[12] = cmp(arr[n][i+1],arr[n][i+2]);
if (!(cc[12]>0)) {return n;}
cc[02] = cc[01]==0 ? cc[12] : cmp(arr[n][i+0],arr[n][i+2]);
if (cc[02]>0)
    {
    swap(n,i+1,i+2); swap(n,i+0,i+1); // bubble last to top
    cc[13] = cc[23]==0 ? cc[12] : cmp(arr[n][i+1],arr[n][i+3]);
    if (cc[13]>0)
        {
        swap(n,i+2,i+3); swap(n,i+1,i+2); // bubble
        return n;
        }
    else    {
    cc[23] = cc[23]==0 ? cc[12] : (cc[01]==0 ? cc[30] : cmp(arr[n][i+2],arr[n][i+3]));  // new cc23 | c03 //repaired
        if (cc[23]>0)
            {
            swap(n,i+2,i+3);
            return n;
            }
        return n;
        }
    }
else    {
    if (cc[12]>0)
        {
        swap(n,i+1,i+2);
        cc[23] = cc[23]==0 ? cc[12] : cmp(arr[n][i+2],arr[n][i+3]); // new cc23
        if (cc[23]>0)
            {
            swap(n,i+2,i+3);
            return n;
            }
        return n;
        }
    else    {
        return n;
        }
    }
return n;
}

1
Trường hợp sử dụng hơi khác với bối cảnh ban đầu của câu hỏi. Với chiều dài cố định sắp xếp các chi tiết quan trọng và đếm cmp của các giao dịch hoán đổi là không đủ. Tôi thậm chí sẽ không ngạc nhiên nếu nó không phải là loại thực sự sẽ tiêu tốn thời gian, nhưng một thứ gọi ánh sáng hoàn toàn khác biệt () trong init. Tôi không biết làm thế nào để thực hiện mesure thời gian thực tế bằng Javascript. Có lẽ với nút?
kriss

0

Có lẽ tôi muộn để đảng, nhưng ít nhất sự đóng góp của tôi là một mới cách tiếp cận.

  • thực sự nên được nội tuyến
  • ngay cả khi nội tuyến, có quá nhiều chi nhánh
  • phần phân tích về cơ bản là O (N (N-1)) có vẻ ổn với N = 6
  • mã có thể hiệu quả hơn nếu chi phíswap sẽ cao hơn (chi phí củacompare )
  • Tôi tin tưởng vào các chức năng tĩnh được nội tuyến.
  • Phương pháp này liên quan đến sắp xếp thứ hạng
    • thay vì hàng ngũ, hàng cấp bậc (độ lệch) được sử dụng.
    • tổng của các cấp bậc là 0 cho mọi chu kỳ trong bất kỳ nhóm hoán vị.
    • thay vì SWAP()ing hai phần tử, các chu trình được theo đuổi, chỉ cần một temp và một trao đổi (đăng ký-> đăng ký) (mới <- cũ).

Cập nhật: đã thay đổi mã một chút, một số người sử dụng trình biên dịch C ++ để biên dịch mã C ...

#include <stdio.h>

#if WANT_CHAR
typedef signed char Dif;
#else
typedef signed int Dif;
#endif

static int walksort (int *arr, int cnt);
static void countdifs (int *arr, Dif *dif, int cnt);
static void calcranks(int *arr, Dif *dif);

int wsort6(int *arr);

void do_print_a(char *msg, int *arr, unsigned cnt)
{
fprintf(stderr,"%s:", msg);
for (; cnt--; arr++) {
        fprintf(stderr, " %3d", *arr);
        }
fprintf(stderr,"\n");
}

void do_print_d(char *msg, Dif *arr, unsigned cnt)
{
fprintf(stderr,"%s:", msg);
for (; cnt--; arr++) {
        fprintf(stderr, " %3d", (int) *arr);
        }
fprintf(stderr,"\n");
}

static void inline countdifs (int *arr, Dif *dif, int cnt)
{
int top, bot;

for (top = 0; top < cnt; top++ ) {
        for (bot = 0; bot < top; bot++ ) {
                if (arr[top] < arr[bot]) { dif[top]--; dif[bot]++; }
                }
        }
return ;
}
        /* Copied from RexKerr ... */
static void inline calcranks(int *arr, Dif *dif){

dif[0] =     (arr[0]>arr[1])+(arr[0]>arr[2])+(arr[0]>arr[3])+(arr[0]>arr[4])+(arr[0]>arr[5]);
dif[1] = -1+ (arr[1]>=arr[0])+(arr[1]>arr[2])+(arr[1]>arr[3])+(arr[1]>arr[4])+(arr[1]>arr[5]);
dif[2] = -2+ (arr[2]>=arr[0])+(arr[2]>=arr[1])+(arr[2]>arr[3])+(arr[2]>arr[4])+(arr[2]>arr[5]);
dif[3] = -3+ (arr[3]>=arr[0])+(arr[3]>=arr[1])+(arr[3]>=arr[2])+(arr[3]>arr[4])+(arr[3]>arr[5]);
dif[4] = -4+ (arr[4]>=arr[0])+(arr[4]>=arr[1])+(arr[4]>=arr[2])+(arr[4]>=arr[3])+(arr[4]>arr[5]);
dif[5] = -(dif[0]+dif[1]+dif[2]+dif[3]+dif[4]);
}

static int walksort (int *arr, int cnt)
{
int idx, src,dst, nswap;

Dif difs[cnt];

#if WANT_REXK
calcranks(arr, difs);
#else
for (idx=0; idx < cnt; idx++) difs[idx] =0;
countdifs(arr, difs, cnt);
#endif
calcranks(arr, difs);

#define DUMP_IT 0
#if DUMP_IT
do_print_d("ISteps ", difs, cnt);
#endif

nswap = 0;
for (idx=0; idx < cnt; idx++) {
        int newval;
        int step,cyc;
        if ( !difs[idx] ) continue;
        newval = arr[idx];
        cyc = 0;
        src = idx;
        do      {
                int oldval;
                step = difs[src];
                difs[src] =0;
                dst = src + step;
                cyc += step ;
                if(dst == idx+1)idx=dst;
                oldval = arr[dst];
#if (DUMP_IT&1)
                fprintf(stderr, "[Nswap=%d] Cyc=%d Step=%2d Idx=%d  Old=%2d New=%2d #### Src=%d Dst=%d[%2d]->%2d <-- %d\n##\n"
                        , nswap, cyc, step, idx, oldval, newval
                        , src, dst, difs[dst], arr[dst]
                        , newval  );
                do_print_a("Array ", arr, cnt);
                do_print_d("Steps ", difs, cnt);
#endif

                arr[dst] = newval;
                newval = oldval;
                nswap++;
                src = dst;
                } while( cyc);
        }

return nswap;
}
/*************/
int wsort6(int *arr)
{
return walksort(arr, 6);
}

trông giống như một loại bong bóng. Có khả năng là một ứng cử viên tốt để thực hiện chậm nhất, nhưng vẫn có thể được quan tâm để biết nếu làm việc trên mã làm cho sự khác biệt đó nhiều. Vui lòng đặt mã của bạn ở cùng định dạng với các mã khác, do đó chúng tôi có thể chạy điểm chuẩn trên đó.
kriss

@kriss en.wikipedia.org/wiki/Permuting_group Nó chắc chắn không phải là sắp xếp bong bóng: mã phát hiện các chu kỳ trong hoán vị đã cho và đi theo các chu kỳ này, đặt từng phần tử ở vị trí cuối cùng. wsort6()Chức năng cuối cùng có giao diện chính xác.
tham gia

@joop: xấu của tôi, không có loại bong bóng thực sự. Điều đó đang được nói trong bối cảnh tôi vẫn hy vọng mã sẽ tồi tệ hơn nhiều so với bất kỳ triển khai hiện tại nào khác. Bằng cách này, giải pháp Thứ tự xếp hạng là tối ưu về số lần hoán đổi vì nó trực tiếp tìm vị trí cuối cùng của mọi vật phẩm. Cũng không rõ liệu walksort thậm chí có hoạt động hay không khi chúng ta loại bỏ giả thuyết rằng tất cả các số được sắp xếp đều khác nhau như ở đây. Để điểm chuẩn mã chúng ta nên mã theo dõi. Ngoài ra, như tôi thường biên dịch trên trình biên dịch C ++, mã sẽ không hoạt động vì OP gọi là biến "mới" (và điều đó phá vỡ tô sáng cú pháp).
kriss

Phương pháp này rất gần với thứ tự xếp hạng, chỉ có các bài tập cuối cùng được thực hiện tại chỗ . Ngoài các cấp bậc o1..o5, không cần e[6]mảng tạm thời thứ hai . Và: biên dịch mã C trên trình biên dịch C ++ và đổ lỗi cho mã?
tham gia

@greybeard: cảm ơn bạn, tôi đã thêm một khoảng trắng trước đó #include. Cố định
wildplasser

0
//Bruteforce compute unrolled count dumbsort(min to 0-index)
void bcudc_sort6(int* a)
{
    int t[6] = {0};
    int r1,r2;

    r1=0;
    r1 += (a[0] > a[1]);
    r1 += (a[0] > a[2]);
    r1 += (a[0] > a[3]);
    r1 += (a[0] > a[4]);
    r1 += (a[0] > a[5]);
    while(t[r1]){r1++;}
    t[r1] = a[0];

    r2=0;
    r2 += (a[1] > a[0]);
    r2 += (a[1] > a[2]);
    r2 += (a[1] > a[3]);
    r2 += (a[1] > a[4]);
    r2 += (a[1] > a[5]);
    while(t[r2]){r2++;} 
    t[r2] = a[1];

    r1=0;
    r1 += (a[2] > a[0]);
    r1 += (a[2] > a[1]);
    r1 += (a[2] > a[3]);
    r1 += (a[2] > a[4]);
    r1 += (a[2] > a[5]);
    while(t[r1]){r1++;}
    t[r1] = a[2];

    r2=0;
    r2 += (a[3] > a[0]);
    r2 += (a[3] > a[1]);
    r2 += (a[3] > a[2]);
    r2 += (a[3] > a[4]);
    r2 += (a[3] > a[5]);
    while(t[r2]){r2++;} 
    t[r2] = a[3];

    r1=0;
    r1 += (a[4] > a[0]);
    r1 += (a[4] > a[1]);
    r1 += (a[4] > a[2]);
    r1 += (a[4] > a[3]);
    r1 += (a[4] > a[5]);
    while(t[r1]){r1++;}
    t[r1] = a[4];

    r2=0;
    r2 += (a[5] > a[0]);
    r2 += (a[5] > a[1]);
    r2 += (a[5] > a[2]);
    r2 += (a[5] > a[3]);
    r2 += (a[5] > a[4]);
    while(t[r2]){r2++;} 
    t[r2] = a[5];

    a[0]=t[0];
    a[1]=t[1];
    a[2]=t[2];
    a[3]=t[3];
    a[4]=t[4];
    a[5]=t[5];
}

static __inline__ void sort6(int* a)
{
    #define wire(x,y); t = a[x] ^ a[y] ^ ( (a[x] ^ a[y]) & -(a[x] < a[y]) ); a[x] = a[x] ^ t; a[y] = a[y] ^ t;
    register int t;

    wire( 0, 1); wire( 2, 3); wire( 4, 5);
    wire( 3, 5); wire( 0, 2); wire( 1, 4);
    wire( 4, 5); wire( 2, 3); wire( 0, 1); 
    wire( 3, 4); wire( 1, 2); 
    wire( 2, 3);

    #undef wire
}

Bất kể tốc độ bạn có chắc chắn nó hoạt động? Trong bruteforce sắp xếp các vòng lặp của bạn là đáng ngờ. Dường như với tôi họ sẽ không làm việc nếu chúng ta có số 0 trong các giá trị được sắp xếp.
kriss

1
mảng t [6] được khởi tạo thành 0x0. Vì vậy, nó không quan trọng ở đâu và nếu một khóa có giá trị 0x0 sẽ được viết.
FranG

-1

Chà, nếu chỉ có 6 yếu tố và bạn có thể tận dụng sự song song, muốn giảm thiểu phân nhánh có điều kiện, v.v ... Tại sao bạn không tạo ra tất cả các kết hợp và kiểm tra theo thứ tự? Tôi sẽ mạo hiểm rằng trong một số kiến ​​trúc, nó có thể khá nhanh (miễn là bạn có bộ nhớ được sắp xếp trước)


9
Có 720 thứ tự, và các phiên bản nhanh dưới 100 chu kỳ. Ngay cả khi tính song song lớn có thể là đòn bẩy, với quy mô nhỏ như vậy, chi phí tạo và đồng bộ hóa các luồng có thể sẽ vượt quá chi phí chỉ sắp xếp các mảng trên một lõi.
Kevin Stock

-3

Dưới đây là ba phương pháp sắp xếp điển hình đại diện cho ba loại Thuật toán sắp xếp khác nhau:

Insertion Sort: Θ(n^2)

Heap Sort: Θ(n log n)

Count Sort: Θ(3n)

Nhưng hãy xem thảo luận của Stefan Nelsson về thuật toán sắp xếp nhanh nhất? nơi anh ấy thảo luận về một giải pháp đi xuống O(n log log n).. kiểm tra việc thực hiện nó trong C

Thuật toán sắp xếp bán tuyến tính này đã được trình bày bởi một bài báo vào năm 1995:

A. Andersson, T. Hagerup, S. Nilsson và R. Raman. Sắp xếp theo thời gian tuyến tính? Trong Kỷ yếu của Hội nghị chuyên đề ACM hàng năm lần thứ 27 về Lý thuyết tính toán, trang 427-436, 1995.


8
Điều này là thú vị nhưng bên cạnh điểm. Big-được dự định để ẩn các yếu tố không đổi và hiển thị xu hướng khi kích thước vấn đề (n) lớn. Vấn đề ở đây hoàn toàn là về quy mô vấn đề cố định (n = 6) và tính đến các yếu tố không đổi.
kriss

@kriss bạn nói đúng, so sánh của tôi không có triệu chứng, vì vậy so sánh thực tế sẽ hiển thị nếu nó nhanh hơn hay không cho trường hợp đó
Khaled.K

4
Bạn không thể kết luận, vì mỗi thuật toán khác nhau ẩn một hằng số nhân K khác nhau (và cả hằng số cộng C). tức là: k0, c0 cho sắp xếp chèn, k1, c1 cho sắp xếp heap, v.v. Tất cả các hằng số thực sự khác nhau (bạn có thể nói trong các thuật ngữ vật lý rằng mỗi thuật toán có "hệ số ma sát" riêng), bạn không thể kết luận thuật toán thực sự nhanh hơn trong trường hợp này (hoặc bất kỳ trường hợp n cố định nào).
kriss
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.