.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。