【问题标题】:Can optimised builds and JIT compilation create problems when modifying a variable through a Span<T>?通过 Span<T> 修改变量时,优化构建和 JIT 编译会产生问题吗?
【发布时间】:2019-03-08 18:19:25
【问题描述】:

假设我使用MemoryMarshal.CreateSpan 来访问本地值类型的字节,例如以下(不是很有用)代码:

using System;
using System.Runtime.InteropServices;

// namespace and class boilerplate go here

private static void Main()
{
    int value = 0;
    Span<byte> valueBytes = MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref value, 1));

    var random = new Random();
    while (value >= 0) // the check in question
    {
        random.NextBytes(valueBytes);
        Console.WriteLine(value);
    }
}

虽然此代码按预期工作,但如果变量value 未在循环中修改,除非通过 @ 987654325@跨度?我可以依靠阅读value 给我写到valueBytes 的内容,或者这是否容易被重新排序?还是我只是因为最近涉足了一点 C++ 而变得偏执?

(注意我知道还有其他方法可以达到上述代码的预期效果,这不是关于如何获得全范围32位随机整数的问题或关于某些更大应用程序的XY问题我正在尝试将此代码放入其中,不存在这样更大的应用程序)

【问题讨论】:

  • 好问题!我想它也可以扩展到指令重新排序可能会或可能不会与这种情况混淆(比如通过跨度修改 int 值,然后访问 int 值。是否保证 C# 编译器或 JITer 会检测到这种情况并防止跨度写访问在值变量的访问之后被重新排序?)
  • @elgonzo - 很好,我已经更新了问题以包含它。
  • 听起来你被 C 和 C++ 中的类型双关语规则所困扰。他们确实可怕地滥用了它。 C# 确实记录了原始规则的意图,从较小的类型转换为具有较大对齐要求的较大类型是未定义的。这只是硬处理器行为,在 RISC 时代更为重要。滥用规则停止检查指针别名,真正的邪恶行为,这不是问题。标准的第 23.5.1 节明确允许这种转换。请注意,与您在此处所做的一样,字节级别的寻址也是在 C 和 C++ 中定义的。

标签: c# memory compilation


【解决方案1】:

我认为,唯一确定的答案可以由在 Roslyn 和 RyuJIT 方面实现编译器优化的人提供。

由于您使用的是 .NET Core,您当然可以深入研究源代码并自行找到答案。不过,这将是特定编译器版本的答案。

查看为您的 sn-p 生成的 IL 代码:

// int value = 0;
ldc.i4.0
stloc.0

// MemoryMarshal.CreateSpan(ref value, 1)
ldloca.s 0
ldc.i4.1
call valuetype System.Span`1<!!0> System.Runtime.InteropServices.MemoryMarshal::CreateSpan<int32>(!!0&, int32)

// the rest is omitted

注意ldloca.s 操作码。这个操作loads the address of the local variable onto the evaluation stack

虽然我无法为您提供官方链接来证明这一点,但我很确定 C# 和 JIT 编译器都不会优化掉那个局部变量——只是因为它的地址被使用了,所以这个局部变量有可能会通过其地址进行变异。

如果你查看生成的汇编代码,你会看到:局部变量在那里并且被放置到堆栈上,它不是一个仅寄存器变量。

// int value = 0;
xor         ecx,ecx  
mov         dword ptr [rsp+3Ch],ecx 

WHILE_LOOP_START:
// ... do stuff

// effectively: if (value >= 0) goto WHILE_LOOP_START
cmp         dword ptr [rsp+3Ch],0  
jge         WHILE_LOOP_START  

尝试编写一些不会产生ldloca.s 操作码的代码(例如,在循环中只生成++value),value 变量很可能会成为仅寄存器变量。

如果您修改您的代码以使value 永远不会被写入(初始化除外),JIT 编译器实际上将完全消除检查和变量本身:

LOOP:

// Console.WriteLine(0)
xor         ecx,ecx  
call        CONSOLE_WRITE_LINE

// while (true)
jmp         LOOP

不过有趣的是,C# 编译器不会进行这种优化:

// int value = 0;
ldc.i4.0
stloc.0

br.s WHILE_CHECK

LOOP_START:
// Console.WriteLine(value)
ldloc.0
call void System.Console::WriteLine(int32)

WHILE_CHECK:
// effectively: if (value >= 0) goto LOOP_START
ldloc.0
ldc.i4.0
bge.s LOOP_START

同样,我的答案中的 IL 和汇编代码是特定于平台和编译器的(甚至是特定于 CLR 的)。我无法向您提供证明文件。但我很确定没有编译器会优化掉一个获得地址的局部变量,甚至在调用方法/函数时用作参数。

也许 Roslyn 和 RyuJIT 团队的人可以给你一个更好的答案。

【讨论】:

  • 感谢您的信息,这相当令人放心,最近的所有更改似乎都没有借用 C++ 脚枪!
  • @dymanoid 是正确的——因为本地是“地址占用”,jitted 代码应该适当保守并获取更新。并且 jit 不做任何“基于类型”的别名消歧。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-02-26
  • 1970-01-01
  • 1970-01-01
  • 2021-03-19
  • 1970-01-01
  • 2015-09-03
  • 2019-01-10
相关资源
最近更新 更多