我写了一些无锁的代码,可以很好地在本地运行在大多数情况下,读取
读内存时的局部旋转是否必然意味着I必须总是在旋转之前插入内存屏障吗读吗?
(为了验证这一点,我设法生成了一个阅读器/写入器这种组合导致读者永远看不到写值,下一定很具体条件——专用CPU,附属于CPU的进程,优化器一路打开,没有其他工作在循环——所以箭头确实指向那个方向,但我不是完全确定通过内存旋转的成本障碍。)
通过内存屏障的开销是多少在缓存的存储缓冲区中没有要刷新的内容?也就是说,所有进程正在做的(在C中)是
while ( 1 ) {
__sync_synchronize();
v = value;
if ( v != 0 ) {
... something ...
}
}
我假设它是免费的,它不会妨碍内存总线有流量吗?
另一种表述方式是问:内存屏障做什么任何超过:冲洗存储缓冲区,应用的失效,并防止编译器从重新排序读/写在它的位置?
反汇编,__sync_synchronize()似乎转化为:
lock orl
来自Intel手册(对于新手来说同样模糊):
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的论文中了解到,还有一个无效队列,一个从总线进入缓存的传入无效队列。我不知道这部分怎么用。特别是,您似乎暗示,循环通过一个正常的读操作(即,非锁定的,没有障碍,使用volatile只是为了确保优化器在编译后离开读操作),每次都会检查到"无效队列"(如果存在这样的事情)。如果一个简单的读取是不够好的(即可以读取一个旧的缓存行,仍然是有效的等待队列无效(这听起来有点不连贯,但无效队列如何工作,然后?))),那么原子读取将是必要的,我的问题是:在这种情况下,这会对总线有任何影响吗?(我想可能不会)
我仍然在阅读Intel手册,虽然我看到了关于存储转发的大量讨论,但我还没有找到关于无效队列的良好讨论。我已经决定将我的C代码转换成ASM并进行实验,我认为这是真正感受到它是如何工作的最好方法。
"xchg reg,[mem]"指令将通过核心的lock引脚表示其锁定意图。这个信号编织它的方式通过其他核心和缓存总线控制总线(PCI变体等),将完成他们正在做的事情,最终LOCKA(承认)引脚将信号CPU, xchg可能完成。然后锁信号被关闭。这个序列可能需要很长时间(数百个CPU周期或更多)才能完成。之后,其他核心的适当缓存线将失效,您将拥有一个已知的状态,即在核心之间同步的状态。
xchg指令是实现原子锁所需要的全部。如果锁本身成功,则可以访问已定义锁以控制访问的资源。这样的资源可以是一个内存区域、一个文件、一个设备、一个函数等等。尽管如此,还是要由程序员来编写代码,以便在资源被锁定时使用它,而在资源未锁定时不使用它。通常,成功锁定后的代码序列应该尽可能短,以便尽可能少地阻碍其他代码获取对资源的访问。请记住,如果锁不成功,您需要通过发出一个新的xchg来再次尝试。
"无锁"是一个吸引人的概念,但它需要消除共享资源。如果您的应用程序有两个或多个内核同时读写一个公共内存地址,那么"无锁"不是一个选项。
我可能没有完全理解这个问题,但是……
如果你正在旋转,一个问题是编译器在优化你的旋转。Volatile解决了这个问题
内存屏障(如果有的话)将由写入器发出给自旋锁,而不是读取器。作者实际上没有可以使用——这样做可以确保写入内容被立即推送出去,但它很快就会被推送出去。
屏障防止线程执行代码在其位置上重新排序,这是它的另一个成本。
请记住,屏障通常用于对内存访问集进行排序,因此您的代码很可能在其他地方也需要屏障。例如,屏障需求看起来像这样是很常见的:
while ( 1 ) {
v = pShared->value;
__acquire_barrier() ;
if ( v != 0 ) {
foo( pShared->something ) ;
}
}
这个屏障将阻止if块(即:pShared->something
)中的加载和存储在value
加载完成之前执行。一个典型的例子是,您有一些"生产者",它使用v != 0
的存储来标记其他一些内存(pShared->something
)处于其他预期状态,如:
pShared->something = 1 ; // was 0
__release_barrier() ;
pShared->value = 1 ; // was 0
在这个典型的生产者消费者场景中,您几乎总是需要成对的屏障,一个用于标记辅助内存可见的存储(以便在某事存储之前看不到值存储的效果),另一个用于消费者(以便在值加载完成之前不会启动某事加载)。
这些障碍也是平台特定的。例如,在powerpc上(使用xlC编译器),您将分别为消费者和生产者使用__isync()
和__lwsync()
。需要什么屏障还可能取决于您用于value
的存储和加载的机制。如果您使用了导致intel LOCK
(可能是隐式的)的原子固有特性,那么这将引入隐式屏障,因此您可能不需要任何东西。此外,您可能还需要明智地使用volatile(或者最好使用隐藏的原子实现),以便让编译器执行您想要的操作。