まれに時々CPUモデルをC#からほしくなることがあります。Zxなどを使ってlscpu的な情報をとるのもいいのですが、C#でマネージドに取得するメモです。
CPUモデルをコマンドで取得する
先にCPUモデルを取得するとどんな値になるのかをデファクトなコマンドで試しましょう。いずれのOS、コマンドでもモデル名の値はAMD Ryzen 9 7950X3D 16-Core Processorで共通とわかります。
Linux
lscpuを使って、Vendor IDセクションでModel nameを取得できます。/proc/cpuinfoを読むのも軽量でいいでしょう。
$ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Address sizes: 48 bits physical, 48 bits virtual Byte Order: Little Endian CPU(s): 8 On-line CPU(s) list: 0-7 Vendor ID: AuthenticAMD Model name: AMD Ryzen 9 7950X3D 16-Core Processor CPU family: 25 Model: 97 ... 省略 $ cat /proc/cpuinfo | grep "model name" | uniq model name : AMD Ryzen 9 7950X3D 16-Core Processor
Windows
WindowsならwmicやGet-CimInstanceを使って取得できます。
# コマンドプロンプト $ wmic CPU get NAME Name AMD Ryzen 9 7950X3D 16-Core Processor # PowerShell $ Get-CimInstance -ClassName Win32_Processor DeviceID Name Caption MaxClockSpeed SocketDesignation -------- ---- ------- ------------- ------------ CPU0 AMD Ryzen 9 7950X3D 16-Core Processor AMD64 Family 25 Model 97 Stepping 2 4201 AM5
macOS
macOSはsysctlを使って取得できます。/proc/cpuinfoなんてものはない。
$ sysctl -n machdep.cpu.brand_string
Apple M2
C#でCPUモデルを取得する
C#コードでCPUモデルを取得します。x86_64はOS問わず共通処理でとれますが、ARM64はOSプラットフォームによって処理を分ける必要があるので、Windows・Linux・OSXで処理を分けつつ動作確認します。
| OS | Architecture | サポート状況 |
|---|---|---|
| Windows | x86_64 | 〇 |
| Windows | Arm64 | × (実装してないだけ) |
| Linux | x86_64 | 〇 |
| Linux | Arm64 | 〇 |
| OSX | x86_64 | 〇 |
| OSX | Arm64 | 〇 |
なお、#if OS_OSXプリプロセッサを使うためcsprojに以下の条件付きコンパイルシンボルを仕込んでおきます。Windows ARM64が未実装なのはこれをさぼったからです。
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net9.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> <PropertyGroup Condition="$([MSBuild]::IsOSPlatform('OSX'))"> <DefineConstants>OS_OSX</DefineConstants> </PropertyGroup> </Project>
C#コードは次の通りです。
using System.Runtime.InteropServices; Console.WriteLine($"CPU: {CpuInformation.Current.ModelName}"); public class CpuInformation { public static CpuInformation Current { get; } = new CpuInformation(); public string ModelName { get; private set; } = ""; private CpuInformation() { if (System.Runtime.Intrinsics.X86.X86Base.IsSupported) { // x86_64 OS (Linux, Windows, macOS) ... ModelName = GetX86CpuModelName(); } else { if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { // Linux Arm64 will be here... ModelName = GetLinuxModelName(); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { // macOS will be here... ModelName = GetOSXModelname(); } else { // Windows Arm64 is not supported... Don't like WMI or kernel32.dll SystemInfo ModelName = "Unsupported OS"; } } } private static string GetX86CpuModelName() { Span<int> regs = stackalloc int[12]; // call 3 times (0x80000002, 0x80000003, 0x80000004) for 4 registers // Calling __cpuid with 0x80000000 as the InfoType argument and gets the number of valid extended IDs. var extendedId = System.Runtime.Intrinsics.X86.X86Base.CpuId(unchecked((int)0x80000000), 0).Eax; // Get the information associated with each extended ID. if ((uint)extendedId >= 0x80000004) { int p = 0; for (uint i = 0x80000002; i <= 0x80000004; ++i) { var (Eax, Ebx, Ecx, Edx) = System.Runtime.Intrinsics.X86.X86Base.CpuId((int)i, 0); regs[p + 0] = Eax; regs[p + 1] = Ebx; regs[p + 2] = Ecx; regs[p + 3] = Edx; p += 4; // advance } return ConvertToString(regs); } return $"Unknown CPU Architecture (extendedId: {extendedId})"; static string ConvertToString(ReadOnlySpan<int> regs) { Span<byte> bytes = stackalloc byte[regs.Length * 4]; // int 4byte * 12 for (int i = 0; i < regs.Length; i++) { BitConverter.TryWriteBytes(bytes.Slice(i * 4, 4), regs[i]); } return System.Text.Encoding.ASCII.GetString(bytes).Trim(); } } private static string GetLinuxModelName() { var cpuInfo = File.ReadAllText("/proc/cpuinfo"); var lines = cpuInfo.Split('\n'); foreach (var line in lines) { if (!line.StartsWith("model name")) { continue; } var parts = line.Split(':'); if (parts.Length > 1) { var modelName = parts[1].Trim(); return modelName; } } return "Unknown"; } private static string GetOSXModelname() { #if OS_OSX IntPtr size = IntPtr.Zero; sysctlbyname("machdep.cpu.brand_string", IntPtr.Zero, ref size, IntPtr.Zero, IntPtr.Zero); IntPtr buffer = Marshal.AllocHGlobal(size.ToInt32()); sysctlbyname("machdep.cpu.brand_string", buffer, ref size, IntPtr.Zero, IntPtr.Zero); string result = Marshal.PtrToStringAnsi(buffer); Marshal.FreeHGlobal(buffer); return result; #else return "unknown"; #endif } #if OS_OSX [DllImport("libc")] private static extern int sysctlbyname(string name, IntPtr oldp, ref IntPtr oldlenp, IntPtr newp, IntPtr newlen); #endif }
x86_64でCPUモデル取得
OSごとに処理を分けるのはできるなら避けたいところです。幸いx86_64のOSはCPUID命令を使ってCPUモデルを取得できるので、System.Runtime.Intrinsics.X86.X86Base.CpuIdを使ってCPUモデルを取得します。WikipediaのCPUIDからCPUID EAXのフォーマットは次の通りです。
あとはこれをC#で読めばOKなので、X86Base.CpuId(0x80000002, 0, ...) ~ 0x80000004を呼び出してCPUモデル名を取得しています。GetX86CpuModelNameの具体的にな処理は次の流れです。
0x80000000を見て拡張CPUIDが利用できるか確認します。0x80000000を指定すれば拡張CPUIDの最大IDを取得できるので、0x80000004以上が利用可能ならCPUモデルを取得します。
var extendedId = System.Runtime.Intrinsics.X86.X86Base.CpuId(unchecked((int)0x80000000), 0).Eax; if ((uint)extendedId >= 0x80000004) { }
0x80000002 ~ 0x80000004を呼び出してCPUモデルを取得します。CpuIdの戻り値はEax, Ebx, Ecx, Edxの4つの値にASCII文字列が格納されています。
int p = 0; for (uint i = 0x80000002; i <= 0x80000004; ++i) { var (Eax, Ebx, Ecx, Edx) = System.Runtime.Intrinsics.X86.X86Base.CpuId((int)i, 0); regs[p + 0] = Eax; regs[p + 1] = Ebx; regs[p + 2] = Ecx; regs[p + 3] = Edx; p += 4; // advance } return ConvertToString(regs);
あとは、ConvertToStringでEax-Edx各レジスタをバイト列に変換してASCII文字列として結合します。StringBuilderで次のように書いてもいいのですが、今回はSpanを使って高速化しています。
// わかりやすい static string ConvertToString(ReadOnlySpan<int> regs) { var sb = new System.Text.StringBuilder(); foreach (int reg in regs) { var bytes = BitConverter.GetBytes(reg); sb.Append(System.Text.Encoding.ASCII.GetString(bytes)); } return sb.ToString().Trim(); } // 今回は最適化 static string ConvertToString(ReadOnlySpan<int> regs) { Span<byte> bytes = stackalloc byte[regs.Length * 4]; // int 4byte x 12 for (int i = 0; i < regs.Length; i++) { BitConverter.TryWriteBytes(bytes.Slice(i * 4, 4), regs[i]); } return System.Text.Encoding.ASCII.GetString(bytes).Trim(); }
OS固有処理をプリプロセッサで判別する
OSXはsysctlでCPUモデルを呼び出す必要があるのですが、プロセス実行を避けるためDllImportでlibc経由にします。ただDllImportを素直に書くとOSX以外でビルドするとエラーになるため、OSXでのみ有効になるようにcsprojでDefineConstantsにOX_OSXを登録して実行環境がOSXか判別しましょう。csprojで次のようにOSごとの条件付きコンパイルシンボルを登録することで、プリプロセッサでOS判別できます。
<PropertyGroup Condition="$([MSBuild]::IsOSPlatform('Windows'))"> <DefineConstants>OS_WINDOWS</DefineConstants> </PropertyGroup> <PropertyGroup Condition="$([MSBuild]::IsOSPlatform('Linux'))"> <DefineConstants>OS_LINUX</DefineConstants> </PropertyGroup> <PropertyGroup Condition="$([MSBuild]::IsOSPlatform('FreeBSD'))"> <DefineConstants>OS_FREEBSD</DefineConstants> </PropertyGroup> <PropertyGroup Condition="$([MSBuild]::IsOSPlatform('OSX'))"> <DefineConstants>OS_OSX</DefineConstants> </PropertyGroup>
csprojに仕込みがいるのはちょっといやですね。ただ、macOSやWindowsでプロセス呼び出しではなくP/Invokeするならこの手当が必要です。
#if OS_WINDOWS // Windows-specific code #elif OS_LINUX // Linux-specific code #elif OS_FREEBSD // FreeBSD-specific code #elif OS_OSX // OSX-specific code #endif
今回Windows ARM64は実装していませんが、Windows ARM64でCPUモデルを取得するのみWMIを使うなら、同様の対処が必要でしょう。
参考
まとめ
ARM64でもCPUモデルがCPUIDから取得できるといいのですが、調べた感じだとCPUID相当がなさそうでした、まじか。ということで、ARM64だけはWMIや/proc/cpuinfoを使って取得する必要があるのですが、ちょっとやりたくないのでいったん雑にしています。
このコードでAzure VMの実行CPUモデルを都度取得したりしています。各クラウドで同じVMといってもCPUモデルがいろいろあるので、ちょっとした情報収集に使えます。