2010-02-05 25 views
13

Tôi đã nhìn thấy blog này:bài toán tối ưu Odd dưới MSVC

http://igoro.com/archive/gallery-of-processor-cache-effects/

Các "weirdness" trong phần 7 là những gì bắt gặp sự quan tâm của tôi.

Suy nghĩ đầu tiên của tôi là "Thats chỉ C# là lạ".

Không phải tôi đã viết mã C++ sau đây.

volatile int* p = (volatile int*)_aligned_malloc(sizeof(int) * 8, 64); 
memset((void*)p, 0, sizeof(int) * 8); 

double dStart = t.GetTime(); 

for (int i = 0; i < 200000000; i++) 
{ 
    //p[0]++;p[1]++;p[2]++;p[3]++; // Option 1 
    //p[0]++;p[2]++;p[4]++;p[6]++; // Option 2 
    p[0]++;p[2]++;     // Option 3 
} 

double dTime = t.GetTime() - dStart; 

Thời điểm tôi nhận được trên 2,4 Ghz của tôi Core 2 Quad đi như sau:

Option 1 = ~8 cycles per loop. 
Option 2 = ~4 cycles per loop. 
Option 3 = ~6 cycles per loop. 

Bây giờ Đây là khó hiểu. Lý do của tôi đằng sau sự khác biệt đi xuống đến bộ nhớ cache ghi độ trễ (3 chu kỳ) trên chip của tôi và giả định rằng bộ nhớ cache có một cổng ghi 128-bit (Đây là công việc đoán thuần túy về phía tôi).

Trên cơ sở đó trong Tùy chọn 1: Nó sẽ tăng p [0] (1 chu kỳ) sau đó tăng p [2] (1 chu kỳ) sau đó phải đợi 1 chu kỳ (cho bộ đệm) rồi p [1] (1 chu kỳ) sau đó chờ 1 chu trình (cho bộ đệm) rồi p [3] (1 chu kỳ). Cuối cùng là 2 chu kỳ tăng và nhảy (Mặc dù nó thường được thực hiện như là giảm và nhảy). Điều này cho tổng cộng 8 chu kỳ.

Trong tùy chọn 2: Nó có thể tăng p [0] và p [4] trong một chu kỳ rồi tăng p [2] và p [6] trong một chu kỳ khác. Sau đó, 2 chu kỳ để trừ và nhảy. Không cần chờ đợi trên bộ nhớ cache. Tổng số 4 chu kỳ.

Trong tùy chọn 3: Nó có thể tăng p [0] sau đó phải đợi 2 chu kỳ sau đó tăng p [2] rồi trừ và nhảy. Vấn đề là nếu bạn đặt trường hợp 3 để tăng p [0] và p [4] nó STILL mất 6 chu kỳ (mà kinda thổi 128-bit của tôi đọc/ghi cổng ra khỏi nước).

Vậy ... có ai có thể cho tôi biết địa ngục đang diễn ra ở đây không? Tại sao trường hợp 3 mất nhiều thời gian hơn? Ngoài ra tôi rất muốn biết những gì tôi đã sai trong suy nghĩ của tôi ở trên, như tôi rõ ràng là có điều gì đó sai trái! Bất kỳ ý tưởng sẽ được nhiều đánh giá cao! :)

Nó cũng sẽ rất thú vị để xem cách GCC hoặc bất kỳ trình biên dịch khác đối phó với nó là tốt!

Chỉnh sửa: Ý tưởng của Jerry Coffin đã cho tôi một số suy nghĩ.

tôi đã thực hiện một số xét nghiệm hơn (trên một máy khác nhau để tha thứ cho sự thay đổi trong timings) có và không có nops và với số lượng khác nhau của nops

case 2 - 0.46 00401ABD jne   (401AB0h) 

0 nops - 0.68 00401AB7 jne   (401AB0h) 
1 nop - 0.61 00401AB8 jne   (401AB0h) 
2 nops - 0.636 00401AB9 jne   (401AB0h) 
3 nops - 0.632 00401ABA jne   (401AB0h) 
4 nops - 0.66 00401ABB jne   (401AB0h) 
5 nops - 0.52 00401ABC jne   (401AB0h) 
6 nops - 0.46 00401ABD jne   (401AB0h) 
7 nops - 0.46 00401ABE jne   (401AB0h) 
8 nops - 0.46 00401ABF jne   (401AB0h) 
9 nops - 0.55 00401AC0 jne   (401AB0h) 

Tôi đã bao gồm statetements nhảy để bạn có thể thấy rằng nguồn và đích nằm trong một dòng bộ nhớ cache. Bạn cũng có thể thấy rằng chúng tôi bắt đầu nhận được sự khác biệt khi chúng tôi cách nhau 13 byte trở lên. Cho đến khi chúng tôi đạt 16 ... thì tất cả đều sai.

Vì vậy, Jerry không đúng (mặc dù đề xuất của anh KHÔNG giúp được chút ít), tuy nhiên có điều gì đó đang diễn ra. Tôi càng bị cuốn hút và cố gắng tìm hiểu xem nó là gì bây giờ. Nó xuất hiện để được nhiều hơn một số loại liên kết bộ nhớ kỳ quặc chứ không phải là một số loại thông qua lệnh lẻ oddity.

Bất cứ ai muốn giải thích điều này cho một tâm trí tò mò? : D

Chỉnh sửa 3: Interjay có một điểm trên việc hủy đăng ký sẽ thổi chỉnh sửa trước đó ra khỏi nước. Với một vòng lặp chưa được kiểm tra, hiệu suất không cải thiện.Bạn cần phải thêm một nop vào để làm cho khoảng cách giữa nguồn nhảy và đích giống như cho số đếm tốt của tôi ở trên. Hiệu suất vẫn còn hút. Thú vị của nó là tôi cần 6 nops để cải thiện hiệu suất mặc dù. Tôi tự hỏi có bao nhiêu nops bộ vi xử lý có thể phát hành cho mỗi chu kỳ? Nếu 3 của nó sau đó là tài khoản cho bộ nhớ cache ghi độ trễ ... Nhưng, nếu thats nó, tại sao độ trễ xảy ra?

Tò mò và tò mò ...

+0

FWIW, thật dễ dàng để có được GCC chạy trên chỉ là về bất kỳ hệ điều hành để so sánh, và bạn có thể tự do được Trình biên dịch của Intel cho một số. Cài đặt icc đã chết đơn giản đối với tôi trên Ubuntu, chỉ cần nhớ rằng bạn phải có chip Intel để tận dụng tối ưu hóa của nó. –

+0

GCi32 là gì? – jalf

+0

Điều duy nhất tôi có thể nghĩ đến là một số thuật toán lập lịch trình. Vì vòng lặp ngắn hơn, CPU có thể phải trì hoãn một vài chu kỳ giữa các lần lặp để đợi ghi hoàn thành, vì lý do nào đó khiến cho ** thêm chậm lại làm chậm hơn vòng lặp dài hơn.Độ trễ của bộ nhớ cache có vẻ như nó ảnh hưởng đến tất cả các trường hợp như nhau và giống như bạn nói, chiều rộng cổng R/W dường như không phải như vậy. Yếu tố duy nhất tôi có thể tưởng tượng có thể khiến vòng lặp ngắn hơn mất * lâu hơn * là một số loại giới hạn lập lịch trong CPU. – jalf

Trả lời

3

Vâng, tôi đã có một cuộc trò chuyện ngắn với một kỹ sư intel về chính xác vấn đề này và nhận được phản ứng này:

Đó là rõ ràng cái gì để làm mà hướng dẫn kết thúc, trong đó đơn vị thực hiện, làm thế nào một cách nhanh chóng các điểm máy một vấn đề về số lượt tải hàng và cách nhanh chóng và thanh lịch với việc hủy đăng ký thực thi đầu cơ để đối phó với nó (hoặc nếu phải mất nhiều chu kỳ vì một số xung đột nội bộ). Nhưng điều đó nói rằng - bạn cần có một pipetrace chi tiết và mô phỏng chi tiết để tìm ra điều này. Dự đoán xử lý lệnh out-of-order trong các đường ống này là quá khó để làm trên giấy, ngay cả đối với những người thiết kế máy. Đối với giáo dân - không có hy vọng trong địa ngục. Lấy làm tiếc!

Ao Tôi nghĩ rằng tôi muốn thêm câu trả lời ở đây và đóng câu hỏi này một lần và cho tất cả :)

2

Điều này dường như không liên quan đến trình biên dịch. Lúc đầu, tôi nghĩ rằng nó có thể là do thủ thuật biên dịch như vòng lặp unrolling, nhưng nhìn vào lắp ráp được tạo ra, MSVC 9,0 chỉ tạo ra một bản dịch đơn giản từ mã C + +.

Lựa chọn 1:

[email protected]: 
    add DWORD PTR [esi], ecx 
    add DWORD PTR [esi+4], ecx 
    add DWORD PTR [esi+8], ecx 
    add DWORD PTR [esi+12], ecx 
    sub eax, ecx 
    jne SHORT [email protected] 

Phương án 2:

[email protected]: 
    add DWORD PTR [esi], ecx 
    add DWORD PTR [esi+8], ecx 
    add DWORD PTR [esi+16], ecx 
    add DWORD PTR [esi+24], ecx 
    sub eax, ecx 
    jne SHORT [email protected] 

Lựa chọn 3:

[email protected]: 
    add DWORD PTR [esi], ecx 
    add DWORD PTR [esi+8], ecx 
    sub eax, ecx 
    jne SHORT [email protected] 
+0

Vâng tôi đã đi đến cùng một kết luận. Do đó tôi nhìn vào những điều kỳ quặc có thể có của việc sử dụng bộ nhớ cache như ghi-cổng và đưa bộ nhớ cache ghi trễ vào trò chơi. – Goz

2

Các tập lệnh x86 là không có đại diện cách nữa cho những gì đang thực sự được thực hiện bởi CPU. Các hướng dẫn được dịch sang ngôn ngữ máy nội bộ, thuật ngữ "micro-op" được đặt ra trong 486 ngày. Vứt bỏ những thứ như đổi tên đăng ký, thực thi đầu cơ, nhiều đơn vị thực thi và tương tác của chúng với bộ nhớ cache và không có cách nào để dự đoán thời gian sẽ mất bao lâu nữa. Các nhà sản xuất chip đã ngừng đăng dự đoán thời gian chu kỳ từ lâu. Thiết kế của họ là một bí mật thương mại.

+0

Trong khi có bạn nói đúng, đến một mức độ nào đó, tất cả mọi thứ trong điều này sẽ được hoạt động ra khỏi bộ nhớ cache. Điều này dường như tôi là một caveat tối ưu hóa quan trọng và tuy nhiên bí mật thời gian chu kỳ của họ là một hit 50% cho làm một nửa công việc nhiều là một hit lớn. Đây là loại điều mà những người thích intel thường vui vẻ giải thích cho mọi người bởi vì nó làm cho chip của họ trông tốt khi mọi người viết mã cực nhanh. Tôi chắc chắn nó phải được giải thích ở đâu đó. – Goz

+0

@nobugz: Cả Intel và AMD vẫn ghi lại thời gian chờ cho các hướng dẫn riêng lẻ. Tất nhiên, chỉ có rất nhiều cảnh báo về cách các hướng dẫn được lên lịch và thực hiện song song, và đặc biệt là về hệ thống con bộ nhớ/bộ nhớ cache. – jalf

+1

@Goz: Tôi nghi ngờ rằng nó không phải là một hit 50%, nhưng thay vì tăng tốc 33% cho vòng lặp dài hơn. Cơ thể vòng lặp quá ngắn đối với trường hợp 3 mà bạn có thể gặp phải rất nhiều hạn chế về phần cứng (phải có một vài chu kỳ giữa các lệnh nhảy để xác minh dự đoán của dự đoán chi nhánh. phụ thuộc vào tải/lưu trữ và tôi nghi ngờ tốc độ trong trường hợp 2 là do một số tối ưu hóa đặc biệt khởi động, thường không áp dụng và vì lý do nào đó không thể sử dụng cho trường hợp ngắn hơn 3. – jalf

3

Tôi thật sự nghi ngờ những gì bạn thấy là một sự kỳ quặc của dự đoán nhánh thay vì bất cứ điều gì liên quan đến bộ nhớ đệm. Đặc biệt, trên khá một vài CPU, dự đoán nhánh không hoạt động (tốt nhất) khi cả nguồn và đích của nhánh nằm trong cùng một dòng bộ nhớ cache. Đặt đủ mã bên trong vòng lặp (ngay cả NOP) để lấy nguồn và nhắm mục tiêu vào các dòng bộ nhớ cache khác nhau sẽ cho một sự cải thiện đáng kể về tốc độ.

+0

BP phải hoạt động ở mức độ nào đó, hoặc anh ta thấy hiệu suất kém hơn nhiều so với 6 chu kỳ trên mỗi lần lặp.Nhưng vâng, điểm tốt. Tôi đã đề xuất một số vấn đề dự đoán chi nhánh trong một nhận xét khác nữa, nhưng tôi không biết giới hạn "cùng một dòng bộ nhớ cache". Nghe có vẻ như một dự đoán tốt. – jalf

+0

@jalf: Nếu bộ nhớ phục vụ, "không hoạt động chút nào" chỉ có trong Pentium MMX (và có thể là Pentium gốc). Trên các bộ vi xử lý mới hơn, nó hoạt động ở ít nhất một mức độ nào đó, nhưng vẫn gần như không tốt cho các bước nhảy dài hơn. –

+0

Tôi đã thử bỏ vòng lặp cho tùy chọn # 3 sao cho nó có cùng kích thước với các tùy chọn # 1 và # 2 và thời gian vẫn chính xác như nhau. Vì vậy, đây không phải là nguyên nhân. – interjay