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ể?
__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.
CMP EAX, EBX; SBB EAX, EAX
sẽ đặt 0 hoặc 0xFFFFFFFF EAX
tùy thuộc vào việc EAX
lớn hơn hoặc nhỏ hơn EBX
tương ứng. SBB
là "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 là bit mang. Sau đó, một lần nữa, tôi nhớ rằng ADC
và SBB
có độ trễ khủng khiếp & thông trên Pentium 4 vs ADD
và SUB
, và vẫn còn gấp đôi chậm trên CPU Core. Vì 80386 cũng có hướng dẫn SETcc
lư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.
x-y
vàx+y
sẽ không gây ra tràn hoặc tràn?