与在优化和定时C++代码中从函数中获取数据相关的无法解释的费用

rg6*_*rg6 9 c++ optimization performance function-calls pass-by-reference

我已经为计算数量向量的算法编写了优化代码.我已经在各种尝试之前和之后定时将函数中的数据计算出来.我认为计算的具体性质或数量向量的性质是不相关的.下面是代码,时间和细节的概述.

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

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
Run Code Online (Sandbox Code Playgroud)

像这样的主要:

#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 );
    }
}
Run Code Online (Sandbox Code Playgroud)

以下是有关数据的一些相关详细信息:

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

所有这些都需要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 );
    }
    
    Run Code Online (Sandbox Code Playgroud)

    这需要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 )
    }
    
    Run Code Online (Sandbox Code Playgroud)

    我希望这大致相当于原版.数据放在main函数的堆栈中,并通过引用传递给doWork,doWork填充它.7分20秒.这个真的让我很难受.

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

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

所以我很神秘.据我所知,这些解决方案中的任何一个都需要对doWork中的数组/向量的每次访问进行额外的解引用,但与大量其他快速操作和更麻烦的数据相关分支相比,这些额外的解引用非常简单.

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

  1. 更改循环内的代码确实会改变计时.
  2. 原始定时5分50秒,而只是从文件读出对需要12秒,因此大量正在做.
  3. 也许只有涉及计数的操作才会被优化掉,但这似乎是一种奇怪的选择性优化,因为如果这些优化得到优化,编译器就会意识到在doWork中支持计算也是不必要的.
  4. 如果涉及计数的操作被优化掉,为什么它们在其他情况下没有被优化.我实际上并没有在主要使用它们.

是否就是doWork独立于main编译和优化的情况,因此如果函数有义务以任何形式返回数据,则无法确定它是否将被使用?

我的剖析方法是不打印,这是为了避免印刷成本强调各种方法的相对差异,有缺陷吗?

我很感激你能摆脱的任何光芒.

Mik*_*vey 0

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

如果 的大小counts始终是 40,我就会这么做

  long counts[40];
  memset(counts, 0, sizeof(counts));
Run Code Online (Sandbox Code Playgroud)

这在堆栈上进行分配,这不需要时间,并且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 */
}
Run Code Online (Sandbox Code Playgroud)

这样,counts总是足够大,重点是 1) 执行new尽可能少的次数,2) 保持索引counts尽可能简单。

但为了确定起见,我首先会暂停一下。修复之后,我会再次执行此操作,看看接下来可以修复什么。