为啥一个循环比另一个循环检测共享内存更新需要更长的时间?
Posted
技术标签:
【中文标题】为啥一个循环比另一个循环检测共享内存更新需要更长的时间?【英文标题】:Why does one loop take longer to detect a shared memory update than another loop?为什么一个循环比另一个循环检测共享内存更新需要更长的时间? 【发布时间】:2010-03-26 15:26:20 【问题描述】:我编写了一个写入共享内存的“服务器”程序,以及一个从内存读取的客户端程序。服务器有不同的“通道”可以写入,它们只是不同的链接列表,它也可以附加项目。客户端对某些链表感兴趣,并希望读取添加到这些链表中的每个节点,并尽可能降低延迟。
我有两种方法供客户使用:
对于每个链表,客户端保留一个“书签”指针以保持其在链表中的位置。它循环循环链表,一遍又一遍地遍历所有链表(它永远循环),如果可以的话,每次将每个书签向前移动一个节点。是否可以由节点的“下一个”成员的值确定。如果它是非空的,那么跳转到下一个节点是安全的(服务器自动将它从空切换到非空)。这种方法工作正常,但如果有很多列表要迭代,而其中只有少数正在接收更新,那么延迟就会变差。
服务器为每个列表提供一个唯一的 ID。每次服务器将项目附加到列表时,它也会将列表的 ID 号附加到主“更新列表”。客户端只保留一个书签,一个书签到更新列表中。它无休止地检查书签的下一个指针是否为非空(while(node->next_ == NULL)
),如果是,则继续读取给定的 ID,然后处理链表上具有该 ID 的新节点。从理论上讲,这应该可以更好地处理大量列表,因为客户端不必每次都遍历所有列表。
当我对这两种方法的延迟进行基准测试时(使用 gettimeofday),令我惊讶的是 #2 非常糟糕。对于少量链表,第一种方法的延迟通常低于 20us。第二种方法会有一些低延迟,但通常在 4,000-7,000us 之间!
通过在这里和那里插入 gettimeofday,我确定方法 #2 中所有增加的延迟都花在循环中,反复检查下一个指针是否为非空。这让我很困惑;就好像一个流程中的更改需要更长的时间才能使用第二种方法“发布”到第二个流程。我假设正在进行某种我不明白的缓存交互。怎么回事?
更新:最初,方法#2 使用条件变量,因此如果node->next_ == NULL
它将等待该条件,并且服务器将在每次发布更新时通知该条件。延迟是相同的,并且试图弄清楚为什么我将代码减少到上述方法。我在多核机器上运行,所以一个进程自旋锁不应该影响另一个。
更新 2:node->next_ 是不稳定的。
【问题讨论】:
这里真的需要自旋锁吗?为什么不使用平台提供的事件对象之类的东西,以便客户端可以很好地等待(不占用 CPU、缓存或内存访问资源),直到服务器发出新数据的信号? 我先试了一个条件变量,见更新。 你在阅读node->next_
原子吗?
更新后 - 在#2 中,客户端和服务器似乎只通过一个共享的内存位(书签节点)进行通信,因此该资源可能存在更多争用。你能测算客户端读取node->next需要多长时间,服务器写入需要多长时间?
读取始终是瞬时的(0 或 1 微秒)。我会检查写...
【参考方案1】:
既然听起来读写是在不同的 CPU 上进行的,那么memory barrier 可能会有所帮助?您的写入可能不会在您期望的时候发生。
【讨论】:
内存屏障将控制写入的顺序,这会使它们花费更长的时间,但我认为它们不会让另一个 CPU 更快地出现写入。我已经尝试将 GCC 的 __sync_synchronize 在写入和读取方面都无效。【参考方案2】:您在#2 中执行Spin Lock,这通常不是一个好主意,并且正在消耗周期。
【讨论】:
如果预期的等待时间非常短并且经常发生锁定,则自旋锁的性能优于信号。 我在一台多核机器上运行,所以服务器和客户端不会互相窃取周期。【参考方案3】:您是否尝试在第二种方法中每次失败的轮询尝试后添加yield
?只是一个猜测,但它可能会减少电源循环。
使用Boost.Thread 会是这样的:
while(node->next_ == NULL)
boost::this_thread::yield( );
【讨论】:
我可以,但这有什么帮助?问题是延迟,而不是 CPU 使用率。我在多核机器上运行,所以服务器和客户端不会互相窃取周期。以上是关于为啥一个循环比另一个循环检测共享内存更新需要更长的时间?的主要内容,如果未能解决你的问题,请参考以下文章
为啥我使用 modin.pandas 比使用 Pandas 需要更长的时间 [ray]
为啥通过 ASP.NET 方法运行查询比原生 SQL 需要更长的时间?