2016-02-25 16 views
9

Tôi đang triển khai phép nhân C++ cho ma trận với các cấu trúc và kỹ thuật dữ liệu khác nhau (vectơ, mảng và OpenMP) và tôi tìm thấy một tình huống lạ ... phiên bản mảng đang làm việc tốt hơn:Tại sao phép nhân C++ với mảng động làm việc tốt hơn std :: phiên bản vector

lần:

OpenMP mult_1: thời gian: 5,882000 s

mảng mult_2: thời gian: 1,478000 s

cờ biên soạn của tôi là:

/usr/bin/g ++ -fopenmp -pthread -std = C++ 1n-O3

phiên bản C++ vector

typedef std::vector<std::vector<float>> matrix_f; 
void mult_1 (const matrix_f & matrixOne, const matrix_f & matrixTwo, matrix_f & result) { 
    const int matrixSize = (int)result.size(); 
    #pragma omp parallel for simd 
    for (int rowResult = 0; rowResult < matrixSize; ++rowResult) { 
     for (int colResult = 0; colResult < matrixSize; ++colResult) { 
      for (int k = 0; k < matrixSize; ++k) { 
       result[rowResult][colResult] += matrixOne[rowResult][k] * matrixTwo[k][colResult]; 
      } 
     } 
    } 
} 

Phiên bản mảng động

void mult_2 (float * matrixOne, float * matrixTwo, float * result, int size) { 
    for (int row = 0; row < size; ++row) { 
     for (int col = 0; col < size; ++col) { 
      for (int k = 0; k < size; ++k) { 
       (*(result+(size*row)+col)) += (*(matrixOne+(size*row)+k)) * (*(matrixTwo+(size*k)+col)); 
      } 
     } 
    } 
} 
.210

kiểm tra:

C++ phiên bản vector

utils::ChronoTimer timer; 
/* set Up simple matrix */ 
utils::matrix::matrix_f matr1 = std::vector<std::vector<float>>(size,std::vector<float>(size)); 
fillRandomMatrix(matr1); 

utils::matrix::matrix_f matr2 = std::vector<std::vector<float>>(size,std::vector<float>(size)); 
fillRandomMatrix(matr2); 

utils::matrix::matrix_f result = std::vector<std::vector<float>>(size,std::vector<float>(size));  
timer.init(); 
utils::matrix::mult_1(matr1,matr2,result); 
std::printf("openmp mult_1: time: %f ms\n",timer.now()/1000); 

mảng động phiên bản

utils::ChronoTimer timer; 

float *p_matr1 = new float[size*size]; 
float *p_matr2 = new float[size*size]; 
float *p_result = new float[size*size]; 

fillRandomMatrixArray(p_matr1,size); 
fillRandomMatrixArray(p_matr2,size); 

timer.init(); 
utils::matrix::mult_2(p_matr1,p_matr2,p_result,size); 
std::printf("array mult_2: time: %f ms\n",timer.now()/1000); 

delete [] p_matr1; 
delete [] p_matr2; 
delete [] p_result; 

Tôi đã kiểm tra một số bài viết trước đây, nhưng tôi không thể tìm thấy bất kỳ liên quan với tôi vấn đề link, link2, link3:

UPDATE: tôi refactorized thử nghiệm với các câu trả lời, và vector làm việc slighty tốt hơn:

vector mult: Thời gian: 1,194000 s

mảng mult_2: Thời gian: 1,202000 s

Phiên bản vector C++

void mult (const std::vector<float> & matrixOne, const std::vector<float> & matrixTwo, std::vector<float> & result, int size) { 
    for (int row = 0; row < size; ++row) { 
     for (int col = 0; col < size; ++col) { 
      for (int k = 0; k <size; ++k) { 
       result[(size*row)+col] += matrixOne[(size*row)+k] * matrixTwo[(size*k)+col]; 
      } 
     } 
    } 
} 

động mảng phiên bản

void mult_2 (float * matrixOne, float * matrixTwo, float * result, int size) { 
    for (int row = 0; row < size; ++row) { 
     for (int col = 0; col < size; ++col) { 
      for (int k = 0; k < size; ++k) { 
       (*(result+(size*row)+col)) += (*(matrixOne+(size*row)+k)) * (*(matrixTwo+(size*k)+col)); 
      } 
     } 
    } 
} 

Ngoài ra, phiên bản vectorized của tôi là làm việc tốt hơn (0,803 s);

+7

Dữ liệu được sắp xếp khác nhau trong bộ nhớ. Các ma trận của bạn là tiếp giáp trong bộ nhớ trong khi thực hiện 'vector ' phân bổ từng vector một cách riêng biệt. Nếu kích thước được cố định tại thời gian biên dịch, bạn có thể thử 'vector >' hoặc làm điều gì đó khác để đảm bảo rằng ma trận hoàn chỉnh nằm liền kề trong bộ nhớ. – PeterT

+0

Xem http://stackoverflow.com/questions/17259877/1d-or-2d-array-whats-faster về lý do tại sao bạn thường muốn tránh các cấu trúc 2d "thực" (như 'T **', 'vector > '...) để lưu trữ các ma trận dày đặc. – Pixelchemist

+0

Tôi sẽ đoán bố trí bộ nhớ không phải là vấn đề duy nhất của bạn. Hiển thị cho chúng tôi mã bộ đếm thời gian và số lượng chủ đề bạn đang chạy phiên bản openmp. – jepio

Trả lời

12

Một vec tơ vectơ tương tự với một mảng răng cưa - một mảng trong đó mỗi mục nhập là một con trỏ, mỗi con trỏ trỏ vào một mảng nổi khác.

Để so sánh, phiên bản mảng thô là một khối bộ nhớ, nơi bạn làm toán để tìm các phần tử.

Sử dụng một véc tơ đơn, không phải là vec tơ vectơ và thực hiện toán học theo cách thủ công. Hoặc sử dụng vectơ có kích thước cố định std::array s. Hoặc viết một loại trợ giúp có véc tơ (một chiều) của phao, và cho bạn một cái nhìn 2 chiều về nó.

Dữ liệu trong bộ đệm liền kề là bộ nhớ cache và tối ưu hóa thân thiện hơn so với dữ liệu trong một nhóm bộ đệm bị ngắt kết nối.

template<std::size_t Dim, class T> 
struct multi_dim_array_view_helper { 
    std::size_t const* dims; 
    T* t; 
    std::size_t stride() const { 
    return 
     multi_dim_array_view_helper<Dim-1, T>{dims+1, nullptr}.stride() 
     * *dims; 
    } 
    multi_dim_array_view_helper<Dim-1, T> operator[](std::size_t i)const{ 
    return {dims+1, t+i*stride()}; 
    } 
}; 
template<class T> 
struct multi_dim_array_view_helper<1, T> { 
    std::size_t stride() const{ return 1; } 
    T* t; 
    T& operator[](std::size_t i)const{ 
    return t[i]; 
    } 
    multi_dim_array_view_helper(std::size_t const*, T* p):t(p) {} 
}; 
template<std::size_t Dims> 
using dims_t = std::array<std::size_t, Dims-1>; 
template<std::size_t Dims, class T> 
struct multi_dim_array_view_storage 
{ 
    dims_t<Dims> storage; 
}; 
template<std::size_t Dims, class T> 
struct multi_dim_array_view: 
    multi_dim_array_view_storage<Dims, T>, 
    multi_dim_array_view_helper<Dims, T> 
{ 
    multi_dim_array_view(dims_t<Dims> d, T* t): 
    multi_dim_array_view_storage<Dims, T>{std::move(d)}, 
    multi_dim_array_view_helper<Dims, T>{ 
     this->storage.data(), t 
    } 
    {} 
}; 

bây giờ bạn có thể làm điều này:

std::vector<float> blah = { 
    01.f, 02.f, 03.f, 
    11.f, 12.f, 13.f, 
    21.f, 22.f, 23.f, 
}; 

multi_dim_array_view<2, float> view = { {3}, blah.data() }; 
for (std::size_t i = 0; i < 3; ++i) 
{ 
    std::cout << "["; 
    for (std::size_t j = 0; j < 3; ++j) 
    std::cout << view[i][j] << ","; 
    std::cout << "]\n"; 
} 

live example

Không có dữ liệu được sao chép trong lớp xem. Nó chỉ cung cấp một cái nhìn của mảng phẳng là một mảng đa chiều.

2

cách tiếp cận của bạn là hoàn toàn khác nhau:

  • Trong phiên bản "mảng động" bạn phân bổ một đoạn duy nhất của bộ nhớ cho mỗi ma trận và lập bản đồ các hàng của ma trận lên rằng một loạt bộ nhớ chiều.

  • Trong phiên bản "vectơ", bạn sử dụng vec-tơ vectơ "thực" và "động" hai chiều có nghĩa là vị trí lưu trữ của mỗi hàng ma trận không liên quan đến các hàng khác.

gì có thể bạn muốn làm là:

  • Sử dụng vector<float>(size*size) và thực hiện việc lập bản đồ rất tương tự như bạn đang làm trong "mảng động" dụ bằng tay hoặc

  • Viết một lớp xử lý nội bộ bản đồ cho bạn và cung cấp giao diện truy cập 2 chiều (T& operator()(size_t, size_t) hoặc một số loại row_proxy operator[](size_t) trong đó row_proxy lần lượt có T& operator[](size_t))

1

Đây chỉ là để thực thi lý thuyết (trong thực tế) về bộ nhớ tiếp giáp.

Sau khi thực hiện một số phân tích về các mã được tạo với g ++ (-O2) các nguồn có thể được tìm thấy tại địa chỉ: https://gist.github.com/42be237af8e3e2b1ca03

Mã liên quan được tạo ra cho các phiên bản mảng là:

.L3: 
    lea r9, [r13+0+rbx]    ; <-------- KEEPS THE ADDRESS 
    lea r11, [r12+rbx] 
    xor edx, edx 
.L7: 
    lea r8, [rsi+rdx] 
    movss xmm1, DWORD PTR [r9] 
    xor eax, eax 
.L6: 
    movss xmm0, DWORD PTR [r11+rax*4] 
    add rax, 1 
    mulss xmm0, DWORD PTR [r8] 
    add r8, r10 
    cmp ecx, eax 
    addss xmm1, xmm0 
    movss DWORD PTR [r9], xmm1  ; <------------ ADDRESS IS USED 
    jg .L6 
    add rdx, 4 
    add r9, 4      ; <--- ADDRESS INCREMENTED WITH SIZE OF FLOAT 
    cmp rdx, rdi 
    jne .L7 
    add ebp, 1 
    add rbx, r10 
    cmp ebp, ecx 
    jne .L3 

xem làm thế nào sử dụng giá trị của r9 phản ánh bộ nhớ tiếp giáp cho mảng đích và r8 cho một trong các mảng đầu vào.

Ở đầu bên kia, các vector của vector tạo mã như:

.L12: 
    mov r9, QWORD PTR [r12+r11] 
    mov rdi, QWORD PTR [rbx+r11] 
    xor ecx, ecx 
.L16: 
    movss xmm1, DWORD PTR [rdi+rcx] 
    mov rdx, r10 
    xor eax, eax 
    jmp .L15 
.L13: 
    movaps xmm1, xmm0 
.L15: 
    mov rsi, QWORD PTR [rdx] 
    movss xmm0, DWORD PTR [r9+rax] 
    add rax, 4 
    add rdx, 24 
    cmp r8, rax 
    mulss xmm0, DWORD PTR [rsi+rcx] 
    addss xmm0, xmm1 
    movss DWORD PTR [rdi+rcx], xmm0 ; <------------ HERE 
    jne .L13 
    add rcx, 4 
    cmp rcx, r8 
    jne .L16 
    add r11, 24 
    cmp r11, rbp 
    jne .L12 

Không ngạc nhiên, trình biên dịch là thông minh, đủ để không để tạo ra mã cho tất cả các operator [] cuộc gọi, và làm một công việc tốt của nội tuyến chúng, nhưng xem nó cần theo dõi các địa chỉ khác nhau như thế nào qua rdi + rcx khi nó lưu trữ giá trị trở lại vectơ kết quả, và truy cập bộ nhớ bổ sung cho các vector phụ khác nhau (mov rsi, QWORD PTR [rdx]) mà tất cả đều tạo ra một số chi phí.

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