Swanman's Horizon

性欲をもてあましつつなんらかの話をするよ。

ガード節を使ってはいけない?GetTypeKindの使い方。

GetTypeKindとは

GetTypeKindはXE7で新しく実装された組み込み関数で、この関数に型を渡すとTTypeKind型の値が返ってきます。たったそれだけの関数です。
もちろん今までもTTypeKindを取得することはGetTypeKindを使わなくてもできました。TypeInfo関数です。この関数に型を渡すとPTypeInfo型の値を取得でき、そのフィールドであるKindがTTypeKindでした。
TTypeKind型をご存じない方に説明すると、Delphiの型をざっくりと20種類ほどに分類した列挙型で、中身としてはtkIntegerだとか、tkClass、tkProcedureといったものがあり、これで渡された型が何なのかを判別できます。

TTypeKindの必要性

型がどういった型なのか、というのはどうでもいい話にも思えますが、これが真価を発揮するのはジェネリックプログラミングにおいてです*1
例えばTListというクラスを例に挙げます。このクラスは型パラメータとしてTを取り、そのリストを実装するクラスです。このクラスの実装時にはTが何の型なのかは分からず、クラスが使用される時、つまりTに具体的な型を指定する時になって初めて型が判明します。
ここでTTypeKindが必要になります。実装時に型がどういったものなのか分からないのであれば、それを実行時に調べれば良いわけです。

コンパイル時と実行時

話を少し前に戻します。型からTTypeKindを得たい場合、XE6以前ではTypeInfo関数を使用するということは前述の通りですが、これは実行時に行われます。実行時に行われるということは、すなわち余計な処理が入ってしまうことにもなります。次のコードを見て下さい。

function TFoo<T>.GetSize: Integer;
begin
  case PTypeInfo(TypeInfo(T))^.Kind of
    tkInteger, tkInt64: Result := SizeOf(T);
    else Result := 0;
  end;
end;

このコードはTが整数型であればそのサイズを返し、それ以外では0を返すという単純な処理です。しかしこの処理が実際に走る時、TにはIntegerやstring等何らかの型が指定されており、その型に応じて必ずひとつの分岐先にしか分岐しないにもかかわらず、実行時に分岐が行われるという無駄な処理が発生してしまいます。

GetTypeKindの真価

GetTypeKindは組み込み関数で、コンパイル時に解決されます。GetTypeKind(Integer)はコンパイル時にtkIntegerに置き換えられるわけです。これを上記の処理に適用するとどうなるか見てみます。

function TFoo<T>.GetSize: Integer;
begin
  case GetTypeKind(T) of
    tkInteger, tkInt64: Result := SizeOf(T);
    else Result := 0;
  end;
end;

まず単純に置き換えました。これだけではTypeInfoを使ったものと何の変わりも無いように見えます。しかしこれがコンパイルされるとどうなるでしょうか。
例えばTにIntegerを指定した場合を考えてみます。

  case GetTypeKind(Integer) of
    tkInteger, tkInt64: Result := SizeOf(Integer);
    else Result := 0;
  end;

Tに型が指定されたので、TがIntegerに展開されます。そして「GetTypeKind(Integer)はコンパイル時にtkIntegerに置き換えられる」という言葉を思い出してみて下さい。するとこうなります。

  case tkInteger of
    tkInteger, tkInt64: Result := SizeOf(Integer);
    else Result := 0;
  end;

このコード、絶対にtkInteger以外の部分には分岐しませんよね?なので最適化により分岐外のコードがコンパイラによって削除され、最終的にこうなります。

  Result := SizeOf(Integer);

このようにTypeInfo関数を使っていた時と比べてシンプルになっていることが分かります。実際に使われる際は分岐がもっと大規模になったり、その中に書かれるコードもより多くなります。その時に無駄な処理が最適化で消えるということは、処理速度の向上やEXEサイズの減量にも大きく影響することになります。

if文やwhile文などでも同じ

もちろんcase文に限った話ではなく、絶対に実行されないことが分かっているような処理はコンパイル時に最適化されます。

procedure TFoo<T>.PrintTypeName;
begin
  if GetTypeKind(T) = tkInteger then
    Writeln('Integer')
  else if GetTypeKind(T) = tkUString then
    Writeln('UnicodeString')
  else
    Writeln('Other type');
end;

例えば上記のようなコードでTにstringを指定した場合、他の分岐は消えて無くなるので次のようなコードと同等に最適化されます。

begin
  Writeln('UnicodeString')
end;

そしてガード節

ようやくタイトルにあるガード節の話ですが、先にガード節自体を簡単に説明すると、if文のネストが深くならないよう、先にExitやContinueで抜けてしまうような書き方のことです。

procedure TFoo<T>.PrintTypeName;
begin
  if GetTypeKind(T) <> tkInteger then // <- ガード節
    Exit;

  Writeln('Integer')
end;

ところがこのガード節とGetTypeKindを使った最適化の相性があまり良くありません。上記の例でTにstringを渡した場合、こうなってしまいます。

begin
  Exit;

  Writeln('Integer')
end;

最適化でifが消えてそのままExitで抜けることになるため、速度的なデメリットはありません。しかし絶対に実行されないはずのWritelnが実行ファイル内に埋め込まれてしまいます。そのため、最適化を期待する場合は面倒ですがガード節はあまり使わない方が良さそうです。

*1:もちろんそれだけじゃなく、Delphiジェネリクスが実装される遙か以前からIDEが活用しまくってますが