Tại sao chương trình của tôi chậm khi lặp lại chính xác 8192 phần tử?


755

Đây là trích từ chương trình trong câu hỏi. Ma trận img[][]có kích thước SIZE × SIZE và được khởi tạo tại:

img[j][i] = 2 * j + i

Sau đó, bạn tạo một ma trận res[][]và mỗi trường ở đây được tạo thành trung bình của 9 trường xung quanh nó trong ma trận img. Đường viền được để lại ở mức 0 cho đơn giản.

for(i=1;i<SIZE-1;i++) 
    for(j=1;j<SIZE-1;j++) {
        res[j][i]=0;
        for(k=-1;k<2;k++) 
            for(l=-1;l<2;l++) 
                res[j][i] += img[j+l][i+k];
        res[j][i] /= 9;
}

Đó là tất cả các chương trình. Để hoàn thiện hơn, đây là những gì đến trước. Không có mã đến sau. Như bạn có thể thấy, đó chỉ là khởi tạo.

#define SIZE 8192
float img[SIZE][SIZE]; // input image
float res[SIZE][SIZE]; //result of mean filter
int i,j,k,l;
for(i=0;i<SIZE;i++) 
    for(j=0;j<SIZE;j++) 
        img[j][i] = (2*j+i)%8196;

Về cơ bản, chương trình này chậm khi SIZE là bội số của 2048, ví dụ: thời gian thực hiện:

SIZE = 8191: 3.44 secs
SIZE = 8192: 7.20 secs
SIZE = 8193: 3.18 secs

Trình biên dịch là GCC. Từ những gì tôi biết, điều này là do quản lý bộ nhớ, nhưng tôi thực sự không biết quá nhiều về chủ đề đó, đó là lý do tại sao tôi hỏi ở đây.

Ngoài ra làm thế nào để khắc phục điều này sẽ tốt, nhưng nếu ai đó có thể giải thích những lần thực hiện này thì tôi đã đủ hạnh phúc.

Tôi đã biết về malloc / miễn phí, nhưng vấn đề không phải là dung lượng bộ nhớ được sử dụng, đó chỉ là thời gian thực hiện, vì vậy tôi không biết điều đó sẽ giúp ích như thế nào.


67
@bokan nó xảy ra khi kích thước là bội số của bước tiến quan trọng của bộ đệm.
Luchian Grigore

5
@Mysticial, nó không thành vấn đề, nó phơi bày cùng một vấn đề chính xác; mã có thể khác nhau, nhưng về cơ bản cả hai câu hỏi đều hỏi về cùng một thời điểm (và tiêu đề của chúng hoàn toàn giống nhau).
Griwes

33
Bạn không nên xử lý hình ảnh bằng cách sử dụng mảng 2 chiều nếu bạn muốn hiệu suất cao. Xem xét tất cả các pixel ở dạng thô và xử lý chúng như một mảng một chiều. Làm điều này mờ trong hai vượt qua. Đầu tiên thêm giá trị của các pixel xung quanh bằng tổng trượt 3 pixel: slideSum + = src [i + 1] -src [i-1]; mệnh [i] = slideSum;. Sau đó thực hiện tương tự theo chiều dọc và chia cùng một lúc: Dest [i] = (src [i-width] + src [i] + src [i + width]) / 9. www-personal.engin.umd.umich.edu/~jwvm/ece581/18_RopedF.pdf
bokan

8
Thực tế có hai điều đang diễn ra ở đây. Nó không chỉ siêu liên kết.
Bí ẩn

7
(Chỉ là một câu đố nhỏ trong câu trả lời của bạn. Đối với đoạn mã đầu tiên, sẽ rất tuyệt nếu tất cả các vòng lặp của bạn có dấu ngoặc nhọn.)
Trevor Boyd Smith

Câu trả lời:


954

Sự khác biệt được gây ra bởi cùng một vấn đề siêu liên kết từ các câu hỏi liên quan sau đây:

Nhưng đó chỉ là do có một vấn đề khác với mã.

Bắt đầu từ vòng lặp ban đầu:

for(i=1;i<SIZE-1;i++) 
    for(j=1;j<SIZE-1;j++) {
        res[j][i]=0;
        for(k=-1;k<2;k++) 
            for(l=-1;l<2;l++) 
                res[j][i] += img[j+l][i+k];
        res[j][i] /= 9;
}

Đầu tiên lưu ý rằng hai vòng lặp bên trong là tầm thường. Chúng có thể không được kiểm soát như sau:

for(i=1;i<SIZE-1;i++) {
    for(j=1;j<SIZE-1;j++) {
        res[j][i]=0;
        res[j][i] += img[j-1][i-1];
        res[j][i] += img[j  ][i-1];
        res[j][i] += img[j+1][i-1];
        res[j][i] += img[j-1][i  ];
        res[j][i] += img[j  ][i  ];
        res[j][i] += img[j+1][i  ];
        res[j][i] += img[j-1][i+1];
        res[j][i] += img[j  ][i+1];
        res[j][i] += img[j+1][i+1];
        res[j][i] /= 9;
    }
}

Vì vậy, điều đó để lại hai vòng lặp bên ngoài mà chúng ta quan tâm.

Bây giờ chúng ta có thể thấy vấn đề tương tự trong câu hỏi này: Tại sao thứ tự các vòng lặp ảnh hưởng đến hiệu suất khi lặp qua một mảng 2D?

Bạn đang lặp lại cột ma trận thay vì hàng khôn ngoan.


Để giải quyết vấn đề này, bạn nên trao đổi hai vòng.

for(j=1;j<SIZE-1;j++) {
    for(i=1;i<SIZE-1;i++) {
        res[j][i]=0;
        res[j][i] += img[j-1][i-1];
        res[j][i] += img[j  ][i-1];
        res[j][i] += img[j+1][i-1];
        res[j][i] += img[j-1][i  ];
        res[j][i] += img[j  ][i  ];
        res[j][i] += img[j+1][i  ];
        res[j][i] += img[j-1][i+1];
        res[j][i] += img[j  ][i+1];
        res[j][i] += img[j+1][i+1];
        res[j][i] /= 9;
    }
}

Điều này giúp loại bỏ hoàn toàn tất cả các truy cập không tuần tự để bạn không còn bị chậm lại ngẫu nhiên trên các quyền hạn lớn.


Lõi i7 920 @ 3,5 GHz

Mã gốc:

8191: 1.499 seconds
8192: 2.122 seconds
8193: 1.582 seconds

Các vòng ngoài được hoán đổi:

8191: 0.376 seconds
8192: 0.357 seconds
8193: 0.351 seconds

217
Tôi cũng sẽ lưu ý rằng việc bỏ các vòng lặp bên trong không ảnh hưởng đến hiệu suất. Trình biên dịch có thể tự động làm điều đó. Tôi đã không kiểm soát chúng với mục đích duy nhất là loại bỏ chúng để giúp phát hiện vấn đề với các vòng bên ngoài dễ dàng hơn.
Bí ẩn

29
Và bạn có thể tăng tốc mã này lên gấp 3 lần bằng cách lưu các khoản tiền dọc theo mỗi hàng. Nhưng điều đó và các tối ưu hóa khác nằm ngoài phạm vi của câu hỏi ban đầu.
Eric Postpischil

34
@ClickUpvote Đây thực sự là một vấn đề phần cứng (bộ nhớ đệm). Nó không có gì để làm với ngôn ngữ. Nếu bạn đã thử nó trong bất kỳ ngôn ngữ nào khác biên dịch hoặc JIT thành mã gốc, bạn có thể sẽ thấy các hiệu ứng tương tự.
Bí ẩn

19
@ClickUpvote: Bạn có vẻ khá sai lầm. "Vòng lặp thứ hai" đó chỉ là phép lạ điều khiển các vòng bên trong bằng tay. Đây là điều mà trình biên dịch của bạn gần như chắc chắn sẽ làm được, và Mystical chỉ làm điều đó để làm cho vấn đề với các vòng lặp bên ngoài rõ ràng hơn. Đó không phải là điều gì đó bạn nên tự làm.
Lily Ballard

154
Đây là một ví dụ hoàn hảo về một câu trả lời hay trên SO: Tham khảo các câu hỏi tương tự, giải thích từng bước cách bạn tiếp cận nó, giải thích vấn đề, giải thích cách FIX vấn đề, định dạng tuyệt vời và thậm chí là một ví dụ về mã đang chạy trên máy của bạn. Cảm ơn sự đóng góp của bạn.
MattSayar

57

Các thử nghiệm sau đây đã được thực hiện với trình biên dịch Visual C ++ vì nó được sử dụng bởi cài đặt Qt Creator mặc định (tôi đoán không có cờ tối ưu hóa). Khi sử dụng GCC, không có sự khác biệt lớn giữa phiên bản của Mystical và mã "được tối ưu hóa" của tôi. Vì vậy, kết luận là tối ưu hóa trình biên dịch chăm sóc tối ưu hóa vi mô tốt hơn con người (cuối cùng là tôi). Tôi để phần còn lại của câu trả lời của tôi để tham khảo.


Nó không hiệu quả để xử lý hình ảnh theo cách này. Tốt hơn là sử dụng mảng kích thước đơn. Xử lý tất cả các pixel được thực hiện trong một vòng lặp. Truy cập ngẫu nhiên vào các điểm có thể được thực hiện bằng cách sử dụng:

pointer + (x + y*width)*(sizeOfOnePixel)

Trong trường hợp cụ thể này, tốt hơn là tính toán và lưu trữ tổng của ba nhóm pixel theo chiều ngang vì chúng được sử dụng ba lần mỗi nhóm.

Tôi đã thực hiện một số thử nghiệm và tôi nghĩ rằng nó đáng để chia sẻ. Mỗi kết quả là trung bình của năm bài kiểm tra.

Mã gốc của người dùng1615209:

8193: 4392 ms
8192: 9570 ms

Phiên bản của Mystical:

8193: 2393 ms
8192: 2190 ms

Hai pass sử dụng mảng 1D: pass đầu tiên cho tổng số ngang, thứ hai cho tổng dọc và trung bình. Hai địa chỉ vượt qua với ba con trỏ và chỉ tăng như thế này:

imgPointer1 = &avg1[0][0];
imgPointer2 = &avg1[0][SIZE];
imgPointer3 = &avg1[0][SIZE+SIZE];

for(i=SIZE;i<totalSize-SIZE;i++){
    resPointer[i]=(*(imgPointer1++)+*(imgPointer2++)+*(imgPointer3++))/9;
}

8193: 938 ms
8192: 974 ms

Hai pass sử dụng mảng 1D và đánh địa chỉ như thế này:

for(i=SIZE;i<totalSize-SIZE;i++){
    resPointer[i]=(hsumPointer[i-SIZE]+hsumPointer[i]+hsumPointer[i+SIZE])/9;
}

8193: 932 ms
8192: 925 ms

Một lần vượt qua bộ nhớ đệm ngang chỉ một hàng phía trước để chúng ở trong bộ đệm:

// Horizontal sums for the first two lines
for(i=1;i<SIZE*2;i++){
    hsumPointer[i]=imgPointer[i-1]+imgPointer[i]+imgPointer[i+1];
}
// Rest of the computation
for(;i<totalSize;i++){
    // Compute horizontal sum for next line
    hsumPointer[i]=imgPointer[i-1]+imgPointer[i]+imgPointer[i+1];
    // Final result
    resPointer[i-SIZE]=(hsumPointer[i-SIZE-SIZE]+hsumPointer[i-SIZE]+hsumPointer[i])/9;
}

8193: 599 ms
8192: 652 ms

Phần kết luận:

  • Không có lợi ích của việc sử dụng một số con trỏ và chỉ tăng (tôi nghĩ rằng nó sẽ nhanh hơn)
  • Bộ nhớ đệm ngang tốt hơn so với tính toán chúng nhiều lần.
  • Hai lần vượt qua không nhanh hơn ba lần, chỉ hai lần.
  • Có thể đạt được tốc độ nhanh hơn 3,6 lần bằng cả một lần truyền và lưu vào bộ đệm kết quả trung gian

Tôi chắc chắn rằng nó có thể làm tốt hơn nhiều.

LƯU Ý Xin lưu ý rằng tôi đã viết câu trả lời này để nhắm mục tiêu các vấn đề hiệu suất chung thay vì vấn đề bộ đệm được giải thích trong câu trả lời xuất sắc của Mystical. Lúc đầu, nó chỉ là mã giả. Tôi đã được yêu cầu làm các bài kiểm tra trong các bình luận ... Đây là một phiên bản được tái cấu trúc hoàn toàn với các bài kiểm tra.


9
"Tôi nghĩ rằng nó nhanh hơn ít nhất 3 lần" .Modecare để sao lưu yêu cầu đó với một số số liệu hoặc trích dẫn?
Adam Rosenfield

8
@AdamRosenfield "Tôi nghĩ" = giả sử! = "Đó là" = yêu cầu. Tôi không có số liệu cho điều này và tôi muốn xem một bài kiểm tra. Nhưng tôi yêu cầu 7 gia số, 2 phụ, 2 thêm và một div cho mỗi pixel. Mỗi vòng lặp sử dụng var cục bộ ít hơn so với có đăng ký trong CPU. Cái còn lại yêu cầu 7 số gia, 6 số giảm, 1 div và từ 10 đến 20 mul để đánh địa chỉ tùy thuộc vào tối ưu hóa trình biên dịch. Ngoài ra, mỗi hướng dẫn trong vòng lặp yêu cầu kết quả của hướng dẫn trước, điều này loại bỏ các lợi ích của kiến ​​trúc siêu vô hướng của Pentium. Vì vậy, nó phải được nhanh hơn.
bokan

3
Câu trả lời cho câu hỏi ban đầu là tất cả về hiệu ứng bộ nhớ và bộ nhớ cache. Lý do mã của OP quá chậm là vì mẫu truy cập bộ nhớ của nó đi theo cột thay vì theo hàng, có vị trí tham chiếu bộ đệm rất kém. Nó đặc biệt tệ ở 8192 vì sau đó các hàng liên tiếp kết thúc bằng cách sử dụng cùng một dòng bộ đệm trong bộ đệm hoặc bộ đệm được ánh xạ trực tiếp với độ kết hợp thấp, do đó tỷ lệ bỏ lỡ bộ đệm thậm chí còn cao hơn. Việc thay đổi các vòng lặp cung cấp một hiệu suất rất lớn bằng cách tăng đáng kể cục bộ bộ đệm.
Adam Rosenfield

1
Làm tốt lắm, đó là một vài con số ấn tượng. Như bạn đã tìm thấy, tất cả là về hiệu suất bộ nhớ - sử dụng một vài con trỏ với số gia không mang lại lợi ích gì.
Adam Rosenfield

2
@AdamRosenfield Tôi đã khá lo lắng sáng nay vì tôi không thể sao chép các bài kiểm tra. Có vẻ như việc tăng hiệu suất chỉ với trình biên dịch Visual C ++. Sử dụng gcc, chỉ có một sự khác biệt nhỏ.
bokan
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.