Как CLR быстрее меня при вызове Windows API

Я тестировал различные способы генерации метки времени, когда я нашел что-то удивительное (для меня).

Вызов Windows GetSystemTimeAsFileTime с использованием P/Invoke примерно на 3 раза медленнее, чем вызов DateTime.UtcNow, который внутренне использует оболочку CLR для того же 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();

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 |

Ответ 1

CLR почти наверняка передает указатель на локальную (автоматическую, стек) переменную, чтобы получить результат. Стек не уплотняется или не перемещается, поэтому нет необходимости связывать память и т.д., И при использовании собственного компилятора такие вещи не поддерживаются в любом случае, поэтому для их учета нет накладных расходов.

Однако в С# объявление p/invoke совместимо с передачей члена экземпляра управляемого класса, живущего в куче мусора. P/invoke должен связывать этот экземпляр или рисковать перемещением выходного буфера во время/до того, как функция OS записывает на него. Несмотря на то, что вы передаете переменную, хранящуюся в стеке, p/invoke все равно должен проверить и посмотреть, находится ли указатель в кучу мусора, прежде чем он сможет развернуть код пиннинга, так что ненужные накладные расходы даже для идентичного случая.

Возможно, вы сможете получить лучшие результаты, используя

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

Исключив параметр out, p/invoke больше не имеет дело с сглаживанием и сжатием кучи, теперь полностью зависит ваш код, который устанавливает указатель.

Ответ 2

Когда управляемый код вызывает неуправляемый код, происходит переход стека, убедившись, что вызывающий код имеет разрешение UnmanagedCode, позволяющее сделать это.

Эта попытка стека выполняется во время выполнения и имеет значительные затраты на производительность.

Можно удалить проверку времени выполнения (есть еще одно компиляционное время JIT), используя атрибут SuppressUnmanagedCodeSecurity:

[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 |

Имейте в виду, что это может быть чрезвычайно опасно для безопасности.

Также, используя unsafe, как предложил Бен Фойгт, он снова возвращается на второй план:

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