0
点赞
收藏
分享

微信扫一扫

关于 Span 的一切:探索新的 .NET 明星:5. .NET 运行时的处理

zhyuzh3d 2022-05-24 阅读 70


.5. NET 运行时会怎么样?

  • ​​1. Span<T> 是什么?​​
  • ​​2. Span<T> 是如何实现的?​​
  • ​​3. 什么是 Memory<T>,以及为什么你需要它?​​
  • ​​4. Span 和 Memory 是如何与 .NET 库集成的?​​
  • ​​5. NET 运行时​​
  • ​​6. C# 语言和编译器受到什么影响?​​

.NET 运行时提供的安全保证之一是确保访问数组的下标不会越界,实践中被称为边界检查,考虑如下方法:

[MethodImpl(MethodImplOptions.NoInlining)]
static int Return4th(int[] data) => data[3];

在我撰写本文的 x64 机器上,生成的汇编代码如下所示:

sub      rsp, 40
cmp dword ptr [rcx+8], 3
jbe SHORT G_M22714_IG04
mov eax, dword ptr [rcx+28]
add rsp, 40
ret
G_M22714_IG04:
call CORINFO_HELP_RNGCHKFAIL
int3

​cmp​​​ 指令比较下标 3 与数组的长度,如果下标 3 越界,随后的 ​​jbe​​ 指令跳转到检查失败代码 ( 抛出异常 )。JIT 需要生成代码来确保此类访问不会导致下标越界,但是这不意味着每次独立的数组访问都需要边界检查。考入下面的 Sum 方法:

static int Sum(int[] data)
{
int sum = 0;
for (int i = 0; i < data.Length; i++) sum += data[i];
return sum;
}

JIT 需要生成确保访问 data[i] 的代码不会下标越界,但是因为可以告诉 JIT,从循环结构 i 永远不会越界 ( 循环从头到尾遍历每个元素 ),JIT 可以优化掉对数组访问的边界检查。这样,生成的汇编代码如下所示:

G_M33811_IG03:
movsxd r9, edx
add eax, dword ptr [rcx+4*r9+16]
inc edx
cmp r8d, edx
jg SHORT G_M33811_IG03

​cmp​​ 指令还在循环中,但是只是简单地比较下标 i 和数组的长度 ( 它保存在 r8d 寄存器中 ),没有了其它的边界检查。

运行时使用类似对 Span ( 包括 Span<T> 和 ReadOnlySpan<T> ) 的优化。对比上一示例的如下代码,这里仅仅变更了参数类型:

static int Sum(Span<int> data)
{
int sum = 0;
for (int i = 0; i < data.Length; i++) sum += data[i];
return sum;
}

上述代码生成的汇编代码几乎相同。

G_M33812_IG03:
movsxd r9, r8d
add ecx, dword ptr [rax+4*r9]
inc r8d
cmp r8d, edx
jl SHORT G_M33812_IG03

汇编代码非常类似,因为省去了边界检查。但是因为 JIT 识别到 Span 原生的索引器,意味着 JIT 对索引器生成特定的代码,而不是转换实际代码到汇编中。

所有这些说明了,对于运行时可以使用 Span 对数组相同类型的优化,使得 Span 成为访问数据的高效机制。更深入内容请参见:​​bit.ly/2zywvyI​​。

举报

相关推荐

0 条评论