19

我正在扩展现有的日志库。这是一个有两个方面的系统:前端是任务将日志消息写入的地方,后端是应用程序可以插入侦听器的地方,侦听器将这些消息转发到不同的接收器。后端曾经是一个硬连线的监听器,我现在正在扩展它以提高灵活性。该代码将专门用于嵌入式设备,其中高性能(以每毫秒转发的字节数衡量)是非常重要的设计和实现目标。

出于性能原因,消息被缓冲,并且在后台任务中完成转发。该任务从队列中获取大量消息,将它们全部格式化,然后通过注册函数将它们传递给侦听器。这些侦听器将过滤消息,并且只会将那些通过过滤条件的消息写入其接收器。

鉴于此,我最终N需要通知函数(侦听器)来向其发送M消息,这是一个相当经典的N*M问题。现在我有两种可能性:我可以遍历消息,然后遍历通知函数,将消息传递给每个函数。

for(m in formatted_messages) 
  for(n in notification_functions)
    n(m);

void n(message)
{
    if( filter(message) )
      write(message);
}

或者我可以遍历所有通知函数,并一次将我拥有的所有消息传递给它们:

for(n in notification_functions)
    n(formatted_messages);

void n(messages)
{
  for(m in messages)
    if( filter(m) )
      write(m);
}

关于哪种设计更有可能允许每个时间片处理更多的消息,是否有任何基本考虑?(注意这个问题如何决定监听器的界面。这不是一个微优化问题,而是一个关于如何进行不影响性能的设计的问题。我只能在很久以后才能测量,然后重新设计监听器界面会很昂贵.)

我已经做了一些考虑:

  • 这些侦听器需要将消息写入某处,这相当昂贵,因此函数调用本身在性能方面可能不太重要。
  • 在 95% 的情况下,只有一个听众。
4

4 回答 4

9

关于哪种设计更有可能允许每个时间片处理更多的消息,是否有任何基本考虑?

通常,与此相关的主要考虑因素通常归结为两个主要方面。

  1. 如果您的循环中的一个循环遍历可能具有良好内存局部性的对象(例如循环遍历一组值),则将该部分保留在内部循环中可能会将对象保留在 CPU 缓存中,并提高性能。

  2. 如果您打算尝试并行化操作,则在外循环中保留“更大”(以计数计)集合可以让您有效地并行化外循环,并且不会导致线程的过度订阅等。它通常更简单、更干净在外部级别并行化算法,因此在外部循环设计具有潜在更大并行工作“块”的循环可以简化这一点,如果以后有可能的话。

这些侦听器需要将消息写入某处,这相当昂贵,因此函数调用本身在性能方面可能不太重要。

这可能会完全否定将一个循环移到另一个循环之外的任何好处。

在 95% 的情况下,只有一个听众。

如果是这种情况,我可能会将侦听器循环置于外部范围,除非您计划并行化此操作。鉴于这将在嵌入式设备上的后台线程中运行,并行化不太可能,因此将侦听器循环作为外部循环应该减少总指令数(它实际上变成了 M 个操作的循环,而不是 M 个循环单次操作)。

于 2013-06-27T17:58:45.333 回答
5

与监听器签名的变化相比,循环的顺序的优势可能要小得多(请注意,无论哪个循环在外面,监听器都可以维护第一个接口,即两个循环都可以在调用者中)。

第二个接口(即向每个侦听器发送一系列消息)的自然优势是您可以对侦听器的实现进行可能的分组。例如,如果写入设备,监听器可以将多条消息打包成一个write,而如果接口接收一条消息,那么监听器要么缓存(有内存和 cpu 成本),要么writes每次调用需要执行多个.

于 2013-06-27T18:17:49.183 回答
2

因此,这里有几个因素会起作用:

缓存中的消息有多接近,它们占用了多少空间?如果它们相对较小(几千字节或更少)并且靠得很近(例如,在执行大量其他内存分配的系统中,不是一个分配了几秒钟内存的链表)。

如果它们很接近而且很小,那么我相信第二个选项更有效,因为消息将一起预取/缓存,其中调用所有n侦听器和过滤器函数(还假设有很多函数,不是一个、两个或三)可能会导致更多的“缓存丢弃”以前的消息。当然,这也取决于侦听器和过滤器函数的实际复杂程度。他们做了多少工作?如果每个函数都做了很多工作,那么执行的顺序可能并不重要,因为它只是微不足道的。

于 2013-06-27T18:09:21.370 回答
0

没有任何“基本”原因可以说明一个设计比另一个更好。根据您的库的使用方式,可能会出现一些非常小的速度差异。我个人更喜欢先迭代听众,然后再迭代消息。

我猜处理程序的主体通常非常快。您可能希望将侦听器作为外部循环进行迭代,以便重复调用相同的代码。像间接呼叫预测这样的东西会以这种方式更好地工作。当然,您最终会更糟地使用数据缓存,但希望每个消息缓冲区足够小以轻松放入 L1。

为什么不让听众接受 aconst vector<message> &并让他们自己进行迭代呢?他们可以做任何有益的缓冲,最后只做一次昂贵的写入。

于 2013-06-27T18:08:17.033 回答