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

Dan*_*Tao 12 c# struct atomic alignment value-type

我是一个修补匠 - 毫无疑问.出于这个原因(并且除此之外),我最近做了一个小实验来证实我怀疑写入a struct不是原子操作,这意味着试图强制执行某些约束的所谓"不可变"值类型可能假设失败了.

我使用以下类型作为插图写了一篇关于此的博客文章:

struct SolidStruct
{
    public SolidStruct(int value)
    {
        X = Y = Z = value;
    }

    public readonly int X;
    public readonly int Y;
    public readonly int Z;
}
Run Code Online (Sandbox Code Playgroud)

虽然上面看起来像一个类型,它不可能是真实的,X != Y或者Y != Z,其实这个可以,如果值是"中等分配"它是由一个单独的线程复制到另一个位置同一时间发生.

好的,很重要.好奇心和更多.但后来我有这个预感:我的64位CPU 应该居然能以原子复制64位的,对不对?那么,如果我摆脱Z并且只是坚持XY怎么办?那只是64位; 应该可以一步覆盖它们.

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

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

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

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

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

Mic*_*sen 14

您正在寻找的模式是CPU的本机字大小.

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

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

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

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

  • @Dan:你可能已经想到了这一点,这要归功于你对24位结构的实验,但这不是你应该依赖的东西.最好进行正确的锁定,因为尽管运行时不太可能停止使用此优化,但是如果结构的大小需要更改,正确的锁定可以保护您. (2认同)

Nic*_*rey 5

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

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

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

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

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

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

12.6.6原子读写 符合的CLI应保证对所有正确对齐的内存位置的读写访问权限不大于本机字大小(类型的大小 native int)是原子的(参见§12.6.2),当所有的写访问都是一个位置大小相同.原子写入除了写入之外不得改变任何位.除非使用显式布局控制(请参阅分区II(控制实例布局))来更改默认行为,否则不应大于自然字大小(a的大小native int)的数据元素.对象引用应被视为存储在本机字大小中.

[ 注意:不保证内存的原子更新(读 - 修改 - 写),除了为此目的提供的方法作为类库的一部分(参见Partition IV).(重点煤矿)"小数据项"的原子写入(不超过本地字大小大的项目) 要求做原子读取/修改/上不支持直接写入到小数据项硬件写.结束说明 ]

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