2013-06-27 30 views
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%的情况下,只有一个听众。
+0

有些东西告诉我,选择循环遍历函数作为外部循环(即循环遍历函数,然后通过消息)可能会更有效率,但也许这只是错误的直觉,你甚至不应该听我说。 – 2013-06-27 17:59:01

+0

你能组合发送给同一个监听器的多条消息吗? – Mysticial

+0

第一种方法是M * N,但秒对我来说似乎是N.这可能吗? – StackedCrooked

回答

4

循环的顺序可能比监听器的签名中的变化要少得多(注意,无论哪个循环在外面,监听器都可以保持第一个界面,即两个循环都可以在调用者中)。

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

+0

在听众中合并消息写入操作是我没有想到的一个非常重要的考虑事项。感谢您的观察! – sbi

8

是否有这方面的设计更容易让每个时隙要处理更多数量的消息的任意基本考虑?

一般来说,主要考虑因素通常归结为两个主要的问题。

  1. 如果环中的一个被遍历其可以潜在地具有良好的存储器局部性的对象(如上循环值的阵列),保持在内部循环的那部分可以潜在地保持CPU的高速缓存中的对象并提高性能。

  2. 如果您计划尝试并行化操作,请在外循环中保留“较大”(按照计数)集合,这样可以有效地并行化外循环,而不会导致线程订阅等。在外层级上并行化算法通常更简单,更简洁,因此如果以后可能的话,设计具有潜在更大的并行“工作块”的外层环路可以简化此操作。

那些听众需要的地方写邮件,这是相当昂贵的,所以函数调用本身可能不是太重要的性能,明智的。

这可能会完全否定将一个环路移到另一个环路的好处。

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

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

+0

Re#1:“对象”主要是字符串加上一些元数据('__FILE__','__LINE__',时间戳等),但我目前的做法是把它们变成字符串(“格式化”)一次,在后台任务中,而不是单独在听众中。所以基本上“对象”是普通的字符缓冲区(通常在0.1-1kB之间)。你怎么看待char缓冲区的位置? Re#2:不会并行化要求我将'M'消息转发给'N'个并行工作任务 - 这正是我目前遇到的问题? – sbi

+0

@sbi如果他们是字符缓冲区,那么位置就不会太多。基本上,如果有什么东西或者很大,或者使用指针(甚至是内部的),那么图形位置就会出现在窗口之外。如果字符缓冲区大小不变,并存储在一个数组中,则可能存在局部性,但在这种情况下,监听器正在执行IO的事实可能意味着在这种情况下它确实无关紧要。 –

0

没有任何“根本”的原因,为什么一个是比另一个更好的设计。根据图书馆的使用情况,可能会出现一些速度上的微小差异。我个人更喜欢首先迭代侦听器,然后再遍历消息。

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

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

+0

无论我在哪里编写'messages'或'formatted_messages',那么这可能是一个'std :: vector'缓冲区。 (不过我没有使用'std :: string')。每个缓冲区都是1k,但通常消息使用<0.1k。如果通过“处理机构”来引用听众,那么,不,他们并没有真正快速地考虑要求。写入内部闪存很慢(〜100ms),因此我们使用NVRAM作为现金,并将另一个后台任务从那里复制到闪存中...... – sbi

1

所以,有几个因素将在这里发挥:

如何并拢在高速缓存中的信息,以及他们是如何占用的空间?如果它们相对较小(几千字节或更少)并且靠近在一起(例如,在系统中执行大量其他内存分配的情况下,不是链接列表与分配数秒的内存分配)。

如果它们靠得很近,那么我相信第二个选择会更有效率,因为消息将被预取/缓存在一起,在那里调用所有的n监听器和过滤器函数(假设有很多函数,不是一个,两个或三个)可能会导致更多的“缓存抛出”以前的消息。这也取决于听众和过滤器功能实际上有多复杂。他们做了多少工作?如果每个功能都做了相当多的工作,那么按顺序执行它可能就不那么重要了,因为它只是微不足道的。

+0

分支目标预测器在调用许多不同功能时似乎讨厌它来自同一条指令。但是,正如你所看到的,在良好的数据高速缓存使用和充分利用CPU之间进行权衡。 – tmyklebu

+0

消息在1k时被截断,但99.9%的消息最多只有0.1k。它们存储在动态分配的缓冲区中(每个1k),通常不会被释放,而是被回收。我可以编写一个分配器来分配这些缓冲区的大块来改善它们的局部性,但是由于在前端它们被并行线程消耗,无论如何它们最终都会被扰乱,所以我不认为这会导致多买我。正如我写的,通常(99%)是一个功能,而且更多的是两个极其罕见。 (当然,它可能是用户以不可预知的方式使用它。) – sbi