选择任一循环作为外循环是否有优势?

时间:2013-06-27 17:53:24

标签: c++ performance loops

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

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

鉴于此,我最终有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 个答案:

答案 0 :(得分:9)

  

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

一般而言,这方面的主要考虑因素通常归结为两个主要方面。

  1. 如果你的一个循环在可能具有良好内存位置的对象上循环(例如循环遍历值数组),那么将该部分保留在内部循环中可能会将对象保留在CPU缓存中,并提高绩效。

  2. 如果您打算尝试并行化操作,在外部循环中保持“更大”(在计数方面)集合允许您有效地并行化外部循环,而不会导致线程的过度订阅等在外层并行化算法通常更简单,更简洁,因此在外循环中设计具有可能更大的并行“块”工作的循环可以简化这一点,如果以后可能的话。

  3.   

    那些监听器需要在某处写消息,这是相当昂贵的,因此函数调用本身可能在性能上不太重要。

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

      

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

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

答案 1 :(得分:5)

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

第二个接口的自然优势(即向每个侦听器发送一系列消息)是您可以对侦听器的实现进行可能的分组。例如,如果写入设备,则侦听器可以将多个消息打包到单个write中,而如果接口接收单个消息,则侦听器缓存(具有内存和CPU成本)或需要每次通话执行多次writes

答案 2 :(得分:2)

因此,有几个因素会在这里发挥作用:

缓存中的邮件距离多近,以及它们占用了多少空间?如果它们相对较小(几千字节或更少)并且靠近在一起(例如,在一个执行大量其他内存分配的系统中,没有一个链接列表,内存分配几秒钟)。

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

答案 3 :(得分:0)

没有任何“基本”原因,为什么一个是比另一个更好的设计。根据库的使用方式,可能会出现一些非常小的速度差异。我个人更喜欢首先迭代侦听器而第二次迭代消息。

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

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