如何实现高效的C++运行时统计

11
我想知道是否有一种良好的方法来监视我的应用程序内部,最好是现有库的形式。
我的应用程序是高度多线程的,并使用消息系统在线程之间和外部世界之间进行通信。我的目标是监视发送的消息类型、频率等情况。
还可以在更普遍的方式下提供其他统计数据,例如每分钟生成多少个线程,调用了多少次new/delete,或应用程序的更专业方面;你来命名吧。
真棒的是像Google Chrome中“内部页面”那样的东西,比如net或chrome://tracing,但以命令行方式展示。
如果存在一个足够通用以适应我的应用程序特定性的库,那就太好了。 否则,我准备实现一个小类来完成工作,但我不知道从哪里开始。我认为最重要的是代码不应过多干涉,以免影响性能。
你们对这个问题有什么建议吗?
编辑:我的应用程序在Linux上运行,在嵌入式环境中运行,遗憾的是Valgrind不支持该环境 :(

gprof是否被支持?gcc编译器上的-pg选项呢? - pyCthon
是的,这是我们拥有的一个功能。虽然我的问题是针对一个长时间运行的程序(服务),因此统计信息应该在运行时可访问 :-) - Gui13
1
很遗憾,你不能再添加一个标签“嵌入式”。 - Thomas Matthews
嵌入式添加 :) 很好的观点。 - Gui13
7个回答

3
我了解您正在尝试实现运行时统计信息的收集,例如您发送了多少字节,运行时间有多长以及用户激活特定功能的次数等等。
通常,为了从各种来源(如工作线程)编译此类运行时统计信息,我会让每个来源(线程)递增其自己的本地计数器,以获得最基本的数据,但不对该数据进行任何冗长的数学或分析处理。
然后,在主线程(或您想要分析和显示这些统计信息的任何地方),我会向每个工作线程发送一个“RequestProgress”类型的消息。作为响应,工作线程将汇总所有基本数据并执行一些简单的分析。这些数据以及基本分析结果将在“ProgressReport”消息中发送回请求(主)线程。然后,主线程聚合所有这些数据,并进行额外的(可能是昂贵的)分析、格式化和向用户或日志记录的显示。
主线程通过用户请求(例如当他们按下“S”键时)或定时间隔发送此“RequestProgress”消息。如果我要使用定时间隔,则通常会实现另一个新的“心跳”线程。这个线程所做的就是“Sleep()”一段指定的时间,然后向主线程发送一个“Heartbeat”消息。主线程接着根据这个“Heartbeat”消息向要收集统计信息的每个工作线程发送“RequestProgress”消息。
收集统计信息似乎应该相当简单。那么为什么需要这样一个复杂的机制呢?答案有两个方面。
首先,工作线程有自己的工作要做,计算使用统计信息不是其中之一。试图重构这些线程以承担第二个与其主要目的不相关的责任,有点像试图将方形钉子塞进圆孔中。它们没有被建立来做这个,所以代码会抵制编写。
其次,如果您尝试做太多或太频繁地计算运行时统计信息,这将是昂贵的。例如,假设您有一个工作线程在网络上发送多播数据,并且您想要收集吞吐量数据。有多少字节,多长时间,平均每秒多少字节。您可以让工作线程自己计算所有这些内容,但这是很多工作,而且该CPU时间更好地用于工作线程正在执行的任务——发送多播数据。如果您只是每次发送消息时递增了发送的字节数的计数器,则计数对线程的性能影响很小。然后,在偶尔的“RequestProgress”消息的响应中,您可以找出开始和停止时间,并将其发送给主线程进行所有的除法等操作。

3

我建议在你的代码中,保留计数器并进行递增。计数器可以是static类成员或全局变量。如果你使用一个类来定义计数器,你可以让构造函数将计数器和名称注册到一个仓库中。然后,你可以通过查询仓库来查询和重置计数器。

struct Counter {
    unsigned long c_;
    unsigned long operator++ () { return ++c_; }
    operator unsigned long () const { return c_; }
    void reset () { unsigned long c = c_; ATOMIC_DECREMENT(c_, c); }
    Counter (std::string name);
};

struct CounterAtomic : public Counter {
    unsigned long operator++ () { return ATOMIC_INCREMENT(c_, 1); }
    CounterAtomic (std::string name) : Counter(name) {}
};

ATOMIC_INCREMENT是一个平台特定的机制,用于原子性地增加计数器。GCC提供了内置的__sync_add_and_fetch来实现这个目的。而ATOMIC_DECREMENT则类似,使用GCC内置的__sync_sub_and_fetch实现。

struct CounterRepository {
    typedef std::map<std::string, Counter *> MapType;
    mutable Mutex lock_;
    MapType map_;
    void add (std::string n, Counter &c) {
        ScopedLock<Mutex> sl(lock_);
        if (map_.find(n) != map_.end()) throw n;
        map_[n] = &c;
    }
    Counter & get (std::string n) const {
        ScopedLock<Mutex> sl(lock_);
        MapType::const_iterator i = map_.find(n);
        if (i == map_.end()) throw n;
        return *(i->second);
    }
};

CounterRepository counterRepository;

Counter::Counter (std::string name) {
    counterRepository.add(name, *this);
}

如果您知道同一个计数器将被多个线程递增,那么请使用CounterAtomic。对于特定于线程的计数器,只需使用Counter即可。

在我看来,这是一个不错的开始,比valgrind建议更好,因为它会违反“..以便性能不受影响..”的要求。但这并没有解决多线程方面的问题,增加/重置计数器不一定是原子操作...我会考虑将其中一些保留在线程专用变量中... - nhed
@nhed:感谢您提醒我关于OP的MT要求。我已经使用原子操作更新了答案。 - jxh
这是一个很棒的答案。我会尝试那个东西,看看效果如何。还要感谢__sync_*_and_fetch,我不知道这个,之前一直在使用互斥锁! - Gui13
@Gui13:谢谢。注意我之前在“reset”中有一个错误,我刚刚修复了它。 - jxh

1

使用共享内存(POSIX、System V、mmap或任何可用的方式)。通过将原始内存块转换为数组定义,将固定长度的易失性无符号32位或64位整数数组放入其中。请注意,易失性并不能让您获得原子性;它可以防止编译器优化破坏您的统计值。使用像gcc的__sync_add_and_fetch()或更新的C++11 atomic<>类型这样的内部函数。

然后,您可以编写一个小程序,连接到相同的共享内存块,并可以打印一个或所有统计信息。此小型统计信息读取程序和主程序必须共享一个强制执行每个统计信息在数组中位置的公共头文件。

显而易见的缺点是您被限制在了固定数量的计数器上。但从性能方面来看,很难超越它。影响是在程序的各个点上对整数进行原子递增。


1
在嵌入式系统中,一种常见的技术是为“日志”保留一块内存,并将其视为循环队列。编写一些代码来读取这个内存块,可以在运行时帮助进行“快照”。
在网上搜索“调试日志记录”。应该会找到一些源代码,可以用来测试。我去过的大多数商店通常都自己开发。
如果您有额外的非易失性内存,可以保留一个区域并写入其中。如果您的系统足够大以支持文件系统,则还包括文件。
最坏的情况是将数据写出到调试(串行)端口。
对于实际的实时测量,我们通常使用连接到GPIO或测试点的示波器,并向GPIO /测试点输出脉冲。

0

如果您使用的是C++11,您可以使用std::atomic<>

#include <atomic>

class GlobalStatistics {
public:

    static GlobalStatistics &get() {
        static GlobalStatistics instance;
        return instance;
    }

    void incrTotalBytesProcessed(unsigned int incrBy) {
        totalBytesProcessed += incrBy;
    }

    long long getTotalBytesProcessed() const { return totalBytesProcessed; }


private:

    std::atomic_llong totalBytesProcessed;

    GlobalStatistics() { }
    GlobalStatistics(const GlobalStatistics &) = delete;
    void operator=(const GlobalStatistics &) = delete;
};

0

这是一个很好的答案,@John Dibling!我有一个系统与此非常相似。然而,我的“stat”线程每秒查询工作线程10次,这影响了工作线程的性能,因为每当“stat”线程请求数据时,都会有一个关键部分访问这些数据(计数器等),这意味着工作线程在检索数据时被阻塞。结果证明,在工作线程负载较重的情况下,这种10Hz的统计查询会影响工作线程的整体性能。

因此,我转向了稍微不同的统计报告模型 - 不再从主线程主动查询工作线程,而是让工作线程将其基本统计计数器报告给其专用的统计库,主线程可以随时查询该库,而不会直接影响工作线程。


0

看一下valgrind/callgrind。

它可以用于分析,这是我理解你正在寻找的。我不认为它可以在运行时工作,但它可以在进程完成后生成。


@Gui13:你是在寻找编译时的统计数据,例如valgrind提供的吗?还是你需要运行时的统计数据,例如用户点击弹出式鸭子的次数? - John Dibling
遗憾的是,我的应用程序运行在一个嵌入式平台上,而该平台不受valgrind支持。 - Gui13

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接