tech.guitarrapc.cóm

Technical updates

C#でCPUモデルを取得する

まれに時々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ならwmicGet-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のフォーマットは次の通りです。

image

あとはこれを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)
{

}

0x800000020x80000004を呼び出して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でDefineConstantsOX_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モデルがいろいろあるので、ちょっとした情報収集に使えます。