用于高效函数调用匹配的数据结构

时间:2012-05-28 22:04:28

标签: c++ algorithm data-structures stl-algorithm

我正在构建一个工具,除其他外必须衡量产品变更对性能的影响。

为了完成这项工作,我已经实现了一个探测器,无论何时调用函数或返回它都会跟踪并通知我。首先,我将输出转储到一个文件中,以了解我将要使用的数据,这里看起来像或多或少:

FuncCall1
   FuncCall2
      FuncCall3
      FuncRet3
      FuncCall4
      FuncRet4
      FuncCall5
        FuncCall6
        FuncRet6
      FuncRet5
    FuncRet2
FuncRet1

为了更好地直观地了解这些数据的外观,下面是前10000个函数调用的图表:(x轴:时间,y轴:深度/嵌套): Function Call/Return graph http://img444.imageshack.us/img444/4710/proflog.gifhttp://img444.imageshack.us/img444/4710/proflog.gif

当一个函数开始执行时,我将记录它的名称/标识符和当前的高精度时间戳,当它返回时,我需要查找存储开始时间的条目并添加一个标记它返回的新时间戳。

总而言之,我将对这些数据执行的操作是:

  1. 插入带有当前时间戳的新函数调用标记。
  2. 查找某个ID的最新函数调用并存储返回时间戳。
  3. 查看在某个函数中调用了哪些其他函数(并查看其花费时间的位置) - 例如,如果我在上一个示例中查看函数#2,我想知道它调用函数#3 ,功能#4,功能#5和功能#5调用功能#6然后返回(标记所有呼叫/返回时间戳)。
  4. 现在,我对数据结构有几个想法可能对这种情况有用:

    1. 自动平衡树(即AVL),其中每个节点的密钥将是功能标识符,每个节点中的值将是一堆时间戳对。这种方法在标记函数时间戳和每个节点都是堆栈这一事实时会给我快速插入和查找,它还会处理正确的返回时间戳与开始时间戳的匹配 - 总是(我假设)a的最新返回时间戳某些函数应该匹配最嵌套/最近的函数调用。 在这种方法中,使用不同的标识符维护嵌套函数调用会有点麻烦,因为我必须遍历树并根据它们的时间戳来匹配它们以找出它们的嵌套 - 不理想。

    2. 维护一个尚未返回的函数列表(将保留调用堆栈信息)并使用skip-list,其中每个级别将等于函数调用嵌套级别。这种方法会使操作#3更容易,但查找会更慢,我可能需要维护非常长的未返回函数列表 - 例如main(),这些函数必须在我的应用程序的整个生命周期内维护。在这里,我还可以使用哈希表来提高查找速度,从而牺牲更多的内存使用量。内存使用率至关重要 - 此分析器可轻松生成大约20 MB / s。

    3. 我之所以没有使用简单的堆栈来跟踪这些数据,是因为我需要定期将部分结果同步到不同的机器,并且在所有内容都返回之前至少有部分结果可用。

      我已经查看了区间树,范围​​树和其他类型的数据结构,但是我找不到任何能够有效满足我所有3项要求的数据结构。

      也许有一个数据结构可以满足他们所有我不知道的东西?任何想法?

      更新

      这个怎么样:

      拥有一个具有函数调用的树以及它们的嵌套调用,以及一个单独的堆栈用于未返回的函数。

      现在堆栈中的每个元素都有一个指向它在树中的副本的指针,当一个新的函数调用到来时,我将查看堆栈中的顶部元素,跟踪它指向树中表示的指针,添加新函数作为该调用的子函数调用,并使用指向新创建的树节点的指针将其复制到堆栈上。

      对于函数返回,它类似,对于每个函数返回,堆栈上的最新条目将始终是它的调用 - 跟踪调用指针,在树中保存返回时间并弹出调用。

      你觉得我的想法有什么重大缺陷吗?

      更新2:

      我的方法非常有效。我将等待2天并回答我的问题。

3 个答案:

答案 0 :(得分:1)

从一个线程的角度来看,我认为最有效的是拥有一个严重的侵入式数据结构 - 你组合调用堆栈和AVL树,如下所示:

// one of these per call
struct {
    function *func; // func in the tree (or ID)
    timestamp time; // timestamp of call
    call *prev_call; // previous function call
    call *next_call; // next function call
} call;

// one of these per function
struct {
    call *last_call; // last call of this function
    your_type id; // identifier

    // insert tree-specifics here
} function;

我还没有完全解决这个问题,但我认为这是要走的路。

答案 1 :(得分:1)

您可以使用跟踪类。缺点:您必须在每个必须记录/确定的函数的开头声明此跟踪器的实例。它还为您的测量添加了大量的循环,因此只有大量函数才能用这种方法正确跟踪。

实现这一目标的另一种方法是编写一个真正的探查器但是这样的工具已经存在(gprof),并为它编写一个解析器。你仍然可以编写自己的......一项非常长的任务。

我建议您分别测试每个功能或组,在单元测试中,这是我们通常有效地进行测试的方式。然后弹出一个分析器并尝试优化你正在运行的10%代码90%的时间。你可以通过远距离观察它们来关注细节。

以下是我的工具之一:

  

Jul Jul 09 00:49:12 2010 - [3799946229640] - D:\ Code Project \ include / design / BTL / algorithm / dispatch_policy.h :: operator()#| ... operator()(){

     

Jul Jul 09 00:49:12 2010 - [3799946246830] - D:\ Code Project \ include / design / BTL / algorithm / dispatch_policy.h :: operator()#| ... shape *,shape * < / p>      

Jul Jul 09 00:49:12 2010 - [3799946265738] - D:\ Code Project \ include / design / BTL / algorithm / dispatch_policy.h :: operator()#| ...} operator():46027 CPU_CYCLES

如上所述,输出很大,使得深度分析不实用,由于20Mb / s的输出流,你不能长时间监视程序。只有当您已经知道在哪里进行调查时,它才有用。对于这种工具所需的大量带宽的另一个问题是,您必须使用缓冲的ostreams ...使该工具对真实软件更具侵入性。我已经经历了x10减速!

答案 2 :(得分:-1)

树的想法似乎......浪费。

你在做什么需要一个简单的堆栈。

  • 记录ID /条目时间戳对
  • 执行子呼叫
  • 使用退出时间戳修改堆栈顶部条目

此外,所有儿童电话实际上都是同一父母的孩子......

在你的位置,我只会使用:

  • 用于自动退出时间戳记录的RAII
  • 类似deque的快速记录追加结构

记录结构如下:

struct Record {
    unsigned level;
    unsigned id;
    unsigned entry;
    unsigned exit;
};

然后,你保留两个线程局部结构:

extern thread_local unsigned CurrentLevel;
extern thread_local std::deque<Record> CallRecords;

最后,您实现了一个简单的RAII类:

class CallRecorder: boost::noncopyable() {
public:
    CallRecord(unsigned id):
        record(*CallRecords.insert(CallRecords.end(),
                                   Record{CurrentLevel++, id, time(), 0}))
    {
    }

    ~CallRecord() { record.exit = time(); --CurrentLevel; }

private:
    Record& record;
};

您可能会将父级的ID传递给每个子级调用,但这似乎不值得。这是您在利用流时可以重建的信息(在一侧保留单独的堆栈)。

我只有两个音符:

  • 您可能希望创建自己的deque实现来分配更大的块(例如4K页面),并且最重要的是优化附加。
  • 使用RAII,我们处理常规回报和异常。由于程序终止,因此不会记录abort。可以检测到它留下不完整的痕迹。