2016-06-18 102 views
10

Ho provato diversi modi di generare un timestamp quando ho trovato qualcosa di sorprendente (per me).Come è il CLR più veloce di me quando si chiama l'API di Windows

Chiamata di di Windows GetSystemTimeAsFileTime utilizzando P/Invoke è circa 3 volte più lento di chiamare DateTime.UtcNow che utilizza internamente involucro del CLR per lo stesso GetSystemTimeAsFileTime.

Come può essere?

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

mio codice di prova utilizzando 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); 
} 

E i risultati:

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

CLR può chiamare direttamente. Pinvoke passa attraverso il livello di marshalling. –

+0

@DavidHeffernan anche quando i parametri non hanno bisogno del marshalling? – i3arnon

+1

@ i3arnon: Qualcosa deve analizzarli per provarlo. –

risposta

4

Il CLR passa quasi certamente un puntatore a una variabile locale (automatica, stack) per ricevere il risultato. Lo stack non viene compattato o riposizionato, quindi non c'è bisogno di appuntare la memoria, ecc. E quando si usa un compilatore nativo, tali cose non sono supportate comunque, quindi non ci sono spese generali per tenerne conto.

In C#, tuttavia, la dichiarazione p/invoke è compatibile con il passaggio di un membro di un'istanza di classe gestita che vive nell'heap raccolto. P/invoke deve bloccare questa istanza o altrimenti rischiare di spostare il buffer di output durante/prima che la funzione OS scriva su di esso. Anche se si passa una variabile memorizzata nello stack, p/invoke deve ancora verificare e vedere se il puntatore si trova nell'heap garbage collector prima che possa diramarsi attorno al codice pinning, quindi c'è un overhead diverso da zero anche per il caso identico.

E 'possibile che si potrebbe ottenere risultati migliori usando

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

Eliminando la out parametro, P/Invoke non ha più a che fare con aliasing e heap compattazione, che è ormai completamente la responsabilità del vostro codice che imposta fino al puntatore.

4

Quando il codice gestito richiama il codice non gestito, viene eseguito uno stack walk assicurandosi che il codice chiamante disponga dell'autorizzazione UnmanagedCode per abilitarlo.

La camminata in pila viene eseguita in fase di esecuzione e comporta notevoli costi in termini di prestazioni.

E 'possibile rimuovere il controllo in fase di esecuzione (c'è ancora un JIT in fase di compilazione uno) utilizzando la SuppressUnmanagedCodeSecurity attribute:

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

Questo porta mia implementazione circa la metà della strada verso il CLR di:

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

Tenete a mente, tuttavia, che farlo potrebbe essere estremamente rischioso in termini di sicurezza.

anche utilizzando unsafe come Ben Voigt ha suggerito lo porta a metà strada di nuovo:

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