在调用Windows API时,CLR如何比我更快

i3a*_*non 11 .net c# clr performance pinvoke

当我发现令人惊讶的事情(对我而言)时,我测试了生成时间戳的不同方法.

GetSystemTimeAsFileTime使用P/Invoke 调用Windows 比调用DateTime.UtcNow内部使用CLR的包装器的速度快3倍GetSystemTimeAsFileTime.

怎么可能?

DateTime.UtcNow是实施:

public static DateTime UtcNow {
    get {
        long ticks = 0;
        ticks = GetSystemTimeAsFileTime();
        return new DateTime( ((UInt64)(ticks + FileTimeOffset)) | KindUtc);
    }
}

[MethodImplAttribute(MethodImplOptions.InternalCall)] // Implemented by the CLR
internal static extern long GetSystemTimeAsFileTime();
Run Code Online (Sandbox Code Playgroud)

核心CLR的包装GetSystemTimeAsFileTime:

FCIMPL0(INT64, SystemNative::__GetSystemTimeAsFileTime)
{
    FCALL_CONTRACT;

    INT64 timestamp;

    ::GetSystemTimeAsFileTime((FILETIME*)&timestamp);

#if BIGENDIAN
    timestamp = (INT64)(((UINT64)timestamp >> 32) | ((UINT64)timestamp << 32));
#endif

    return timestamp;
}
FCIMPLEND;
Run Code Online (Sandbox Code Playgroud)

我的测试代码使用BenchmarkDotNet:

public class Program
{
    static void Main() => BenchmarkRunner.Run<Program>();

    [Benchmark]
    public DateTime UtcNow() => DateTime.UtcNow;

    [Benchmark]
    public long GetSystemTimeAsFileTime()
    {
        long fileTime;
        GetSystemTimeAsFileTime(out fileTime);
        return fileTime;
    }

    [DllImport("kernel32.dll")]
    public static extern void GetSystemTimeAsFileTime(out long systemTimeAsFileTime);
}
Run Code Online (Sandbox Code Playgroud)

结果如下:

                  Method |     Median |    StdDev |
------------------------ |----------- |---------- |
 GetSystemTimeAsFileTime | 14.9161 ns | 1.0890 ns |
                  UtcNow |  4.9967 ns | 0.2788 ns |
Run Code Online (Sandbox Code Playgroud)

Ben*_*igt 5

CLR几乎可以肯定地将指针传递给局部(自动,堆栈)变量以接收结果。堆栈不会被压缩或重定位,因此不需要固定内存等,并且在使用本机编译器时,无论如何都不支持此类操作,因此无需考虑开销。

但是在C#中,p / invoke声明与传递生活在垃圾收集堆中的托管类实例的成员兼容。P / invoke必须固定该实例,否则有可能在OS函数写入实例之前/期间使输出缓冲区移动。即使您确实传递了存储在堆栈中的变量,p / invoke仍然必须测试并查看指针是否在垃圾回收堆中,然后才能围绕固定代码进行分支,因此即使在相同的情况下,开销也不为零。

使用以下方法可能会获得更好的结果

[DllImport("kernel32.dll")]
public unsafe static extern void GetSystemTimeAsFileTime(long* pSystemTimeAsFileTime);
Run Code Online (Sandbox Code Playgroud)

通过消除out参数,p / invoke不再需要处理别名和堆压缩,现在这完全由设置指针的代码负责。


i3a*_*non 5

当托管代码调用非托管代码时,需要进行堆栈遍历,以确保调用代码具有UnmanagedCode权限以启用此权限。

该堆栈遍历是在运行时完成的,并且会在性能上产生大量成本。

可以通过使用SuppressUnmanagedCodeSecurity属性删除运行时检查(仍然是JIT编译时检查):

[SuppressUnmanagedCodeSecurity]
[DllImport("kernel32.dll")]
public static extern void GetSystemTimeAsFileTime(out long systemTimeAsFileTime);
Run Code Online (Sandbox Code Playgroud)

这使我的实现大约达到了CLR的一半:

                  Method |    Median |    StdDev |
------------------------ |---------- |---------- |
 GetSystemTimeAsFileTime | 9.0569 ns | 0.7950 ns |
                  UtcNow | 5.0191 ns | 0.2682 ns |
Run Code Online (Sandbox Code Playgroud)

请记住,尽管这样做可能在安全方面极具风险。

unsafe按照Ben Voigt的建议使用,将其再次中途使用:

                  Method |    Median |    StdDev |
------------------------ |---------- |---------- |
 GetSystemTimeAsFileTime | 6.9114 ns | 0.5432 ns |
                  UtcNow | 5.0226 ns | 0.0906 ns |
Run Code Online (Sandbox Code Playgroud)