c++速度比较迭代器与索引

c++ speed comparison iterator vs index

本文关键字:索引 迭代器 比较 速度 c++      更新时间:2023-10-16

我目前正在用c++编写一个linalg库,用于教育和个人用途。作为它的一部分,我实现了一个带有自定义行和列迭代器的自定义矩阵类。虽然提供了非常好的特性来处理std::algorithm和std::numeric函数,但我对索引和迭代器/std::inner_product方法之间的矩阵乘法进行了速度比较。结果差异显著:

// 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;
}

对于std::inner_product/i迭代器方法,它采用:

bash-3.2$ ./main
3.78358
--- End of test ---

对于索引(//out)方法:

bash-3.2$ ./main
0.106173
--- End of test ---

这几乎比迭代器方法快40倍。你在代码中看到了什么可以降低迭代器计算速度的东西吗?我应该指出,我尝试了这两种方法,它们产生了正确的结果。

谢谢你的想法。

您必须了解的是,矩阵运算非常容易理解,编译器非常善于对矩阵运算中涉及的内容进行优化。

考虑C=AB,其中C是MxN,A是MxQ,B是QxN。

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写上面的内容。)

编译器看到了这一点,注意到真正发生的是,他以1的步幅穿过a和c,以Q的步幅走过b。他消除了下标计算中的乘法运算,并进行了直接索引。

在这一点上,内部循环的形式是:

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

你有一个循环来(重新)初始化r1和r2的每一次通过

这是一个绝对最小的计算,你可以做一个简单的矩阵乘法。你不能做得比这少,因为你必须做乘法、加法和索引调整。

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

迭代器和std::inner_product()方法就是这样做的:它增加了公吨的开销。

这只是一些关于低级代码优化的附加信息和一般建议。


为了最终找出在低级别代码(紧循环和热点)中花费的时间,

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

一些可能性:

  1. 使用boost::filter_iterator来实现跳过每个N元素的迭代器是浪费的。内部实现必须一次增加一个。如果N很大,那么通过boost::filter_iterator访问下一个元素就变成了O(N)运算,而不是简单的迭代器运算,后者将是O(1)运算
  2. 您的boost::filter_iterator实现使用了模运算符。尽管整数除法和模运算在现代CPU上很快,但它仍然不如简单的整数运算快

简单地说,

  • 无论是整数还是浮点,递增、递减、加法和减法都是最快的
  • 乘法和位偏移稍微慢一些
  • 除法和模运算会更慢
  • 最后,浮点三角函数和超越函数,尤其是那些需要调用标准数学库函数的函数,将是最慢的