2016-06-18 7 views
10

Ich testete verschiedene Möglichkeiten zur Generierung eines Zeitstempels, als ich etwas überraschend (für mich) fand.Wie ist die CLR schneller als ich beim Aufruf Windows-API

Aufruf von Windows GetSystemTimeAsFileTime mit P/Invoke ist etwa 3x langsamer als Aufruf DateTime.UtcNow, die intern CLR Wrapper für die gleiche GetSystemTimeAsFileTime verwendet.

Wie kann das sein?

Hier DateTime.UtcNow's implementation:

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 wrapper for 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; 

Mein Testcode verwendet 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); 
} 

Und die Ergebnisse:

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

CLR es direkt aufrufen können. Pinvoke durchläuft die Rangierschicht. –

+0

@DavidHeffernan auch wenn die Parameter nicht Marshalling benötigen? – i3arnon

+1

@ i3arnon: Etwas muss sie analysieren, um das zu beweisen. –

Antwort

4

Die CLR übergibt fast sicher einen Zeiger auf eine lokale (automatische, Stapel) Variable, um das Ergebnis zu erhalten. Der Stapel wird nicht kompaktiert oder verschoben, so dass es nicht notwendig ist, Speicher usw. zu fixieren, und wenn ein systemeigener Compiler verwendet wird, werden solche Dinge sowieso nicht unterstützt, so dass es keinen Overhead gibt, um sie zu berücksichtigen.

In C# ist die p/invoke-Deklaration jedoch kompatibel mit der Übergabe eines Members einer verwalteten Klasseninstanz, die sich im Sammelspeicher für überflüssige Objekte befindet. P/invoke muss diese Instanz anheften oder riskieren, dass sich der Ausgabepuffer während/vor dem Schreiben der OS-Funktion bewegt. Obwohl Sie eine auf dem Stack gespeicherte Variable übergeben, muss p/invoke trotzdem testen und sehen, ob der Zeiger in den Garbage Collected Heap gehört, bevor er um den Pinning-Code herum verzweigen kann, so dass selbst für den identischen Fall ein Overhead ungleich Null besteht.

Es ist möglich, dass Sie bessere Ergebnisse

[DllImport("kernel32.dll")] 
public unsafe static extern void GetSystemTimeAsFileTime(long* pSystemTimeAsFileTime); 

mit bekommen konnte durch den Wegfall der out Parameter, p/aufrufen muss nicht mehr mit Aliasing und Heap-Verdichtung zu behandeln, dass jetzt ist vollständig in der Verantwortung des Codes, der setzt den Zeiger hoch.

4

Wenn verwalteter Code nicht verwalteten Code aufruft, gibt es einen Stack-Pfad, der sicherstellt, dass der aufrufende Code die UnmanagedCode-Berechtigung besitzt, die dies ermöglicht.

Dieser Stack Walk ist zur Laufzeit ausgeführt und hat erhebliche Kosten in der Leistung.

Es ist möglich, die Laufzeitprüfung (es gibt noch eine JIT-Kompilierung-one) unter Verwendung des SuppressUnmanagedCodeSecurity attribute zu entfernen:

[SuppressUnmanagedCodeSecurity] 
[DllImport("kernel32.dll")] 
public static extern void GetSystemTimeAsFileTime(out long systemTimeAsFileTime); 

Das bringt meine Implementierung etwa die Hälfte der Weg in Richtung der CLR:

    Method | Median | StdDev | 
------------------------ |---------- |---------- | 
GetSystemTimeAsFileTime | 9.0569 ns | 0.7950 ns | 
        UtcNow | 5.0191 ns | 0.2682 ns | 

Denken Sie jedoch daran, dass dies in sicherheitstechnischer Hinsicht äußerst riskant sein kann.

Auch unsafe wie Ben Voigt mit vorgeschlagen bringt es auf halbem Weg wieder:

    Method | Median | StdDev | 
------------------------ |---------- |---------- | 
GetSystemTimeAsFileTime | 6.9114 ns | 0.5432 ns | 
        UtcNow | 5.0226 ns | 0.0906 ns |