9

我为计算量向量的算法编写了优化代码。在尝试将函数中计算的数据从函数中取出之前和之后,我已经对其进行了计时。我认为计算的具体性质或数量向量的性质是不相关的。下面是代码、时间安排和详细信息的概述。

所有代码都使用以下标志编译:

g++ -Wall -Wextra -Werror -std=c++11 -pedantic -O3

我有这样的课:

#ifndef C_H
#define C_H

#include <iostream>
#include <iterator>
#include <vector>
Class c {
    public:
        void doWork( int param1, int param2 ) const {
            std::array<unsigned long,40> counts = {{0}};
            // LOTS of branches and inexpensive operations:
            // additions, subtractions, incrementations, and dereferences
            for( /* loop 1 */ ) {
                // LOTS MORE branches and inexpensive operations
                counts[ /* data dependent */ ] += /* data dependent */;
                for( /* loop 2 */ ) {
                    // YET MORE branches inexpensive operations
                    counts[ /* data dependent */ ] += /* data dependent */;
                }
            }
            counts [ /* data dependent */ ] = /* data dependent */;
            /* exclude for profiling
            std::copy( &counts[0], &counts[40], std::ostream_iterator( std::cout, "," ) );
            std::cout << "\n";
            */
        }
    private:
        // there is private data here that is processed above
        // the results get added into the array/vector as they are computed
};

#endif

像这样的主要内容:

#include <iostream>
#include "c.h"
int main( int argc, char * argv ) {
    Class c( //set the private data of c by passing data in );
    int param1;
    int param2;
    while( std::cin >> param1 >> param2 ) {
        c.doWork( int param1, int param2 );
    }
}

以下是有关数据的一些相关细节:

  • 标准输入读取 2000 万对(从文件重定向)
  • 2000 万次调用 c.doWork
  • 通过 c.doWork 中的外循环进行了 6000 万次 TOTAL 迭代
  • 通过 c.doWork 中的内部循环进行了 1.8 亿次 TOTAL 迭代

所有这些都需要运行 5 分 48 秒。当然,我可以在类函数中打印数组,这就是我一直在做的事情,但是我将公开发布代码,并且一些用例可能包括想要做一些除了打印向量之外的事情。在这种情况下,我需要更改函数签名以实际将数据发送给用户。这就是问题出现的地方。我尝试过的事情:

  • 在 main 中创建一个向量并通过引用传递它:

    std::vector<unsigned long> counts( 40 );
    while( std::cin >> param1 >> param2 ) {
        c.doWork( param1, param2, counts );
        std::fill( counts.begin(), counts.end(), 0 );
    }
    

    这需要 7 分 30 秒。删除对 std::fill 的调用只会减少 15 秒,因此这并不能解释差异。

  • 在 doWork 函数中创建一个向量并返回它,利用移动语义。由于这需要为每个结果动态分配,我没想到这会很快。奇怪的是,它并没有慢很多。7分40秒。

  • 按值返回当前在 doWork 中的 std::array。自然,这必须在返回时复制数据,因为堆栈数组不支持移动语义。7分30秒

  • 通过引用传递一个 std::array 。

    while( std::cin >> param1 >> param2 ) {
        std::array<unsigned long,40> counts = {{0}};
        c.doWork( param1, param2, counts )
    }
    

    我希望这与原始版本大致相同。数据放在主函数的栈中,并通过引用传递给doWork,doWork填充它。7分20秒。这个真的让我很困扰。

我没有尝试将指针传递给 doWork,因为这应该等同于通过引用传递。

一种解决方案自然是有两个版本的函数:一个在本地打印,一个返回。障碍是我必须复制所有代码,因为这里的整个问题是我无法有效地从函数中获取结果。

所以我很迷惑。我知道这些解决方案中的任何一个都需要对 doWork 中的数组/向量的每次访问都进行额外的取消引用,但是与大量其他快速操作和更麻烦的数据相关分支相比,这些额外的取消引用非常微不足道。

我欢迎任何想法来解释这一点。我唯一的想法是编译器正在优化代码,因此在原始情况下省略了一些其他必要的计算组件,因为编译器意识到这是不必要的。但这似乎在几个方面是禁忌的:

  1. 对循环内的代码进行更改确实会更改时间。
  2. 最初的时间是 5 分 50 秒,而仅仅从文件中读取对需要 12 秒,所以我们做了很多工作
  3. 也许只有涉及计数的操作被优化掉,但这似乎是一种奇怪的选择性优化,因为如果这些被优化掉,编译器可能会意识到在 doWork 中支持计算也是不必要的。
  4. 如果涉及计数的操作正在被优化掉,为什么在其他情况下它们没有被优化。我实际上并没有在 main 中使用它们。

doWork 是否独立于 main 进行编译和优化,因此如果该函数有义务以任何形式返回数据,则无法确定是否会使用它?

我为了避免打印成本以强调各种方法的相对差异而没有打印的分析方法有缺陷吗?

我很感激你能散发出的任何光芒。

4

3 回答 3

0

有时可以应用非正统的解决方案,这可能就是其中之一。您是否考虑过使数组成为全局数组?

尽管如此,局部变量的一个关键好处是优化器可以找到对它的所有访问,仅使用来自函数的信息。这使得寄存器分配变得更加容易。

static函数内的变量几乎相同,但在您的情况下,该堆栈数组的地址会转义,再次击败优化器。

于 2013-04-29T00:04:35.880 回答
0

我要做的就是暂停它几次,看看它大部分时间都在做什么。查看您的代码,我怀疑最多时间进入 a) 最内层循环,尤其是索引计算,或 2) std::array.

如果大小counts总是40,我会做

  long counts[40];
  memset(counts, 0, sizeof(counts));

memset这在堆栈上分配,与您正在做的任何其他事情相比,这不需要时间,也不需要时间。

如果大小在运行时发生变化,那么我所做的是一些静态分配,如下所示:

void myRoutine(){
  /* this does not claim to be pretty. it claims to be efficient */
  static int nAlloc = 0;
  static long* counts = NULL;
  /* this initially allocates the array, and makes it bigger if necessary */
  if (nAlloc < /* size I need */){
    if (counts) delete counts;
    nAlloc = /* size I need */;
    counts = new long[nAlloc];
  }
  memset(counts, 0, sizeof(long)*nAlloc);
  /* do the rest of the stuff */
}

这样,counts总是足够大,关键是1)new尽可能少地做,2)保持索引counts尽可能简单。

但首先我会暂停一下,只是为了确定。修复它之后,我会再做一次,看看接下来我可以修复什么。

于 2013-04-28T22:13:20.513 回答
0

编译器优化是一个值得关注的地方,但您还需要关注另一个地方。您在代码中所做的更改可能会干扰缓存布局。如果每次分配给阵列的内存都在不同的内存部分,系统中缓存未命中的数量可能会增加,这反过来会降低性能。您可以查看 CPU 上的硬件性能计数器,以便更好地猜测它。

于 2013-04-28T22:14:21.187 回答