防止编译器/CPU指令重排序 C#

20

我有一个包含两个Int32的Int64,就像这样:

[StructLayout(LayoutKind.Explicit)]
public struct PackedInt64
{
    [FieldOffset(0)]
    public Int64 All;
    [FieldOffset(0)]
    public Int32 First;
    [FieldOffset(4)]
    public Int32 Second;
}

现在我想要构造函数(对于所有第一个和第二个)。但是这个结构体要求在离开构造函数之前必须分配所有字段。 考虑所有的构造函数。

public PackedInt64(Int64 all)
{
    this.First = 0;
    this.Second = 0;
    Thread.MemoryBarrier();
    this.All = all;
}

我希望确保在构造函数中this.All被最后赋值,以防止编译器优化或CPU指令重新排序导致超过一半的字段被覆盖。

Thread.MemoryBarrier()是否足够? 是否是最佳选择?


清零 First 和 Second 的目的是什么? - zmbq
5
我会使用位运算来选出4字节整数,并消除令人讨厌的显式布局。 - David Heffernan
@DavidHeffernan 那是一种看待它的方式。然而,显式布局让我感到温暖和舒适。 :) - Carl R
1
只有在您提供了另一个线程如何访问该结构的良好描述时,才能进行推理。除非采取锁定措施,否则几乎没有可靠的方法。无论是您明确采取的还是操作系统在启动另一个线程时隐含的方式。这本身已经满足了内存屏障要求。 - Hans Passant
1
@CarlR:阅读我在答案中提供的显式布局文档,并告诉我它是否仍然让你感到温暖和舒适。 - Ben Voigt
显示剩余4条评论
4个回答

14

是的,这是预防重排序的正确且最佳方式。

在您的示例代码中执行 Thread.MemoryBarrier(),处理器将不允许以这样的方式重排序指令,即对 FirstSecond 的访问/修改会出现在对 All 的访问/修改之后。由于它们都占用相同的地址空间,因此您不必担心较晚的更改会被先前的更改覆盖。

请注意,Thread.MemoryBarrier() 仅适用于当前正在执行的线程 -- 它不是一种锁类型。但是,考虑到此代码正在构造函数中运行,而且没有其他线程可以访问这些数据,因此这应该完全没问题。然而,如果您确实需要跨线程保证操作,则需要使用锁定机制来保证独占访问。

请注意,在基于x86的计算机上,您实际上可能不需要此指令,但我仍然建议使用此代码,以防您有一天在其他平台上运行(例如IA64)。参见下面的图表,了解哪些平台会重新排序内存后保存,而不仅仅是后加载。

enter image description here


4
MemoryBarrier可以防止重排序,但此代码仍有缺陷。 LayoutKind.ExplicitFieldOffsetAttribute在文档中被描述为影响对象的内存布局,当该对象传递到非托管代码时。它可用于与C语言的union进行互操作,但不能用于模拟C语言的union
即使当前平台上该行为符合您的预期,也不能保证它将在未来继续这样做。唯一作出的保证是在与非托管代码进行互操作的上下文中(也就是p/invoke、COM互操作或C++/CLI - 它只是起作用)。
如果您想以便携、未来兼容的方式读取字节子集,则必须使用位操作或字节数组和BitConverter。即使语法不是很好看。

1
它确实可以用来模拟C联合体。我在一篇博客文章中利用了这个事实。 - Kendall Frey
1
使用LayoutKind.Sequential属性可以强制成员按它们出现的顺序顺序布局。对于可平坦化类型,LayoutKind.Sequential控制托管内存和非托管内存中的布局。对于不可平坦化类型,它控制在将类或结构体封送到非托管代码时的布局,但不控制托管内存中的布局。Blittable vs Nonblittable types: https://msdn.microsoft.com/zh-cn/library/75dwhxf7.aspx - Carl R
Kendall:请再读一遍我的第三段。 - Ben Voigt
1
@Carl:也许你正在寻找这句话:“使用LayoutKind.Explicit属性来控制每个数据成员的精确位置。这会影响托管和非托管布局,对于可平坦化和不可平坦化类型都是如此。”我不知道什么是正确的,在这一点上,我想看到公共语言运行时的ECMA标准中的正式语言,以了解做出了哪些保证,哪些是特定版本中的实现细节。 - Ben Voigt
@BenVoigt 是的,谢谢!如果您找到任何关于这方面的ECMA标准文档,请分享。 :) - Carl R

2

1
编写良好的代码意味着您需要考虑到未预期的环境。由于代码可能在具有不同内存排序的系统上运行,建议使用内存屏障。 - David Pfeffer
1
是的,我同意这就是为什么我在我的回答中说“是否这是最佳选择取决于您使用的系统”。 - Anandaraj

0

首先,我知道这个答案并没有真正解决重新排序的问题,而是将其否定了。通过使用不安全代码,您可以完全避免写入FirstSecond

public unsafe PackedInt64(long all) {
    fixed (PackedInt64* ptr = &this)
        *(long*) ptr = all;
}

这并不是最优雅的解决方案,可能也不符合大多数公司关于托管代码的政策,但它应该能够工作。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接