2011-11-14 34 views
20

Tôi đang viết mã toán học cần nhân số lớn nhanh. Nó phân tách thành phép nhân của một mảng các số nguyên với một số nguyên duy nhất. Trong C++ này trông như thế này (trên của unsigned):Tối ưu hóa bộ kết hợp x64 Vòng lặp MUL

void muladd(unsigned* r, const unsigned* a, unsigned len, unsigned b) { 
    unsigned __int64 of = 0; // overflow 
    unsigned i = 0; // loop variable 
    while (i < len) { 
     of += (unsigned __int64)a[i] * b + r[i]; 
     r[i] = (unsigned)of; 
     of >>= 32; 
     ++i; 
    } 
    r[i] = (unsigned)of; // save overflow 
} 

tôi trải ra vòng lặp này bằng tay, chuyển nó đến 64 bit và làm việc trên đầu ra trình biên dịch .asm để tối ưu hóa nó hơn nữa. Vòng lặp chính hiện tại trông giống như sau:

mov rax, rdi        ; rdi = b 
mul QWORD PTR [rbx+r10*8-64]    ; rdx:rax = a[i] * b; r10 = i 
mov rsi, QWORD PTR [r14+r10*8-64]  ; r14 = r; rsi = r[i] 
add rax, rsi 
adc rdx, 0 
add rax, r11        ; r11 = of (low part) 
adc rdx, 0 
mov QWORD PTR [r14+r10*8-64], rax  ; save result 
mov r11, rdx 

; this repeats itself 8 times with different offsets 

Khi tôi đo điểm chuẩn này, tôi nhận thấy khoảng 6,3 chu kỳ trên avarage mỗi phép nhân trên Core2 Quad của tôi.

Câu hỏi của tôi là: tôi có thể tăng tốc độ này bằng cách nào đó không? Thật không may, tôi thấy không có cách nào để tránh một trong những bổ sung và phép nhân luôn luôn cần RDX: RAX, vì vậy tôi cần phải di chuyển dữ liệu xung quanh và không thể sắp xếp "nhân song song".

Có ý tưởng nào không?

Cập nhật: Sau một số thử nghiệm khác, tôi đã quản lý tốc độ lên tới khoảng 5.4 chu kỳ trên MUL 64 bit (bao gồm tất cả thêm, di chuyển và vòng lặp). Tôi đoán đây là điều tốt nhất bạn có thể nhận được trên một Core2, vì Core2 không có lệnh MUL rất nhanh: nó có thông lượng là 3 và độ trễ là 6 (resp. 7). Sandy bridge sẽ tốt hơn nhiều với thông lượng là 1 và độ trễ của chu kỳ 3 (resp. 4).

Về số lượng thấp hơn nhiều đối với GMP: tôi nhận được điều đó từ mã nguồn của họ và có vẻ như với tôi rằng đó là một con số lý thuyết. Nhưng điều chắc chắn là nó là một con số được tính cho một CPU AMD K9. Và từ những gì tôi đã đọc, tôi thu thập các AMD có một đơn vị MUL nhanh hơn các chip Intel (cũ hơn).

+7

Bạn có thể muốn xem xét một số quy trình lắp ráp trong GMP. Họ có một chức năng mà thực hiện chính xác điều này và được viết trong lắp ráp cho hầu hết các bộ vi xử lý bao gồm x64. – Mysticial

+0

GMP thực sự có hỗ trợ tốt cho một mul_basecase nhanh và appearantly phải mất một số 2.35 chu kỳ mỗi MUL, rất tốt đẹp. Nếu tôi hiểu nó một cách chính xác, chúng nhân hai vectơ xen kẽ nhau, điều đó dường như giữ cho các phụ thuộc thấp và cải thiện việc xử lý tràn. – cxxl

Trả lời

0

Dường như thường trình của bạn có thể hưởng lợi từ SSE. PMULLD và PADDD có vẻ như hướng dẫn có liên quan. Không chắc chắn tại sao trình biên dịch của bạn không tạo ra SSE từ đó.

+0

Làm việc với các hệ số nhân 32 bit x 32 bit. Nhưng không phải cho nhân 64-bit x 64-bit. – Mysticial

+1

Bạn có thực sự cần nhân khẩu qword khi bạn chỉ giữ từ khóa quan trọng nhất? –

+0

Tôi lưu RAX về bộ nhớ và RDX được sử dụng dưới dạng carry (thông qua R11) và được thêm vào phần tử tiếp theo. Thật không may, tôi cần QWORD MUL. – cxxl

0

Tôi chỉ muốn chỉ ra rằng việc tính chu kỳ là vô ích vì hướng dẫn của bạn sẽ được chuyển thành mã micro sẽ được thực hiện theo thứ tự hoặc bị tạm dừng tùy thuộc vào mọi thứ khác mà CPU đang thực hiện. Nếu bạn có một thói quen nhanh chóng, mà bạn làm, nó không thực sự fruitfull để thử và cạo râu một chu kỳ lý thuyết, trừ khi bạn biết thói quen của bạn sẽ luôn luôn chạy trong cô lập hoàn toàn.

+1

OP benchmarked mã của mình, và rõ ràng là có kết quả lặp lại. Anh ta không đếm chu kỳ lý thuyết, anh ta thực sự đo chu kỳ thực tế. Các hướng dẫn được dịch sang microcode và được sắp xếp lại có thể dự đoán được và khá nổi tiếng (xem www.agner.org). Hơn nữa _complete_ cách ly là không cần thiết để tối ưu hóa, một hệ điều hành trong nền chạy mã sẽ thường không cạo ra nhiều hơn một vài phần trăm, nếu ở tất cả. – hirschhornsalz

+0

Oh xin lỗi, tôi đã quên rằng anh ấy đã lược tả nó :) – Tobias

+0

Đây phải là nhận xét chứ không phải trả lời. –

1

Tôi đã từng viết một vòng lặp trông giống như thế này, với số lượng xử lý tối thiểu trên nhiều dữ liệu với kết quả là vòng lặp bị giới hạn bởi tốc độ bộ nhớ.

Tôi muốn thử tìm nạp trước a [i] và r [i]

nếu sử dụng gcc sử dụng chức năng __builtin_prefetch() hoặc hướng dẫn PREFETCHT0 trong lắp ráp

http://gcc.gnu.org/onlinedocs/gcc-3.3.6/gcc/Other-Builtins.html

Khi này hoạt động kết quả có thể gây ấn tượng. Vì vậy, miễn là vòng lặp dài một nghìn lần lặp lại, tôi sẽ tìm nạp trước [i + 64] và r [i + 64] làm điểm bắt đầu và xem mức độ khác biệt của nó đối với CPU của bạn. Bạn có thể cần thử khoảng cách tìm nạp trước lớn hơn.

+1

Tôi đã thử điều đó. Kết quả là nó không có sự khác biệt về Core2 Quad của tôi. Qua các hướng dẫn sử dụng CPU và hướng dẫn của Agner Fog, tôi có ý tưởng rằng các bộ vi xử lý ngày nay có một logic prefetch tốt để phát hiện các vòng lặp đơn giản khá tốt, do đó không cần tìm nạp trước thủ công. – cxxl

0

R có chứa bất kỳ điều gì quan trọng trước cuộc gọi không?

Nếu có, và bạn đang tích lũy vào nó, sau đó dừng đọc ngay bây giờ.

Nếu không (nghĩa là bạn luôn tích lũy số không) và giả sử bạn đang gọi hàm này trên mảng lớn hơn đáng kể so với kích thước bộ nhớ cache, thì tôi sẽ tìm cách loại bỏ nhu cầu đọc từ r và chuyển đổi "kết quả lưu" MOV thành MOVNT (_mm_stream_ps trong nội tại).

Điều này có thể cải thiện đáng kể hiệu suất. Làm sao ? Hiện tại, bộ nhớ cache của bạn đang tìm nạp các dòng bộ nhớ cache từ một, tìm nạp các dòng bộ nhớ cache từ r và ghi các dòng bộ nhớ cache trở lại r. Với các cửa hàng phát trực tuyến như vậy, bạn chỉ cần tìm nạp các dòng bộ nhớ cache từ một và ghi trực tiếp tới r: lưu lượng xe buýt ít hơn nhiều. Nếu bạn nhìn vào bất kỳ triển khai memcpy hiện đại nào của CRT, nó sẽ chuyển sang sử dụng các cửa hàng trực tuyến trên một số ngưỡng liên quan đến kích thước bộ nhớ cache (và chạy almost twice as fast như một memcpy sử dụng các bước di chuyển thông thường).

+0

Điều đó rất thú vị. 'R' trống trên cuộc gọi của hàm, nhưng bị từ từ lấp đầy. Plus, sau khi chức năng được thực hiện, tôi hy vọng rằng nó sẽ được sử dụng cho một cái gì đó (vì nó là kết quả :)). Tôi mong đợi MOVNT không được thuận lợi, vì chúng tôi đang lấp đầy 'r' một cách tuần tự. Agner Fog viết "phương pháp lưu trữ dữ liệu mà không có bộ nhớ đệm là thuận lợi nếu, và chỉ khi nào, nhớ cache cấp 2 có thể được mong đợi" (http://www.agner.org/optimize/optimizing_cpp.pdf). Tôi nghĩ rằng một bộ nhớ cache L2 bỏ lỡ có thể được loại trừ trong 99%. – cxxl