はじめに
毎度おなじみ?タイトル詐欺です。
プロセス名からプロセスIDを引く際に、System.Diagnostics
名前空間のProcess.GetProcessesByName
を使用したら絶望したので滅ぼすことにしました。
まずは通常通りProcess.GetProcessesByName
でプロセスIDを取得する
まずは、以下のコードを実行してみます。
public int TestProcess_GetProcessesByName()
{
var ps = Process.GetProcessesByName("explorer");
if (ps.Length > 0)
{
return ps[0].Id;
}
return 0;
}
上記のコードをBenchmarkDotNetでベンチマークしてみます。
BenchmarkDotNet=v0.13.5, OS=Windows 10 (10.0.19044.3086/21H2/November2021Update)
Intel Core i7-10700K CPU 3.80GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK=7.0.300-preview.23179.2
[Host] : .NET 7.0.4 (7.0.423.11508), X64 RyuJIT AVX2
ShortRun : .NET 7.0.4 (7.0.423.11508), X64 RyuJIT AVX2
Job=ShortRun IterationCount=3 LaunchCount=1
WarmupCount=3
| Method | Mean | Error | StdDev | Allocated |
|------------------------------- |---------:|----------:|----------:|----------:|
| TestProcess_GetProcessesByName | 2.266 ms | 0.4786 ms | 0.0262 ms | 10.71 KB |
は?アロケーション多すぎでしょ!こんなやつ絶対に復讐してやる!
EnumProcesses
+GetProcessImageFileName
の場合
EnumProcesses
とGetProcessImageFileName
を使ってプロセス名からプロセスIDを取得してみます。
-
EnumProcesses
で、すべてのプロセスIDを取得 -
OpenProcess
で、プロセスを開く -
GetProcessImageFileName
で、プロセス名を取得 - プロセス名をチェックして対象プロセスだったらプロセスIDを返す
テストコード
[LibraryImport("psapi.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool EnumProcesses(ref uint processIds, uint arraySizeBytes, out uint bytesCopied);
[LibraryImport("kernel32.dll")]
private static partial IntPtr OpenProcess(uint dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, uint dwProcessId);
[LibraryImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool CloseHandle(IntPtr handle);
[LibraryImport("psapi.dll", EntryPoint = "GetProcessImageFileNameW", StringMarshalling = StringMarshalling.Utf16)]
private static partial uint GetProcessImageFileName(IntPtr hProcess, ref char lpImageFileName, uint nSize);
public int TestEnumProcessAndGetProcessImageFileName()
{
int pid = 0;
var size = 2048;
var pids = ArrayPool<uint>.Shared.Rent(size);
Span<uint> pidSpan = pids;
while (true)
{
if (!EnumProcesses(ref MemoryMarshal.GetReference(pidSpan), (uint)pidSpan.Length, out var b))
{
return pid;
}
if(pidSpan.Length > b)
{
pidSpan = pidSpan[..(int)(b / sizeof(uint))];
break;
}
else
{
ArrayPool<uint>.Shared.Return(pids);
size *= 2;
pids = ArrayPool<uint>.Shared.Rent(size);
pidSpan = pids;
}
}
var processName = ArrayPool<char>.Shared.Rent(512);
for (var i = 0; i < pidSpan.Length; i++)
{
var handle = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_VM_WRITE, false, pidSpan[i]);
if (handle == IntPtr.Zero) continue;
Span<char> processNameSpan = processName;
var processNameLength = GetProcessImageFileName(handle, ref MemoryMarshal.GetReference(processNameSpan), (uint)processNameSpan.Length);
if (processNameLength == 0)
{
CloseHandle(handle);
continue;
}
processNameSpan = processNameSpan[..(int)processNameLength];
if (processNameSpan.EndsWith("explorer.exe"))
{
CloseHandle(handle);
pid = (int)pidSpan[i];
break;
}
CloseHandle(handle);
}
ArrayPool<char>.Shared.Return(processName);
ArrayPool<uint>.Shared.Return(pids);
return pid;
}
| Method | Mean | Error | StdDev | Allocated |
|------------------------------------------ |---------:|---------:|----------:|----------:|
| TestEnumProcessAndGetProcessImageFileName | 3.436 ms | 1.361 ms | 0.0746 ms | 4 B |
流行りのArrayPool
でアロケーションは消せましたが、速度がいまいちです。
CreateToolhelp32Snapshot
の場合
-
CreateToolhelp32Snapshot
で、スナップショットを取る -
Process32First
で、最初のPROCESSENTRY32
を取得 -
PROCESSENTRY32.szExeFile
をチェックして対象プロセスならPROCESSENTRY32.th32ProcessID
を返す -
Process32Next
で、ほかのプロセスも調べる
テストコード
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr CreateToolhelp32Snapshot(uint flags, uint th32ProcessID);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto, Size = 568)]
private struct PROCESSENTRY32
{
internal uint dwSize;
internal uint cntUsage;
internal uint th32ProcessID;
internal IntPtr th32DefaultHeapID;
internal uint th32ModuleID;
internal uint cntThreads;
internal uint th32ParentProcessID;
internal int pcPriClassBase;
internal uint dwFlags;
internal char szExeFile;
}
[DllImport("kernel32", SetLastError = true, CharSet = CharSet.Auto)]
static extern bool Process32First([In] IntPtr hSnapshot, ref PROCESSENTRY32 lppe);
[DllImport("kernel32", SetLastError = true, CharSet = CharSet.Auto)]
static extern bool Process32Next([In] IntPtr hSnapshot, ref PROCESSENTRY32 lppe);
public int TestCreateToolhelp32Snapshot()
{
var h = CreateToolhelp32Snapshot(0x2, 0);
if(h == IntPtr.Zero) return 0;
var pe = new PROCESSENTRY32();
pe.dwSize = (uint)Marshal.SizeOf(typeof(PROCESSENTRY32));
if(!Process32First(h, ref pe)) return 0;
var targ = "explorer.exe".AsSpan();
do
{
var sp = MemoryMarshal.CreateSpan(ref pe.szExeFile, 260);
if(sp.StartsWith(targ))
{
return (int)pe.th32ProcessID;
}
} while (Process32Next(h, ref pe));
return 0;
}
| Method | Mean | Error | StdDev | Allocated |
|----------------------------- |---------:|----------:|----------:|----------:|
| TestCreateToolhelp32Snapshot | 4.100 ms | 0.7543 ms | 0.0413 ms | 5 B |
アロケーションは消せてるけど、遅い!
そもそも、Process.GetProcessesByName
って内部的にどのAPIを使用してるの?
C#のソースコードを読んでいくと、GetProcessInfos
という関数があります。
ここでNtQuerySystemInformation
を呼んで、情報を取得しているようです。これが最速の取得方法みたいなので、実装してみます。
NtQuerySystemInformation
の場合
この関数は、低レベルなAPIのため使い方が少し難しいです。以下の記事が参考になりました。
上記コードを参考に、黒魔術を使ってマーシャリング周りを改善してみます。
テストコード
[DllImport("ntdll.dll", SetLastError = false, ExactSpelling = true)]
public static extern int NtQuerySystemInformation(byte SystemInformationClass, ref byte SystemInformation, uint SystemInformationLength, out uint ReturnLength);
[StructLayout(LayoutKind.Sequential)]
private struct UNICODE_STRING
{
public ushort Length;
public ushort MaximumLength;
public IntPtr Buffer;
}
[StructLayout(LayoutKind.Sequential)]
private struct SYSTEM_PROCESS_INFORMATION
{
public uint NextEntryOffset;
public uint NumberOfThreads;
public long Reserved1;
private long Reserved1_1;
private long Reserved1_2;
private long Reserved1_3;
private long Reserved1_4;
private long Reserved1_5;
public UNICODE_STRING ImageName;
public int BasePriority;
public IntPtr UniqueProcessId;
public IntPtr Reserved2;
public uint HandleCount;
public uint SessionId;
public IntPtr Reserved3;
public UIntPtr PeakVirtualSize;
public UIntPtr VirtualSize;
public uint Reserved4;
public UIntPtr PeakWorkingSetSize;
public UIntPtr WorkingSetSize;
public IntPtr Reserved5;
public UIntPtr QuotaPagedPoolUsage;
public IntPtr Reserved6;
public UIntPtr QuotaNonPagedPoolUsage;
public UIntPtr PagefileUsage;
public UIntPtr PeakPagefileUsage;
public UIntPtr PrivatePageCount;
public long Reserved7;
private long Reserved7_1;
private long Reserved7_2;
private long Reserved7_3;
private long Reserved7_4;
private long Reserved7_5;
}
public unsafe int TestNtQuerySystemInformation()
{
const int SYSTEM_PROCESS_INFORMATION = 5;
const int STATUS_INFO_LENGTH_MISMATCH = unchecked((int)0xC0000004);
var buffer = ArrayPool<byte>.Shared.Rent(0x100000);
Span<byte> span = buffer;
while(true)
{
var status = NtQuerySystemInformation(SYSTEM_PROCESS_INFORMATION, ref MemoryMarshal.GetReference(span), (uint)span.Length, out var bufferLength);
if (status == 0) break;
if (status != STATUS_INFO_LENGTH_MISMATCH)
throw new Exception();
// Buffer resize
ArrayPool<byte>.Shared.Return(buffer);
buffer = ArrayPool<byte>.Shared.Rent((int)bufferLength);
span = buffer;
}
int pid = 0;
uint offset = 0;
while (true)
{
ref var rb = ref MemoryMarshal.GetReference(span);
ref var r = Unsafe.As<byte, SYSTEM_PROCESS_INFORMATION>(ref Unsafe.Add(ref rb, offset));
if (r.NextEntryOffset == 0) break;
var s = new ReadOnlySpan<char>(r.ImageName.Buffer.ToPointer(), r.ImageName.Length / 2);
if(s.SequenceEqual("explorer.exe"))
{
pid = (int)r.UniqueProcessId;
break;
}
offset += r.NextEntryOffset;
}
ArrayPool<byte>.Shared.Return(buffer);
return pid;
}
| Method | Mean | Error | StdDev | Allocated |
|----------------------------- |---------:|----------:|----------:|----------:|
| TestNtQuerySystemInformation | 2.401 ms | 0.7427 ms | 0.0407 ms | 4 B |
速い! そしてアロケーションも消えて、いい感じです。
こういう時に、どんなメモリに対しても同じAPIで操作できるSpan
はすごく便利です。
本来だったら、GCHandle.Alloc
を使用するところも、ArrayPool
+Span
+ref
で乗り切れます。
まとめ
最後に、ベンチマークをまとめて実行した結果です。
BenchmarkDotNet=v0.13.5, OS=Windows 10 (10.0.19044.3086/21H2/November2021Update)
Intel Core i7-10700K CPU 3.80GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK=7.0.300-preview.23179.2
[Host] : .NET 7.0.4 (7.0.423.11508), X64 RyuJIT AVX2
ShortRun : .NET 7.0.4 (7.0.423.11508), X64 RyuJIT AVX2
Job=ShortRun IterationCount=3 LaunchCount=1
WarmupCount=3
| Method | Mean | Error | StdDev | Allocated |
|------------------------------------------ |---------:|----------:|----------:|----------:|
| TestProcess_GetProcessesByName | 2.545 ms | 1.5778 ms | 0.0865 ms | 10970 B |
| TestEnumProcessAndGetProcessImageFileName | 3.234 ms | 2.2597 ms | 0.1239 ms | 4 B |
| TestCreateToolhelp32Snapshot | 3.930 ms | 1.5387 ms | 0.0843 ms | 5 B |
| TestNtQuerySystemInformation | 2.139 ms | 0.6918 ms | 0.0379 ms | 4 B |
最後のNtQuerySystemInformation
を使用した方法が、実行速度、ヒープアロケーション、どちらを見ても最速となりました。
やっぱり復讐は楽しいなぁ!!
ソースコード全体
今回使用したコードは以下の通りです。