2014-10-11 43 views
2

我目前正在为C++编写一个linalg库,用于教育目的和个人使用。作为它的一部分,我使用自定义行和列迭代器实现了一个自定义矩阵类。虽然提供了使用std :: algorithm和std :: numeric函数的非常好的功能,但我在索引和iterator/std :: inner_product方法之间执行了矩阵乘法的速度比较。结果显著不同:C++速度比较迭代器与索引

// used later on for the custom iterator 
template<class U> 
struct EveryNth { 
    bool operator()(const U&) { return m_count++ % N == 0; } 
    EveryNth(std::size_t i) : m_count(0), N(i) {} 
    EveryNth(const EveryNth& element) : m_count(0), N(element.N) {} 

private: 
    int m_count; 
    std::size_t N; 
}; 

template<class T, 
     std::size_t rowsize, 
     std::size_t colsize> 
class Matrix 
{ 

private: 

    // Data is stored in a MVector, a modified std::vector 
    MVector<T> matrix; 

    std::size_t row_dim;     
    std::size_t column_dim; 

public: 

    // other constructors, this one is for matrix in the computation 
    explicit Matrix(MVector<T>&& s): matrix(s), 
            row_dim(rowsize), 
            column_dim(colsize){ 
    }  

    // other code... 

    typedef boost::filter_iterator<EveryNth<T>, 
            typename std::vector<T>::iterator> FilterIter; 

    // returns an iterator that skips elements in a range 
    // if "to" is to be specified, then from has to be set to a value 
    // @ param "j" - j'th column to be requested 
    // @ param "from" - starts at the from'th element 
    // @ param "to" - goes from the from'th element to the "to'th" element 
    FilterIter begin_col(std::size_t j, 
          std::size_t from = 0, 
          std::size_t to = rowsize){ 
     return boost::make_filter_iterator<EveryNth<T> >(
      EveryNth<T>(cols()), 
      matrix.Begin() + index(from, j), 
      matrix.Begin() + index(to, j) 
      ); 
    } 

    // specifies then end of the iterator 
    // so that the iterator can not "jump" past the last element into undefines behaviour 
    FilterIter end_col(std::size_t j, 
         std::size_t to = rowsize){ 
     return boost::make_filter_iterator<EveryNth<T> >(
      EveryNth<T>(cols()), 
      matrix.Begin() + index(to, j), 
      matrix.Begin() + index(to, j) 
      ); 
    } 

    FilterIter begin_row(std::size_t i, 
          std::size_t from = 0, 
          std::size_t to = colsize){ 
     return boost::make_filter_iterator<EveryNth<T> >(
      EveryNth<T>(1), 
      matrix.Begin() + index(i, from), 
      matrix.Begin() + index(i, to) 
      ); 
    } 

    FilterIter end_row(std::size_t i, 
         std::size_t to = colsize){ 
     return boost::make_filter_iterator<EveryNth<T> >(
      EveryNth<T>(1), 
      matrix.Begin() + index(i, to), 
      matrix.Begin() + index(i, to) 
      ); 
    } 

    // other code... 

    // allows to access an element of the matrix by index expressed 
    // in terms of rows and columns 
    // @ param "r" - r'th row of the matrix 
    // @ param "c" - c'th column of the matrix 
    std::size_t index(std::size_t r, std::size_t c) const { 
     return r*cols()+c; 
    } 

    // brackets operator 
    // return an elements stored in the matrix 
    // @ param "r" - r'th row in the matrix 
    // @ param "c" - c'th column in the matrix 
    T& operator()(std::size_t r, std::size_t c) { 
     assert(r < rows() && c < matrix.size()/rows()); 
     return matrix[index(r,c)]; 
    } 

    const T& operator()(std::size_t r, std::size_t c) const { 
     assert(r < rows() && c < matrix.size()/rows()); 
     return matrix[index(r,c)]; 
    } 

    // other code... 

    // end of class 
}; 

现在在运行的主要功能如下:

int main(int argc, char *argv[]){ 


    Matrix<int, 100, 100> a = Matrix<int, 100, 100>(range<int>(10000)); 


    std::clock_t begin = clock(); 
    double b = 0; 
    for(std::size_t i = 0; i < a.rows(); i++){ 
     for (std::size_t j = 0; j < a.cols(); j++) { 
       std::inner_product(a.begin_row(i), a.end_row(i), 
            a.begin_column(j),0);   
     } 
    } 

    // double b = 0; 
    // for(std::size_t i = 0; i < a.rows(); i++){ 
    //  for (std::size_t j = 0; j < a.cols(); j++) { 
    //   for (std::size_t k = 0; k < a.rows(); k++) { 
    //    b += a(i,k)*a(k,j); 
    //   } 
    //  } 
    // } 


    std::clock_t end = clock(); 
    double elapsed_secs = double(end - begin)/CLOCKS_PER_SEC; 
    std::cout << elapsed_secs << std::endl; 

    std::cout << "--- End of test ---" << std::endl; 

    std::cout << std::endl; 
    return 0; 
} 

对于性病:: inner_product /迭代器,它需要的方法:

bash-3.2$ ./main 

3.78358 
--- End of test --- 

和索引(// out)方法:

bash-3.2$ ./main 

0.106173 
--- End of test --- 

这比迭代器方法快了近40倍。你看到代码中的任何东西都会减慢迭代器计算的速度吗?我应该提及的是,我尝试了两种方法,并产生了正确的结果。

谢谢你的想法。

+2

它可能属于代码审查......你也应该补充你的编译器,以及所有编译器选项。 – quantdev 2014-10-11 12:17:21

+0

看起来你并没有使用你的迭代器迭代,而是每次都创建新的迭代器。或者我读错了吗? – Galik 2014-10-11 12:21:25

+0

@Galik:更新了帖子。复制时发生错误。 – Vincent 2014-10-11 12:31:55

回答

2

你必须明白的是,矩阵操作是很好理解的,编译器非常擅长对矩阵操作中涉及的事物进行优化。考虑C = AB,其中C是M×N,A是M×Q,B是Q×N。

double a[M][Q], b[Q][N], c[M][N]; 
for(unsigned i = 0; i < M; i++){ 
    for (unsigned j = 0; j < N; j++) { 
    double temp = 0.0; 
    for (unsigned k = 0; k < Q; k++) { 
     temp += a[i][k]*b[k][j]; 
    } 
    c[i][j] = temp; 
    } 
} 

(你不会相信如何诱惑我写上面FORTRAN IV)

编译器着眼于这一点,注意到什么是真正发生的事情是,他通过和步行c步长为1,b步长为Q.他消除了下标计算中的乘法,并进行直接索引。

在这一点上,内环的形式为:

temp += a[r1] * b[r2]; 
r1 += 1; 
r2 += Q; 

而且你周围的循环(重新)初始化r1和r2每次穿过。

这是您可以做一个简单的矩阵乘法的绝对最小计算。你做不到这一点,因为你必须做这些乘法和增加以及索引调整。

你所能做的就是增加开销。

这就是iterator和std :: inner_product()方法的作用:它增加了公制的开销。

+0

嗯,我预期该指数的方法要快一些,只是没有那么多。所以我开始担心。 – Vincent 2014-10-11 21:11:44

+0

记住,在C/C++ for循环的语义需要编译器发射的代码,重新评估循环的终止条件,其全部,在每次穿过环。编译器不知道循环体没有重构矩阵,所以必须检查。如果您知道矩阵的大小是固定的乘法的时间(这该死的好应该的!),你可以而且应该在初始化缓存,比方说,a.rows()和a.cols()的值部分的形式。 – 2014-10-14 21:09:47

+0

你能提供一个简短的例子吗? – Vincent 2014-10-15 06:46:31

2

这只是对低级代码优化的一些额外信息和一般建议。


要最终找出时间是在低级别的代码花(紧密循环和热点),

  1. 您必须能够实现代码的多个版本计算相同的结果,采用不同的实施策略。
    • 您需要广泛的数学和计算知识才能做到这一点。
  2. 您必须检查拆装(机器码)。
  3. 您还必须在指令级采样分析器下运行您的代码,以查看机器代码的哪一部分执行得最为严重(即热点)。
    • 为了收集足够数量的样本分析器的,你需要在一个紧凑的循环运行的代码,在数百万或数十亿倍。
  4. 您必须比较不同版本的代码(来自不同的实施策略)之间的热点反汇编。
  5. 根据以上信息,您可以得出结论:某些实施策略效率较低(更浪费或多余)。
    • 如果你到达这一步,你现在可以发布并与他人分享你的发现。

一些可能性:

  1. 使用boost::filter_iterator为执行该跳过每N元素是一种浪费的迭代器。内部实现必须一次递增1。如果N很大,则通过boost::filter_iterator访问下一个元素将变为O(N)操作,而不是简单的迭代器算术,该操作将是O(1)操作。
  2. 您的boost::filter_iterator实现使用模运算符。尽管现代CPU上的整数除法和模运算速度很快,但它仍然不如简单整数算法那么快。

简单地说,

  • 递增,递减,加法和减法是最快的,对于整数和浮点运算。
  • 乘法和位移稍慢。
  • 分部和模数操作会下注较慢。
  • 最后,浮点三角函数和超越函数,尤其是那些需要调用标准数学库函数的函数,将是最慢的。
+0

非常感谢您的评论。我仍然处于开发的早期阶段,所以实际上我很满意迭代器是按照原样工作的。不过,我相信这个测试显示还有很多工作还在进行中。到时候我会发布结果和代码审查。 – Vincent 2014-10-12 11:01:56