在我的机器上,我得到如下结果:
Method
Runtime
Mean
Ratio
Zeroing
.NET FW 4.8
22.85 ns
1.00
Zeroing
.NET Core 3.1
18.60 ns
0.81
Zeroing
.NET 5.0
15.07 ns
0.66
请注意,这种零实际上需要在比我提到的更多的情况下。特别是,默认情况下,c#规范要求在执行开发人员的代码之前,将所有本地变量初始化为默认值。你可以通过这样一个例子来了解这一点:
using System;
using System.Runtime.CompilerServices;
using System.Threading;
unsafe class Program
{
static void Main()
{
while (true)
{
Example();
Thread.Sleep(1);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
static void Example()
{
Guid g;
Console.WriteLine(*&g);
}
}
运行它,您应该只看到所有0输出的guid。这是因为c#编译器在编译的示例方法的IL中发出一个.locals init标志,而.locals init告诉JIT它需要将所有的局部变量归零,而不仅仅是那些包含引用的局部变量。然而,在.NET 5中,运行时中有一个新属性(dotnet/runtime#454):
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Module | AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Event | AttributeTargets.Interface, Inherited = false)]
public sealed class SkipLocalsInitAttribute : Attribute { }
}
c#编译器可以识别这个属性,它用来告诉编译器在其他情况下不发出.locals init。如果我们对前面的示例稍加修改,就可以将属性添加到整个模块中:
using System;
using System.Runtime.CompilerServices;
using System.Threading;
[module: SkipLocalsInit]
unsafe class Program
{
static void Main()
{
while (true)
{
Example();
Thread.Sleep(1);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
static void Example()
{
Guid g;
Console.WriteLine(*&g);
}
}
现在应该会看到不同的结果,特别是很可能会看到非零的guid。在dotnet/runtime#37541中,.NET5 中的核心库现在都使用这个属性来禁用.locals init(在以前的版本中,.locals init在构建核心库时通过编译后的一个步骤删除)。请注意,c#编译器只允许在不安全的上下文中使用SkipLocalsInit,因为它很容易导致未经过适当验证的代码损坏(因此,如果/当您应用它时,请三思)。
除了使零的速度更快,也有改变,以消除零完全。例如,dotnet/runtime#31960, dotnet/runtime#36918, dotnet/runtime#37786,和dotnet/runtime#38314 都有助于消除零,当JIT可以证明它是重复的。
这样的零是托管代码的一个例子,运行时需要它来保证其模型和上面语言的需求。另一种此类税收是边界检查。使用托管代码的最大优势之一是,在默认情况下,整个类的潜在安全漏洞都变得无关紧要。运行时确保数组、字符串和span的索引被检查,这意味着运行时注入检查以确保被请求的索引在被索引的数据的范围内(即greather大于或等于0,小于数据的长度)。这里有一个简单的例子:
public static char Get(string s, int i) => s[i];