Edited at
D言語Day 3

@nogcに関する考察

More than 3 years have passed since last update.

ドーモ。皆さん。最近Phobosのコントリビューターとして影の薄いSHOOです。

今日は比較的最近追加された @nogc という属性について考えていきたいと思います。


@nogc ってなに?

D言語には、ガベージコレクション(GC)という機能が言語機能の一つとして備わっています。GCは、newなどによって動的に確保されたメモリが使われなくなった時に、自動的にメモリを開放してくれるという便利機能で、これによりプログラマ自身がメモリの生存期間を考えずにプログラム本来の流れに集中してコーディングできるなどの恩恵が有ります。

しかし、GCは便利である一方、 GCが動くとプログラムが一度完全に停止してしまう という問題が有ります。

プログラムの停止はほんの一瞬ですので、たいていのプログラムではこれが問題になることはほとんどありません。しかしながら、以下の様なごく一部のプログラムでは問題になる場合があります。


  • シューティングゲームや格闘ゲームのような、一瞬の処理の遅れが極めて致命的な操作障害になるようなプログラム。

  • 医療や航空機・車載用のような、一瞬の処理の遅れが人命に関わるようなプログラム。

  • 組み込みプログラムの割り込み処理などでのリアルタイム性確保が必要なプログラム。

こういったプログラムでは、GCの動作が致命的な動作不良になるケースが有り、これが原因でD言語はゲームプログラミングには向かない、などと言われることが有りました。

※ちなみに、GCがある言語でもゲームプログラミングは可能なので、この風評被害はお門違いなのですが、それについてはここでは議論しないことにします。あぁ、そういえば最近D言語製のシューティングゲームがリリースされましたね……(チラッチラッ

ところで、現状のD言語においてGCが動作するタイミングというのは、newなどによって新しくメモリが確保されようとした時で、かつ、新しいメモリが確保できなかった場合に限ります。つまり、 新しいメモリを確保しようとしなければ、GCが動作することは絶対にありません。 この性質を利用することで、先の問題は完全に解決することになります。

Q: そんなことできるのか?

A: 無理

不可能ではないけど、無理です。考えてみましょう。クラスや動的配列の生成、文字列の連結、クロージャの生成、ひょっとしたらこれらが使われてるかもしれない標準ライブラリの関数やライブラリの使用など、一切が禁じられた状態でプログラミングできますか?不可能じゃないけど無理というのはそういうことです。

※なお、GCが動作する可能性のある操作はここにかかれています。

クラスや配列の生成などは頑張れば何とかなりますが、特に難しいのは標準ライブラリなど自分で作っていない関数の使用ですね。使う場合は、中のソースコードをじっくり見て、GCが動作する可能性がないことを確認しなければなりません。もちろん、ライブラリをバージョンアップしたらそれをまたやり直し…。 でも、これ以外にGCの動作を防ぐ方法はありません。 絶対にGCが動かないプログラムを書くには、避けて通れないことなのです。

これを助けるための機能として導入されたのが、@nogcなのです。

@nogc属性のある関数内では、GCが動作する可能性のある操作を禁止することが出来ます。今まで、プログラマ自身がGCが動作しないようにしていたチェックをコンパイラが行ってくれるようになるのです。また、@nogcな関数は@nogcな関数内で呼ぶことができるため、@nogc属性がついていれば、ライブラリの関数など自分で作成していない関数でも中身のチェックなしに安心して呼び出すことが出来ます。


@nogcの現状

先に述べたGCが動いてもらっては困る場面では、ぜひ@nogcを使用していきたいところですが、まだまだ難点が多いのも事実です。

肝心の標準ライブラリに@nogcが付いているものがまだあまり多くないので、標準ライブラリを使用するほとんどの関数には@nogcをつけることが出来ません。もっとも、これに関しては@safepurenothrowなんかも似たような状況ですが……。

「これは@nogcで動かせるべき!」という標準ライブラリの関数があったら、遠慮せずBugzillaへ登録しましょう。


頑張って使う

現状、そのまま適用するのは非常に難易度が高いといえるでしょう。

使えるようにするには、コツ(と多少の強引さ)が必要です。


プロパティに使える

単純なプロパティなんかには現状の@nogcをすぐにでも適用できる場合が多いです。というか、pureとかnothrowとかもだいたい使えます。

たとえば、メンバ変数を返すだけのgetterなプロパティには、次のような属性をつけることができます。

int foo() pure @nogc nothrow @safe const @property

{
return _val;
}

同様に、setterはこんな感じ

void foo(int val) pure @nogc nothrow @safe @property

{
_val = val;
}

付けるべき属性が多いですが、我慢です全力で有り難みを感じつつ付けまくりましょう。


ラッパーに使える

Cの関数とかシステムコール・WinAPIなどしか呼ばないなど、比較的ローレベルの関数しか呼ばないものには、@nogcは使いやすいです。反面、@safeなんかは使いにくく、@trustedを多発するような関数になることが多いでしょう。これらをラップする関数を書くときなんかは@nogcが使える場合があります。

たとえば、core.stdc.*以下の関数には@nogcが付けられています。core.sys.windows.*以下の関数なんかにも。

(もっとも、これは本当に付けられていていいものかどうかは疑問です。たとえばWindowsAPIのウィンドウプロシージャのコールバック型WNDPROCなんかにも@nogcが付いているのですが、ウィンドウプロシージャなんかには普通にGC使う関数を渡す気がします。WNDPROC@nogcなのは変……。ということは、その関数を中で呼び出すDispatchMessageなんかにも@nogcは付けてはいけないはず……。いやいや、そんなことを言ったらWinAPIとかシステムコールなんかはどこでどんなコールバックが呼ばれるかわからない以上@nogcなんて付けられないんじゃ……。 まぁいいか。

以下、WinAPIを使ってUTF-8を指定したコードページのマルチバイト文字列に変換した時、バイト数がどうなるか調べる関数を@nogcで書いてみました。

※ ごめんなさい。WinAPIな例しか思いつかなかった……。

size_t mbsLength(string s, uint codePage = 0) @nogc

{
import core.stdc.stdlib;
import core.sys.windows.windows;
// UTF-8をUCS-2に変換
auto wcslen = MultiByteToWideChar(CP_UTF8, 0, s.ptr, s.length, null, 0);
auto wcsbufsize = (wcslen+1)*wchar.sizeof;
auto wcsbuf = (cast(wchar*)malloc(wcsbufsize))[0..wcslen+1];
scope (exit) free(wcsbuf.ptr);
MultiByteToWideChar(CP_UTF8, 0, s.ptr, s.length, wcsbuf.ptr, wcsbufsize);
// UCS-2をcodePageで表現されるマルチバイト文字列に変換した場合のサイズを返す
return WideCharToMultiByte(codePage, 0, wcsbuf.ptr, wcslen, null, 0, null, null);
}
@nogc unittest
{
assert(mbsLength("あいうえお", 932) == 10);
}


OutputRangeが使える

さっきのmbsLength、ホントは文字列の長さがほしいんじゃなくて、変換後の文字列がほしいんだよね…

そんなニーズに答えるのが、OutputRangeです。

まず、変換後の文字列を返す@nogcな関数を考えます。

char[] toMBS(string s, uint codePage = 0) @nogc

{
import core.stdc.stdlib;
import core.sys.windows.windows;
// UTF-8をUCS-2に変換
auto wcslen = MultiByteToWideChar(CP_UTF8, 0, s.ptr, s.length, null, 0);
auto wcsbufsize = (wcslen+1)*wchar.sizeof;
auto wcsbuf = (cast(wchar*)malloc(wcsbufsize))[0..wcslen+1];
scope (exit) free(wcsbuf.ptr);
MultiByteToWideChar(CP_UTF8, 0, s.ptr, s.length, wcsbuf.ptr, wcsbufsize);
// UCS-2をcodePageで表現されるマルチバイト文字列に変換
auto mbslen = WideCharToMultiByte(codePage, 0, wcsbuf.ptr, wcslen, null, 0, null, null);
auto mbsbufsize = mbslen+1;
auto mbsbuf = (cast(ubyte*)malloc(mbsbufsize))[0..mbslen+1];
scope (exit) free(mbsbuf.ptr); // <- ここでダングリングポインタになる
WideCharToMultiByte(codePage, 0, wcsbuf.ptr, wcslen, cast(char*)mbsbuf.ptr, mbsbufsize, null, null);
// 変換された文字列を返す
return cast(char[])mbsbuf[0..$-1];
}

@nogc unittest
{
assert(toMBS("あいうえお", 932) == "\x82\xA0\x82\xA2\x82\xA4\x82\xA6\x82\xA8");
}

いかがでしょう? ダングリングポインタとキャッキャウフフするunittestの完成です。

いけませんね。

GCつかえるなら戻り値に.dupつければOKなのですが、@nogcだとそうは行きません。

他にはスマートポインタなんかを使えば中で確保したメモリを関数外に外出しするのも安全かもしれませんが、使い勝手の良さそうなスマートポインタは標準ライブラリでは提供されていません。(std.typecons.RefCountedがそれに近いですが、いかにもライブラリ製作者向けで、汎用としてはあまり使い勝手がよくありません。)

そこで、こういう場合にはOutputRangeを使うとよいでしょう。

import std.range;

// テンプレートなので属性の推論が行われる。 OutputRangeが@nogcならこの関数は@nogcにできる。
void intoMBS(OutputRange)(ref OutputRange dst, string s, uint codePage = 0)
if (isOutputRange!(OutputRange, ubyte))
{
import core.stdc.stdlib;
import core.sys.windows.windows;
// UTF-8をUCS-2に変換
auto wcslen = MultiByteToWideChar(CP_UTF8, 0, s.ptr, s.length, null, 0);
auto wcsbufsize = (wcslen+1)*wchar.sizeof;
auto wcsbuf = (cast(wchar*)malloc(wcsbufsize))[0..wcslen+1];
scope (exit) free(wcsbuf.ptr);
MultiByteToWideChar(CP_UTF8, 0, s.ptr, s.length, wcsbuf.ptr, wcsbufsize);
// UCS-2をcodePageで表現されるマルチバイト文字列に変換
auto mbslen = WideCharToMultiByte(codePage, 0, wcsbuf.ptr, wcslen, null, 0, null, null);
auto mbsbufsize = mbslen+1;
auto mbsbuf = (cast(ubyte*)malloc(mbsbufsize))[0..mbslen+1];
scope (exit) free(mbsbuf.ptr);
WideCharToMultiByte(codePage, 0, wcsbuf.ptr, wcslen, cast(char*)mbsbuf.ptr, mbsbufsize, null, null);
// OutputRangeに出力
put(dst, mbsbuf[0..$-1]);
}

// GCを使わなければ@nogcにできる
@nogc unittest
{
ubyte[512] buf;
auto tmp = buf[];
tmp.intoMBS("あいうえお", 932);
assert(buf[0..$-tmp.length] == "\x82\xA0\x82\xA2\x82\xA4\x82\xA6\x82\xA8");
}

// もちろん、@nogcじゃなくていい場合は普通にappenderとかでもOK
unittest
{
import std.array;
auto app = appender!(string)();
app.intoMBS("あいうえお", 932);
assert(app.data == "\x82\xA0\x82\xA2\x82\xA4\x82\xA6\x82\xA8");
}

// 自作OutputRangeを作ってもいい
struct ArrayOut(T)
{
T[] buf;
void put(T[] x) @nogc { buf[0..x.length] = x[]; buf = buf[x.length..$]; }
void put(T x) @nogc { buf[0] = x; buf = buf[1..$]; }
bool empty() @nogc pure nothrow @safe const @property { return buf.length == 0; }
size_t length() @nogc pure nothrow @safe const @property { return buf.length; }
}

// OutputRangeが@nogcなので、intoMBSも@nogcになれるため、@nogcなunittestで呼べる
@nogc unittest
{
char[512] buf;
auto tmp = ArrayOut!ubyte(cast(ubyte[])buf[]);
tmp.intoMBS("あいうえお", 932);
assert(buf[0..$-tmp.length] == "\x82\xA0\x82\xA2\x82\xA4\x82\xA6\x82\xA8");
}

要するに、std.array.replaceIntoみたいな関数が増えればいいなって、私は思う。


気合で使う

「なんかこの関数は理論的にGCを使わない気がする。」

「この関数のせいで俺の関数が@nogcにならない。」

「この関数、将来的には@nogcになるはずだ。」

@nogcだと検討のためのwritelnもできない……不便だ……。」

そんな人のために黒魔術の紹介

/*******************************************************************************

* 関数の属性を強制変更する黒魔術
*/

auto ref assumeAttr(alias fn, alias attrs, Args...)(auto ref Args args)
if (!is(typeof(fn!Args)) && isCallable!fn)
{
alias Func = SetFunctionAttributes!(typeof(&fn), functionLinkage!fn, attrs);
return (cast(Func)&fn)(args);
}

/// ditto
auto ref assumeAttr(alias fn, alias attrs, Args...)(auto ref Args args)
if (is(typeof(fn!Args)) && isCallable!(fn!Args))
{
alias Func = SetFunctionAttributes!(typeof(&(fn!Args)), functionLinkage!(fn!Args), attrs);
return (cast(Func)&fn!Args)(args);
}

/// ditto
auto assumeAttr(alias attrs, Fn)(Fn t)
if (isFunctionPointer!Fn || isDelegate!Fn)
{
return cast(SetFunctionAttributes!(Fn, functionLinkage!Fn, attrs)) t;
}

/*******************************************************************************
* 関数の属性を得る
*/

template getFunctionAttributes(T...)
{
alias fn = T[0];
static if (T.length == 1 && (isFunctionPointer!(T[0]) || isDelegate!(T[0])))
{
enum getFunctionAttributes = functionAttributes!fn;
}
else static if (!is(typeof(fn!(T[1..$]))))
{
enum getFunctionAttributes = functionAttributes!(fn);
}
else
{
enum getFunctionAttributes = functionAttributes!(fn!(T[1..$]));
}
}

/*******************************************************************************
* 強制的に@nogcにする
*/

auto ref assumeNogc(alias fn, Args...)(auto ref Args args)
{
return assumeAttr!(fn, getFunctionAttributes!(fn, Args) | FunctionAttribute.nogc, Args)(args);
}

/// ditto
auto assumeNogc(T)(T t)
if (isFunctionPointer!T || isDelegate!T)
{
return assumeAttr!(getFunctionAttributes!T | FunctionAttribute.nogc)(t);
}


使いどころの検討

現状の@nogcにまつわる問題が改善した、という前提にたつと、ほかにどんなことができるようになるでしょうか。

先に述べたようなGCと相性の悪い特殊なプログラムでの活用はもちろんですが、他にも例えば次のようなものが考えられます。


  • デストラクタ。これはもともとGCで管理されているメモリは扱ってはいけないことになっていますので、積極的に@nogcをつけるべきでしょう。

  • 大量のメモリを使用し、かつ繰り返し呼ばれる関数。このような関数はGCを頻繁に動かしますので、たとえGCが動いて良くても、@nogcを付けてメモリの使い方を工夫することで速度効率の向上を期待できます。

  • ISR, GCで停止しないスレッド。ここでも語られていますが、GCの管理しないスレッドを立ち上げる場合は、そのエントリーポイントとなる関数は当然@nogcであるべきです。あるスレッドではGCが動かず、他のスレッドではGCが動く……。高度なリアルタイム性が要求されるスレッドや割り込み処理ではGCを動かさないようにするなど、選択できるような仕組みはちょっと面白そうですね。

  • ヒープがない環境の場合。組み込みなんかではよくあることです。そんな時にはmain関数を@nogcにしてしまいましょう。


総括

このように、@nogcというのは、 まぁ、普通は使いません。

しかしながら、システムプログラミング言語としては、GCを動かすことのできないプログラムを作ることも、時として必要な場合があるのです。

特に、最近ではD言語でARMマイコンを動かしたなんていう報告もあるくらいなので、この手の需要は今後広がるのでは無いかと思います。

どうしてもそのようなプログラムを書く必要がある時には、この記事が解決の一助となれば幸いです。

つぎは D言語 Advent Calendar 2014 4日目 @alpha_kai_NET さんです。