2011-07-25 40 views
8

我写了一些无锁的代码,在大多数情况下可以与当地的 一起使用。自旋锁总是需要记忆障碍吗?旋转在记忆障碍上是否昂贵?

本地在读取内存时是否必然意味着我必须始终在旋转 之前插入内存屏障?

(为了验证这一点,我设法产生一个读/写器组合 这导致读取器从未看到 写入的值,在某些非常特殊的条件 - 专用CPU,过程连接到CPU, 优化器接通一路向上,在 环没有进行任何的工作 - 这样的箭头做点这个方向发展,但我不 完全确信通过存储纺 屏障的成本)

是什么。通过内存屏障旋转的代价,如果 没有什么要在缓存的存储区中刷新e缓冲区? 即所有的进程正在做(在C)是

while (1) { 
    __sync_synchronize(); 
    v = value; 
    if (v != 0) { 
     ... something ... 
    } 
} 

我是正确的假设,它是免费的,它不会与任何业务拖累 内存总线?

另一种方式把这个要问:没有一个内存屏障做 什么比更多:刷新存储缓冲区,应用 废票给它,并防止 重新排序的编译器读取在其位置/写?


拆卸,__sync_synchronize()似乎翻译成:

lock orl 

从英特尔手册(同样含糊不清的新手):

Volume 3A: System Programming Guide, Part 1 -- 8.1.2 

Bus Locking 

Intel 64 and IA-32 processors provide a LOCK# signal that 
is asserted automatically during certain critical memory 
operations to lock the system bus or equivalent link. 
While this output signal is asserted, requests from other 
processors or bus agents for control of the bus are 
blocked. 

[...] 

For the P6 and more recent processor families, if the 
memory area being accessed is cached internally in the 
processor, the LOCK# signal is generally not asserted; 
instead, locking is only applied to the processor’s caches 
(see Section 8.1.4, “Effects of a LOCK Operation on 
Internal Processor Caches”). 

我的翻译:“当你说LOCK,这将会很昂贵,但我们只在需要的地方使用 。“


@BlankXavier:

我做了测试,如果作家没有明确推出从存储缓冲区写入,它是CPU上运行的唯一的过程中,读者可以永远看作者的作用(我可以用测试程序重现它,但正如我上面提到的,它只发生在特定的测试中,具有特定的编译选项和专用的核心分配 - 我的算法工作正常,只有当我好奇时关于这是如何工作的,并写下了明确的测试,我意识到它可能会出现问题)。

我认为默认情况下,简单的写入是WB写入(回写),这意味着它们不会立即刷新,但读取将取最近的值(我认为他们称之为“存储转发”)。所以我为作者使用CAS指令。我在英特尔手册中发现了所有这些不同类型的写入实现(UC,WC,WT,WB,WP),英特尔第3A卷第11-10章,仍然在了解它们。

我的不确定性在读者身上:我从McKenney的论文中了解到,还有一个无效队列,即从总线进入缓存的无效队列。我不确定这部分是如何工作的。特别是,您似乎暗示循环读取正常(即,非LOCK'ed,没有屏障,并且仅使用volatile来确保优化器在编译之后保留读取)将每次都检查到“无效队列” (如果存在这样的事情)。如果一个简单的读取不够好(也就是说可以读取一个旧的缓存行,该行在排队失效之前仍然显示有效(这对我来说听起来有点不一致,但是失效队列如何工作呢?)),那么原子读取会是必要的,我的问题是:在这种情况下,这会对公共汽车产生什么影响吗? (我想可能不是)

我仍然在阅读我通过英特尔手册的方式,虽然我看到商店转发的一个很好的讨论,但我还没有找到关于失效队列的很好的讨论。我决定将我的C代码转换为ASM并进行实验,我认为这是真正理解它如何工作的最好方法。

+3

“在大多数情况下可以正常读取。” - 如果它总是无法正常工作,那么它就不好...... –

+0

关于全面优化的小循环测试,还有其他一些问题,例如: [Cyrix昏迷错误](http://en.wikipedia.org/wiki/Cyrix_coma_bug#Analysis)(尽管它不适用于这种情况),这可能会影响“假”测试。 –

+0

@Mitch:我的,当然,这就是为什么我问:-) – blais

回答

2

我可能无法正确理解了问题,但...

如果你打转,一个问题是编译器优化旋转远。挥发性解决了这个问题。

内存屏障,如果有的话,将由作者发给自旋锁,而不是读者。作者实际上并没有使用一个 - 这样做可以确保写入立即被推出,但它很快就会熄灭。

屏障阻止执行该代码的线程在其位置上重新排序,这是其它成本。

+0

在上面的修改中回答,所以SO不会让我添加长评论。 – blais

4

“xchg reg,[mem]”指令将通过内核的LOCK引脚发出锁定意图。该信号通过其他内核并缓存到总线主控总线(PCI变种等),这将完成他们正在做的事情,并且最终LOCKA(确认)引脚将向CPU发信号通知CPU可以完成xchg。然后关闭LOCK信号。此序列可能需要很长时间(数百个CPU周期或更长时间)才能完成。之后,其他内核的相应缓存行将会失效,并且您将拥有一个已知状态,即已经在内核之间同步的状态。

xchg指令是实现原子锁所需要的。如果锁定本身成功,则可以访问已定义锁定以控制对其的访问的资源。这样的资源可能是一个内存区域,一个文件,一个设备,一个功能或你有什么。尽管如此,程序员总是可以编写代码,当它被锁定时使用这个资源,而不是当它没有时。通常,成功锁定后的代码序列应该尽可能短,以使其他代码尽可能少地阻碍获取对资源的访问。

请记住,如果锁定不成功,则需要通过发出新的xchg再次尝试。

“无锁”是一个吸引人的概念,但它需要消除共享资源。如果您的应用程序有两个或两个以上的内核,同时从共同的内存地址读取和写入,则“无锁定”不是一个选项。

0

请记住,障碍通常用于对内存访问集进行排序,因此您的代码很可能在其他地方也可能需要屏障。例如,它不会是罕见的屏障要求是这样的,而不是:

while (1) { 

    v = pShared->value; 
    __acquire_barrier() ; 

    if (v != 0) { 
     foo(pShared->something) ; 
    } 
} 

这个屏障将防止如果块加载和存储(即:pShared->something)从执行前value加载完成。一个典型的例子是,你有一些使用v != 0一家商店标志“制片人”,其他一些内存(pShared->something)在其他一些预期的状态,如:

pShared->something = 1 ; // was 0 
__release_barrier() ; 
pShared->value = 1 ; // was 0 

在这个典型的生产消费情况,你几乎总是需要成对的障碍,一个用于标记辅助存储器可见的存储器(以便在存储器之前没有看到存储器的效果)以及消费者的一个障碍(使得某些负载在值加载完成之前未启动)。

这些障碍也是平台特定的。例如,在powerpc上(使用xlC编译器),分别为消费者和生产者使用__isync()__lwsync()。需要什么屏障还可能取决于您用于存储和加载value的机制。如果您使用了导致英特尔LOCK(也许是隐含的)的原子内在属性,那么这将引入一个隐含的障碍,所以您可能不需要任何东西。此外,你可能还需要明智地使用volatile(或者最好使用一个原子实现,在下面这样做),以便让编译器做你想做的事情。