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

Posted

技术标签:

【中文标题】c ++速度比较迭代器与索引【英文标题】:c++ speed comparison iterator vs index 【发布时间】:2014-10-11 12:14:23 【问题描述】:

我目前正在用 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
;

现在在 main 函数中运行以下命令:

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/iterator 方法,它需要:

bash-3.2$ ./main

3.78358
--- End of test ---

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

bash-3.2$ ./main

0.106173
--- End of test ---

这比迭代器方法快了近 40 倍。您是否在代码中看到任何可能减慢迭代器计算的内容?我应该提到我尝试了这两种方法并且它们产生了正确的结果。

感谢您的想法。

【问题讨论】:

它可能属于codereview ...您还应该添加您的编译器,以及所有编译器选项。 看起来您并没有使用迭代器进行迭代,而是每次都在创建新的迭代器。还是我看错了? 开启优化。 @Vincent 请在开启优化后发布您的结果。 试试-O2-O3 【参考方案1】:

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

考虑 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() 方法的作用:它增加了公吨的开销。

【讨论】:

好吧,我预计索引方法会更快,只是没那么快。所以我开始担心了。 请记住,C/C++ for 循环的语义要求编译器在每次通过循环时发出重新评估循环终止条件的代码。编译器不知道循环体没有重塑矩阵,所以它必须检查。如果您知道矩阵大小在乘法期间是固定的(而且它应该是固定的!),您可以并且应该在初始化程序中缓存 a.rows() 和 a.cols() 的值for-stmt 的一部分。 你能提供一个简短的例子吗? for(std::size_t i = 0, std::size_t i_limit = a.rows(); i 【参考方案2】:

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


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

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

一些可能性:

    使用boost::filter_iterator 实现跳过每N 个元素的迭代器是一种浪费。内部实现必须一次加一。如果 N 很大,通过boost::filter_iterator 访问下一个元素将成为O(N) 操作,而不是简单的迭代器算法,即O(1) 操作。 您的boost::filter_iterator 实现使用模运算符。尽管在现代 CPU 上整数除法和模运算速度很快,但仍不如简单的整数运算快。

简单来说,

对于整数和浮点数而言,递增、递减、加法和减法是最快的。 乘法和位移稍慢。 除法和模运算会更慢。 最后,浮点三角函数和超越函数,尤其是那些需要调用标准数学库函数的函数,将是最慢的。

【讨论】:

非常感谢您的评论。我仍处于开发的早期过程中,所以事实上我很满意迭代器按原样工作。但是我相信这个测试表明还有相当多的工作要做。到时候我会发布结果和代码审查。

以上是关于c ++速度比较迭代器与索引的主要内容,如果未能解决你的问题,请参考以下文章

python-3_函数_匿名函数_正则_yield_迭代器_序列化_内置函数_模块

性能 - 使用迭代器或指针迭代向量?

HashMap随机取值和迭代器取值的对比

提高python迭代器的速度

在python中有啥比较高效方法可以转换list中的str类型为float?

opencv学习笔记访问图像中像素的三种方式ROI区域图像叠加和图像混合