Как 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*)×tamp);
#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 |