当调用Windows API时,CLR为什么比我更快?

14

我测试了不同的时间戳生成方式,当我发现一些令人惊讶的事情时。

使用P/Invoke调用Windows的GetSystemTimeAsFileTime比调用内部使用CLR包装器的GetSystemTimeAsFileTimeDateTime.UtcNow慢大约3倍。

怎么会这样呢?

这里是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();

Core 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;

我使用的测试代码 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);
}

结果如下:

                  Method |     Median |    StdDev |
------------------------ |----------- |---------- |
 GetSystemTimeAsFileTime | 14.9161 ns | 1.0890 ns |
                  UtcNow |  4.9967 ns | 0.2788 ns |

2
CLR可以直接调用它。PInvoke通过封送层进行调用。 - David Heffernan
即使参数不需要进行封送,@DavidHeffernan 也会这样做吗? - i3arnon
1
@i3arnon:必须有某种分析它们以证明这一点。 - Ben Voigt
C++/CLI 使用 internalcall 调用约定来发出程序集,就像 CLR 实现一样,这避免了 p/invoke 开销,因为它假设被调用者知道 .NET 的内存布局并将负责处理相关事宜。 - Ben Voigt
3
微软已经在CoreCLR项目中记录了编写非托管代码以便被托管程序直接调用所需的步骤。细节非常重要,很容易创建“GC(垃圾回收)漏洞”,这是pinvoke marshaller为您解决的问题,但代价是一些开销。您需要理解这篇文章所说的所有内容。请点击此处查看文章内容。 - Hans Passant
显示剩余3条评论
2个回答

8
当托管代码调用非托管代码时,会进行堆栈遍历以确保调用代码具有UnmanagedCode权限,从而使其能够执行。
这种堆栈遍历是在运行时完成的,并且对性能有很大的影响。
可以通过使用SuppressUnmanagedCodeSecurity属性来消除运行时检查(仍然存在JIT编译时检查)。
[SuppressUnmanagedCodeSecurity]
[DllImport("kernel32.dll")]
public static extern void GetSystemTimeAsFileTime(out long systemTimeAsFileTime);

这使我的实现方法已经完成了一半,接下来需要更进一步实现与CLR相同。
                  Method |    Median |    StdDev |
------------------------ |---------- |---------- |
 GetSystemTimeAsFileTime | 9.0569 ns | 0.7950 ns |
                  UtcNow | 5.0191 ns | 0.2682 ns |

请记住,从安全的角度考虑这样做可能非常危险。
此外,正如Ben Voigt建议的那样使用unsafe会让它再次变得半途而废。
                  Method |    Median |    StdDev |
------------------------ |---------- |---------- |
 GetSystemTimeAsFileTime | 6.9114 ns | 0.5432 ns |
                  UtcNow | 5.0226 ns | 0.0906 ns |

谢谢,SuppressUnmanagedCodeSecurity与传递指针(unsafe)的组合真是太棒了。在我的系统上,在64位模式下它使调用速度比DateTime.UtcNow快两倍(2.3纳秒对5.3纳秒),或者大约比仅抑制堆栈行走快四倍(8.9纳秒)。奇怪的是,在32位模式下差异不太明显,差别为3.2纳秒对4.3纳秒(是的,在WOW64下,DateTime.UtcNow比本机64位模式下更快)。为想出这个成功组合而喝彩! - DarthGizka

7
CLR几乎肯定会传递指向本地(自动、堆栈)变量的指针以接收结果。堆栈不会被压缩或重定位,因此没有必要固定内存等,并且当使用本机编译器时,这些东西也不受支持,因此没有开销需要考虑。
但在C#中,p / invoke声明与传递存储在垃圾回收堆中的托管类实例的成员兼容。P / invoke必须固定该实例,否则在操作系统函数写入输出缓冲区期间/之前会出现缓冲区移动的风险。即使您传递存储在堆栈上的变量,p / invoke仍必须测试并查看指针是否进入垃圾回收堆,以便在固定代码周围跳过分支,因此即使对于相同的情况,也存在非零开销。
通过使用……可能会获得更好的结果。
[DllImport("kernel32.dll")]
public unsafe static extern void GetSystemTimeAsFileTime(long* pSystemTimeAsFileTime);

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接