毫无疑问,我是个修补工。出于这个原因(除此之外几乎没有),我最近做了一个小实验来证实我的怀疑,即向struct
写入不是一个原子操作,这意味着试图实施某些约束的所谓“不可变”值类型可能在其目标上假设失败。
我用以下类型写了a blog post about this作为说明:
struct SolidStruct
{
public SolidStruct(int value)
{
X = Y = Z = value;
}
public readonly int X;
public readonly int Y;
public readonly int Z;
}
虽然上述类型的
X != Y
或Y != Z
永远不可能是真的,但事实上,如果一个值是“mid assignment”,同时又被一个单独的线程复制到另一个位置,则可能会发生这种情况。好吧,大不了。只是好奇而已。但后来我有了这样的预感:我的64位CPU应该能够以原子方式复制64位,对吧?那么,如果我摆脱了
Z
而只停留在X
和Y
上呢?这只有64位;应该可以一步覆盖它们。果然,成功了。(我知道你们中的一些人可能正在皱眉头,在想,是的,嗯。这有什么意思?幽默我。)当然,我不知道这是不是保证给我的制度。我对寄存器、缓存未命中等几乎一无所知(实际上我只是在重复我听到的术语,而不理解它们的含义);所以这对我来说是一个黑匣子。
接下来我又试了一次,就凭直觉,是一个包含32位、使用2个
short
字段的结构。这似乎也显示了“原子的可分配性”。但后来我尝试了一个24位结构,使用了3个字段:no go。突然,这个结构似乎又一次容易受到“中间任务”副本的影响。
低至16位,有2个
byte
字段:又是原子的!有人能解释一下这是为什么吗?我听说过“位打包”、“缓存线跨行”、“对齐”等等,但我还是不知道这意味着什么,也不知道它是否与此相关。但我觉得我看到了一个模式,却无法确切地说出它是什么;如果清晰明了,我将不胜感激。
最佳答案
您要寻找的模式是CPU的本机字大小。
从历史上看,x86系列本机使用16位值(在此之前是8位值)。出于这个原因,CPU可以用原子方式处理:它是一个设置这些值的单一指令。
随着时间的推移,本机元素大小增加到32位,之后增加到64位。在每种情况下,都添加了一个指令来处理这些特定的比特量。然而,为了向后兼容,旧指令仍然保留,所以您的64位处理器可以与所有以前的本地大小一起工作。
由于您的结构元素存储在毗连内存中(没有填充,即空空间),运行时可以利用这些知识只为这些大小的元素执行该单一指令。简单地说,这会产生你所看到的效果,因为CPU每次只能执行一条指令(虽然我不确定多核系统是否能保证真正的原子性)。
然而,本机元素大小从不为24位。因此,没有一条指令可以写入24位,因此需要多条指令,这样就失去了原子性。