为什么写入24位结构不是原子的(当写入32位结构时)?

时间:2011-02-09 00:25:38

标签: c# struct alignment atomic value-type

我是一个修补匠 - 毫无疑问。出于这个原因(并且除此之外),我最近做了一个小实验,以确认我怀疑写入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 != YY != Z永远不会出现的类型,但事实上这个可以发生值是“mid-assignment”,同时由一个单独的线程复制到另一个位置。

好的,很重要。好奇心和更多。但后来我有这种预感:我的64位CPU 应该实际上能够原子地复制64位,对吧?那么如果我摆脱Z而只是坚持XY怎么办?那只是64位;应该可以一步覆盖它们。

果然,它奏效了。(我知道你们当中有些人现在正在皱起眉头,想着,是的,呃。这怎么会有趣?幽默我。)当然,我不知道这是否有保证给我的系统。我对寄存器,缓存未命中等几乎一无所知(我实际上只是在不理解其含义的情况下反驳了我听过的术语);所以现在这对我来说都是一个黑盒子。

接下来我尝试了 - 仅仅是预感 - 是一个由32位使用2个short字段组成的结构。这似乎也表现出“原子可分配性”。但然后我尝试了一个24位结构,使用了3个byte字段:没有去

突然,结构似乎再次受到“中期任务”副本的影响。

低至16位,带有2个byte字段:原子再次!

有人可以向我解释为什么会这样吗?我听说过“比特打包”,“缓存线跨越”,“对齐”等等 - 但是我再也不知道这意味着什么,也不知道它是否与此相关。但我感觉就像我看到一个模式,却无法确切地说出它是什么;清晰度将不胜感激。

4 个答案:

答案 0 :(得分:14)

您正在寻找的模式是CPU的原生字大小。

从历史上看,x86系列本身使用16位值(之前是8位值)。因此,您的CPU可以原子地处理这些:它是设置这些值的单个指令。

随着时间的推移,本机元素大小增加到32位,后来增加到64位。在每种情况下,都添加了一条指令来处理这个特定的位数。但是,为了向后兼容,旧的指令仍然存在,因此您的64位处理器可以使用所有以前的原始大小。

由于struct元素存储在连续的内存中(没有填充,即空的空间),运行时可以利用这些知识仅为这些大小的元素执行该单个指令。简而言之,这会产生您所看到的效果,因为CPU一次只能执行一条指令(尽管我不确定在多核系统上是否可以保证真正的原子性)。

但是,原生元素大小从不是24位。因此,没有单个指令可以写入24位,因此需要多个指令,并且会失去原子性。

答案 1 :(得分:5)

C#标准(ISO 23270:2006ECMA-334)有关原子性的说法:

12.5变量引用的原子性 以下数据类型的读写应为原子:bool,char,byte,sbyte,short,ushort, uint,int,float和引用类型。此外,读取和写入具有基础类型的枚举类型 在前面的列表中也应该是原子的。 读取和写入其他类型,包括long,ulong,double, 和十进制,以及用户定义的类型,不一定是原子的。强调我的)除了设计的库函数 为此目的,不保证原子读 - 修改 - 写,例如在增量或 减量。
您的示例X = Y = Z = value是3个单独的赋值操作的简写,每个操作都被12.5定义为原子。 3个操作的顺序(将value分配给Z,将Z分配给Y,将Y分配给X not 保证是原子的。

由于语言规范不要求原子性,而X = Y = Z = value; 可能是原子操作,无论它是否依赖于一大堆因素:

  • 编译器编写者的突发奇想
  • 在构建时选择了哪些代码生成优化选项(如果有)
  • 负责将程序集的IL转换为机器语言的JIT编译器的详细信息。比如,在Mono下运行的IL可能表现出与在.Net 4.0下运行时不同的行为(甚至可能与早期版本的.Net不同)。
  • 正在运行程序集的特定CPU。

有人可能还会注意到,即使单个机器指令也不一定是原子操作 - 许多都是可中断的。

此外,访问CLI标准(ISO 23217:2006),我们发现第12.6.6节:

  

12.6.6原子读写   符合标准的CLI应保证正确的读写访问权限   对齐的内存位置不大于本机字大小(类型的大小   当对位置的所有写访问都是时,native int)是原子的(参见§12.6.2)   大小相同。原子写入除了写入之外不得改变任何位。除非   显式布局控制(参见分区II(控制实例布局))用于   改变默认行为,数据元素不大于自然字大小(   native int)的大小应正确对齐。对象引用应予以对待   好像它们以原始字大小存储。

     

[ 注意:无法保证   关于内存的原子更新(读 - 修改 - 写),除了提供的方法   该目的是作为类库的一部分(参见Partition IV)。强调我的)   原子写入“小数据项”(不大于本机字大小的项)    需要在不支持direct的硬件上进行原子读取/修改/写入   写入小数据项。 结束记录]

     

[注意:当大小为0时,没有保证对8字节数据的原子访问   即使某些实现可能执行原子,本机int也是32位   数据在8字节边界上对齐时的操作。 结束记录]

答案 2 :(得分:3)

x86 CPU操作以8位,16位,32位或64位进行;操纵其他尺寸需要多次操作。

答案 3 :(得分:3)

编译器和x86 CPU将要小心移动与结构定义的字节数完全相同的字节数。没有x86指令可以在一次操作中移动24位,但是有8,16,32和64位数据的单指令移动。

如果向24位结构添加另一个字节字段(使其成为32位结构),您应该看到原子性返回。

某些编译器允许您在结构上定义填充,使其行为类似于本机寄存器大小的数据。如果填充24位结构,编译器将添加另一个字节,将大小“舍入”为32位,以便整个结构可以在一条原子指令中移动。缺点是你的结构总是占用内存空间的30%。

请注意,内存中结构的对齐对原子性也很重要。如果多字节结构不是在对齐的地址处开始,则它可能跨越CPU高速缓存中的多个高速缓存行。即使操作码是单个移动指令,读取或写入该数据也需要多个时钟周期和多个读/写。因此,如果数据未对齐,即使单个指令移动也可能不是原子的。 x86确保在对齐边界上进行原始大小读/写的原子性,即使在多核系统中也是如此。

使用x86 LOCK前缀可以通过多步移动实现内存原子性。但是应该避免这种情况,因为它在多核系统中可能非常昂贵(LOCK不仅阻止其他内核访问内存,它还会在操作期间锁定系统总线,这会影响磁盘I / O和视频操作.LOCK可能还强制其他核心清除他们的本地缓存)