2012-01-30 27 views
7

Tôi đang cố gắng tối ưu hóa thuật toán chuyên sâu tính toán và có một số vấn đề về bộ nhớ cache. Tôi có một bộ đệm rất lớn được viết đôi khi và một cách ngẫu nhiên và chỉ đọc một lần ở phần cuối của ứng dụng. Rõ ràng, việc ghi vào bộ đệm tạo ra rất nhiều bộ nhớ đệm và bên cạnh đó gây ô nhiễm các bộ nhớ đệm mà sau đó cần một lần nữa để tính toán. Tôi đã cố gắng sử dụng các công cụ di chuyển không theo thời gian, nhưng bộ nhớ cache bị thiếu (được báo cáo bằng valgrind và được hỗ trợ bởi các phép đo thời gian chạy) vẫn xảy ra. Tuy nhiên, để tiếp tục điều tra các động thái phi thời gian, tôi đã viết một chương trình thử nghiệm nhỏ mà bạn có thể xem bên dưới. Truy cập tuần tự, bộ đệm lớn, chỉ ghi.Tại sao _mm_stream_ps tạo bộ đệm ẩn L1/LL?

#include <stdio.h> 
#include <stdlib.h> 
#include <time.h> 
#include <smmintrin.h> 

void tim(const char *name, void (*func)()) { 
    struct timespec t1, t2; 
    clock_gettime(CLOCK_REALTIME, &t1); 
    func(); 
    clock_gettime(CLOCK_REALTIME, &t2); 
    printf("%s : %f s.\n", name, (t2.tv_sec - t1.tv_sec) + (float) (t2.tv_nsec - t1.tv_nsec)/1000000000); 
} 

const int CACHE_LINE = 64; 
const int FACTOR = 1024; 
float *arr; 
int length; 

void func1() { 
    for(int i = 0; i < length; i++) { 
     arr[i] = 5.0f; 
    } 
} 

void func2() { 
    for(int i = 0; i < length; i += 4) { 
     arr[i] = 5.0f; 
     arr[i+1] = 5.0f; 
     arr[i+2] = 5.0f; 
     arr[i+3] = 5.0f; 
    } 
} 

void func3() { 
    __m128 buf = _mm_setr_ps(5.0f, 5.0f, 5.0f, 5.0f); 
    for(int i = 0; i < length; i += 4) { 
     _mm_stream_ps(&arr[i], buf); 
    } 
} 

void func4() { 
    __m128 buf = _mm_setr_ps(5.0f, 5.0f, 5.0f, 5.0f); 
    for(int i = 0; i < length; i += 16) { 
     _mm_stream_ps(&arr[i], buf); 
     _mm_stream_ps(&arr[4], buf); 
     _mm_stream_ps(&arr[8], buf); 
     _mm_stream_ps(&arr[12], buf); 
    } 
} 

int main() { 
    length = CACHE_LINE * FACTOR * FACTOR; 

    arr = malloc(length * sizeof(float)); 
    tim("func1", func1); 
    free(arr); 

    arr = malloc(length * sizeof(float)); 
    tim("func2", func2); 
    free(arr); 

    arr = malloc(length * sizeof(float)); 
    tim("func3", func3); 
    free(arr); 

    arr = malloc(length * sizeof(float)); 
    tim("func4", func4); 
    free(arr); 

    return 0; 
} 

Chức năng 1 là cách tiếp cận ngây thơ, chức năng 2 sử dụng tính năng bỏ vòng lặp. Chức năng 3 sử dụng movntps, mà trong thực tế đã được lắp vào lắp ráp ít nhất là khi tôi kiểm tra cho -O0. Trong hàm 4, tôi đã cố gắng phát hành một số lệnh movntps cùng một lúc để giúp CPU ghi kết hợp của nó. Tôi đã biên dịch mã với gcc -g -lrt -std=gnu99 -OX -msse4.1 test.c trong đó X là một trong số [0,3]. Kết quả là .. thú vị để nói lúc tốt nhất:

-O0 
func1 : 0.407794 s. 
func2 : 0.320891 s. 
func3 : 0.161100 s. 
func4 : 0.401755 s. 
-O1 
func1 : 0.194339 s. 
func2 : 0.182536 s. 
func3 : 0.101712 s. 
func4 : 0.383367 s. 
-O2 
func1 : 0.108488 s. 
func2 : 0.088826 s. 
func3 : 0.101377 s. 
func4 : 0.384106 s. 
-O3 
func1 : 0.078406 s. 
func2 : 0.084927 s. 
func3 : 0.102301 s. 
func4 : 0.383366 s. 

Như bạn thấy _mm_stream_ps là một chút nhanh hơn so với những người khác khi chương trình không được tối ưu hóa bằng gcc nhưng sau đó không đáng kể mục đích của nó khi tối ưu hóa gcc được bật . Valgrind vẫn báo cáo rất nhiều ghi nhớ cache.

Vì vậy, câu hỏi đặt ra là: Tại sao bộ nhớ cache (L1 + LL) nhớ vẫn xảy ra ngay cả khi tôi đang sử dụng hướng dẫn phát trực tuyến NTA? Tại sao đặc biệt func4 quá chậm ?! Ai đó có thể giải thích/suy đoán những gì đang xảy ra ở đây?

+2

Nếu bạn đang biên dịch với tối ưu hóa được bật, bạn sẽ cần phải nhìn vào hội đồng để thực sự biết những gì đang xảy ra. – RussS

+0

Tôi đang xem hội đồng, mà btw ngày càng khó đọc hơn với mỗi mức tối ưu hóa, nhưng nó không cho tôi biết tại sao gợi ý phi thời gian bị bỏ qua. Ít nhất tôi đoán nó được bỏ qua như valgrind vẫn báo cáo bộ nhớ cache nhớ nơi tôi mong đợi không. Dù sao, tôi biết câu hỏi là khá không đặc biệt, vì vậy tôi sẽ thực sự đánh giá cao bất kỳ đầu vào về những gì có thể xảy ra ở đây. –

Trả lời

8
  1. Có lẽ, các biện pháp benchmark của bạn chủ yếu là thực hiện phân bổ bộ nhớ, không chỉ ghi hiệu suất. Hệ điều hành của bạn có thể cấp phát các trang bộ nhớ không có trong malloc, nhưng trên lần chạm đầu tiên, bên trong các hàm func* của bạn. Hệ điều hành cũng có thể thực hiện một số bộ nhớ sau khi bộ nhớ lớn được cấp phát, vì vậy mọi điểm chuẩn, được thực hiện ngay sau khi cấp phát bộ nhớ, có thể không đáng tin cậy.
  2. Mã của bạn có aliasing vấn đề: trình biên dịch không thể đảm bảo rằng con trỏ mảng của bạn không thay đổi trong quá trình điền mảng này, vì vậy nó luôn phải tải giá trị arr từ bộ nhớ thay vì sử dụng thanh ghi. Điều này có thể làm giảm hiệu suất. Cách dễ nhất để tránh răng cưa là sao chép arrlength vào biến cục bộ và chỉ sử dụng các biến cục bộ để lấp đầy mảng. Có nhiều lời khuyên nổi tiếng để tránh các biến toàn cầu. Bí danh là một trong những lý do.
  3. _mm_stream_ps hoạt động tốt hơn nếu mảng được căn chỉnh 64 byte. Trong mã của bạn không có sự liên kết được đảm bảo (thực tế, malloc căn chỉnh nó theo 16 byte). Việc tối ưu hóa này chỉ đáng chú ý đối với các mảng ngắn.
  4. Bạn nên gọi _mm_mfence sau khi hoàn tất _mm_stream_ps. Điều này là cần thiết cho tính chính xác, không phải cho hiệu suất.
+1

Cảm ơn rất nhiều Evgeny! 1. Đây là nó. Tôi không biết điều đó. Khi tôi thay đổi mã để cấp phát bộ nhớ chỉ một lần, nó đã thay đổi đáng kể thời gian hoạt động thành những gì tôi mong đợi ban đầu. func3 + 4 nhanh hơn 2-3 lần so với func1 + 2. 2. Bạn có thể giải thích thêm về điều này một chút không? Tôi nghĩ rằng răng cưa sẽ chỉ là một vấn đề liên quan đến bộ nhớ ảo <-> bộ nhớ vật lý. Tôi không thấy đây là vấn đề ở đây. 3. Ok, vì vậy tôi sẽ phải sử dụng valloc() hoặc một số chức năng cụ thể khác của libc? Không có bất kỳ tác động nào đối với thời gian hoạt động. Liên kết dòng bộ nhớ cache sẽ giúp CPU kết hợp ghi, tôi có đúng không? 4. Ok. –

+1

Tôi đã thêm một số giải thích về răng cưa cũng như liên kết wikipedia. Liên kết dòng bộ nhớ cache cho phép sử dụng chính xác việc ghi kết hợp cho 64 byte đầu tiên của mảng. Đối với căn chỉnh, bạn có thể sử dụng một số chức năng phụ thuộc vào nền tảng, tôi không nhớ tất cả chúng ngay bây giờ. Hoặc bạn có thể sử dụng thủ thuật '(p + 63) & ~ 63'. Hoặc chỉ cần bỏ qua căn chỉnh nếu các mảng của bạn luôn lớn hơn megabyte. –

+1

Về vấn đề bí danh, bạn nên cố gắng vượt qua 'arr' và 'length' làm đối số cho các hàm của bạn, thay vì có chúng dưới dạng globals. Điều này * có thể * cải thiện các cơ hội tối ưu hóa cho trình biên dịch. – rotoglup

2

nên không func4 là thế này:

void func4() { 
    __m128 buf = _mm_setr_ps(5.0f, 5.0f, 5.0f, 5.0f); 
    for(int i = 0; i < length; i += 16) { 
     _mm_stream_ps(&arr[i], buf); 
     _mm_stream_ps(&arr[i+4], buf); 
     _mm_stream_ps(&arr[i+8], buf); 
     _mm_stream_ps(&arr[i+12], buf); 
    } 
} 
+0

Bạn nói đúng.Cảm ơn :-) Điều này có func4 để về kết quả tương tự như func3. –

Các vấn đề liên quan