LoginSignup
15
14

More than 1 year has passed since last update.

Re:ゼロアロケーションから始めるP/Invoke

Posted at

はじめに

皆さん、黒魔術楽しんでますか?
今回はWINAPIなどの外部DLLを、今風にゼロアロケーションで呼び出す黒魔術を書いていきたいと思います。

例として呼び出すWINAPIは、コンピュータ名を取得するGetComputerNameWをターゲットにします。

GetComputerNameWのシグネチャ

pinvoke.netのGetComputerNameExを参考にGetComputerNameWのpinvokeを書くと以下のようになります。

シグネチャ
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
static extern bool GetComputerNameW(StringBuilder lpBuffer, ref uint lpnSize);

使い方はこんな感じです。

使い方
uint size = 256;
var sb = new StringBuilder((int)size);

GetComputerNameW(sb, ref size);

var name = sb.ToString();

見ただけで黒魔術使い(見習い)としては、とてもムズムズします!

何が良くない?

StringBuilderを結果の受け口として利用しているため、アロケーションが発生してしまいます。全然今風ではないですね!

しかも、結果としてコンピュータ名を取り出すためにToStringでさらにアロケーションが発生してしまいます。

なので、StringBuilderを抹殺するために、最近流行の黒魔術Span<T>stackallocを使います。

StringBuilderを抹殺する

まずは先ほどのGetComputerNameWのシグネチャを変更します。

シグネチャ(変更後)
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
static extern bool GetComputerNameW(ref char buffer, ref uint lpnSize);

第一引数をStringBuilderからref charに変更しました。

使い方(変更後)
Span<char> buffer = stackalloc char[256];
var size = (uint)buffer.Length;

GetComputerNameW(ref MemoryMarshal.GetReference(buffer), ref size);

var name = buffer[..(int)size].ToString();

これでStringBuilderは抹殺できました!stackallocを使ったのでアロケーションも1回分減っています!

でもbufferToStringしているため、ここでアロケーションが発生してしまいます。

もし、コンピュータ名を調べて特定の名前かどうかをチェックするとしたら、ToStringも抹殺可能です!

ToStringの抹殺
Span<char> buffer = stackalloc char[256];
var size = (uint)buffer.Length;

GetComputerNameW(ref MemoryMarshal.GetReference(buffer), ref size);

var targetNameSpan = "Hoge".AsSpan();
if(MemoryExtensions.SequenceEqual(buffer[..(int)size], targetNameSpan))
{
    // コンピュータ名は Hoge
}

MemoryExtensions.SequenceEqualSpan<T>同士が一致しているかを調べてくれます。この関数は内部で恐ろしいほどの最適化が加えられているため、ループで比較するよりもめちゃくちゃ速いです!絶対に使用しましょう!

詳しく知りたい方は以下の記事がすごく勉強になります。

関数化してみる

完全体
public static uint GetComputerName(Span<char> buffer)
{
    [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
    static extern bool GetComputerNameW(ref char buffer, ref uint lpnSize);

    var size = (uint)buffer.Length;
    GetComputerNameW(ref MemoryMarshal.GetReference(buffer), ref size);
    return size;
}

ローカル関数を使ってシグネチャを関数スコープに閉じ込めるのがマイブームです!

使い方
Span<char> buffer = stackalloc char[256];
var size = GetComputerName(buffer);
var str = buffer[..(int)size].ToString();

まとめ

これでゼロアロケーションでWINAPIを高速に詠唱することができるようになりました!

めでたしめでたし!

.
.
.

ではないんです!まだやれることがあります!

詠唱をSkipLocalsInitを使って無詠唱にする!

SkipLocalsInitとはなんぞや?という方も多いと思います。簡単に言うと、宣言された変数を0で初期化するのを止めてくれます。

以下のサイトで詳しく説明してくれています。

今回の場合、GetComputerNameを呼び出す際にstackallocしていますが、デフォルトの動作として確保した領域を0初期化しています。どうせWINAPI側で上書きするのだからすごく無駄です。

使い方は関数にSkipLocalsInit属性をつけてあげればOKです

SkipLocalsInitの使い方
public static uint GetComputerName(Span<char> buffer)
{
    [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
    static extern bool GetComputerNameW(ref char buffer, ref uint lpnSize);

    var size = (uint)buffer.Length;
    GetComputerNameW(ref MemoryMarshal.GetReference(buffer), ref size);
    return size;
}

[SkipLocalsInit]
public static void Call()
{
    Span<char> buffer = stackalloc char[256];
    var size = GetComputerName(buffer);
    var str = buffer[..(int)size].ToString();
}

すごくいい感じでゾクゾクします!
ただし、SkipLocalsInitはコンパイルオプションに/unsafeが必要となります。
黒魔術使いなら当然ONになっているはずなので問題ないですね!

15
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
14