Assembly 围栏如何雾化加载和修改存储操作?

Assembly 围栏如何雾化加载和修改存储操作?,assembly,x86,multiprocessing,Assembly,X86,Multiprocessing,我已经读过,在加载修改存储一个指令之后放置一个围栏指令,比如BTS,使您可以将第二个指令视为原子指令。但是根据Intel的文档,fence指令被描述为 (曼芬斯) 对从内存和内存加载的所有数据执行序列化操作 在MFENCE之前发出的存储到内存指令 指示这个序列化操作保证每个加载 并将MFENCE指令之前的指令存储在程序中 订单在任何加载或存储指令之前全局可见 遵循MFENCE指令 那么,这样的行为如何保证所提到的“原子性” 具体地说,如果我们有两个由不同处理器同时运行的以下代码,在这两种情况下,

我已经读过,在加载修改存储一个指令之后放置一个围栏指令,比如BTS,使您可以将第二个指令视为原子指令。但是根据Intel的文档,fence指令被描述为

(曼芬斯)

对从内存和内存加载的所有数据执行序列化操作 在MFENCE之前发出的存储到内存指令 指示这个序列化操作保证每个加载 并将MFENCE指令之前的指令存储在程序中 订单在任何加载或存储指令之前全局可见 遵循MFENCE指令

那么,这样的行为如何保证所提到的“原子性”

具体地说,如果我们有两个由不同处理器同时运行的以下代码,在这两种情况下,围栏如何防止将0读入CF

start memory assumption: [addr] contains the word 0

BTS WORD PTR [addr], 0
MFENCE

推进一些壁垒不足以授予原子性

对于单线程代码,它们并没有真正的好处,CPU应该知道如何对加载进行排序,并在内部存储,以便在内核串行运行时实现正确的执行(即使在现实中,大多数现代CPU都会按顺序运行)

围栏的好处可能来自以下情况:

thread1:                    |         thread 2:
    store [x],1             |             store [y],1
    load [y] -> r1          |             load [x] -> r2
这是内存一致性问题的一个经典示例—如果读取2个寄存器,程序员可能会预期的结果是1,1(两个存储首先发生,然后两个加载),或者1,0或0,1(如果其中一个线程先于另一个线程运行。您不希望看到的是0,0,因为至少有一个线程应该已经完成了写操作。但是,通过放松内存顺序,这可能是可能的-加载是在管道中提前完成的,而存储是非常晚的。因为地址中没有线程内别名(假设x!=y),CPU不会阻止这种情况

按如下所示添加fence可以保证,如果其中一个线程达到负载,那么前面的存储必须已被分派和观察。这意味着您仍然可以获得0,1和1,0(如果两个存储fence负载首先在一个线程中完成),当然还有1,1,但您不能再获得0,0

thread1:                    |         thread 2:
    store [x],1             |             store [y],1
    mfence                  |             mfence
    load [y] -> r1          |             load [x] -> r2
另见-

但是,您要求原子性-这更强大,让我们以您为例-

BTS WORD PTR [addr], 0
MFENCE
如果我们将其复制到两个线程中,基本上与以前一样,只是隔离墙会紧跟加载和存储(它们被分组到同一条指令中的事实不会改变所完成的基本操作)。如何阻止您先执行两个读取,在两个线程上读取0,然后执行存储(这将在缓存中涉及一些MESI状态的竞争,因为如果两个线程位于不同的内核上,则两个线程将争夺所有权),但最终将导致两个存储都写入该行。然后,您可以随心所欲地执行这些功能,这不会将您从已经破坏的原子性中拯救出来

保证原子性的是一个好的、老式的、像样的锁。即使这样读取,线程也不能同时共享行。这通常被认为是一个缓慢但必要的缺点,但一些现代CPU甚至可以在硬件中优化它们!参见-

编辑: 经过一点搜索,我相信导致这个问题的原因与c++11中如何定义原子关键字有关。这些链接-和,表明一些实现是通过在存储后推送mfences完成的。但是,我不认为这意味着任何常规的(非库)对一个原子变量执行的操作必然是原子的。无论如何,这个机制应该提供多个内存一致性模型,所以我们需要在这里更具体一些

编辑2:
似乎确实有一场大规模的“运动”(不知道如何称呼他们:)为了减少锁的必要性,这里有一个有趣的部分:。这主要是关于软件设计和能够区分真正潜在的数据竞争,但底线似乎总是需要一些锁。c++11的添加,同时使给定一致性模型的使用更为方便,并消除了实现特定于硬件的解决方案的程序员可能仍然会被迫使用旧的解决方案。请注意,C++11原子标准不能保证实现在每个平台上都是无锁的

推进一些限制不足以授予原子性

对于单线程代码,它们并没有真正的好处,CPU应该知道如何对加载进行排序,并在内部存储,以便在内核串行运行时实现正确的执行(即使在现实中,大多数现代CPU都会按顺序运行)

围栏的好处可能来自以下情况:

thread1:                    |         thread 2:
    store [x],1             |             store [y],1
    load [y] -> r1          |             load [x] -> r2
这是内存一致性问题的一个经典示例—如果读取2个寄存器,程序员可能会预期的结果是1,1(两个存储首先发生,然后两个加载),或者1,0或0,1(如果其中一个线程先于另一个线程运行。您不希望看到的是0,0,因为至少有一个线程应该已经完成了写操作。但是,通过放松内存顺序,这可能是可能的-加载是在管道中提前完成的,而存储是非常晚的。因为地址中没有线程内别名(假设x!=y),CPU不会阻止这种情况

按如下所示添加fence可以保证,如果其中一个线程达到负载,那么前面的存储必须已被分派和观察。这意味着您仍然可以获得0,1和1,0(如果两个存储fence负载首先在一个线程中完成),当然还有1,1,但您不能再获得0,0

thread1:                    |         thread 2:
    store [x],1             |             store [y],1
    mfence                  |             mfence
    load [y] -> r1          |             load [x] -> r2
另见-

但是,您要求原子性-这更强大,让我们以您为例-

BTS WORD PTR [addr], 0
MFENCE
如果我们将它复制到两个线程中,本质上与以前一样,只是隔离墙在加载和存储之后(它们被分组到同一条指令中的事实不会改变