vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【C#】Boxing / Unboxing ってどこで使われてるのか調べてみた

はじめに

先日 Effective C# を読んでいたのですが、その中で Boxing / Unboxing (ボックス化 / ボックス化解除)を避けましょう、という話がありました。

Boxing は雑にまとめると int などの値型を Boxing という仕組みを使って object 型にすることで、
参照型として扱えるようにする、ということです( Unboxing は object型から intを取り出す)。

OK。わかりました。値型を object 型の変数に入れないようにします(`・ω・´)ゞ

……( object 型って使わない気がするけど、どこで気を付けたら良いのだろう……?)

ということで、調べてみましたというお話です。

Boxing / Unboxing って何

Boxing / Unboxing をもう少し調べてみます。

  • C# では参照型のすべてのクラスは object クラスを継承しているが、値型の構造体はそうではない。
    これら2種類の値を同様に扱うためにBoxingがある。
  • 値型を参照型と同じように扱いたいとき、元の値を型を持たない参照型のデータにコピーする。
    こうすることで値型のデータを参照型であるかのように扱うことができる。
  • object 型に加えて、 interface 型に変換する場合にも Boxing される( IComparable などジェネリクス版でないもの)
  • Boxing を行うと参照型の変数が新たに生成(コピー)され、またその処理自体も重い(らしい)。
  • Boxing で生成された値はあくまで元の値をコピーしたものであり、別物であることに注意が必要である。
  • なお参照型はデータをヒープ領域へ、値型はデータをスタック領域へとそれぞれメモリーの違った場所に保存されるという違いがある。

ざざっと挙げてみましたが、上記のようなことのようです。

int originalNum = 9;
string originalText = "9";

// Boxingされる.
IComparable sampleComparable = originalNum;
// Boxingされる.
object sampleObject = originalNum;
// Unboxing.
int unboxingNum = (int)sampleObject;

// Boxingされない.
object sampleString = originalText;

誰がobject型を使うのか

さて、Boxing / Unboxing のことが分かったような気になったところで本題。

自分では呼んでいない気がする object 型を、誰が使っているのでしょうか。

通常こういう場合、2つのケースが考えられます。

  1. 昔は存在し、注意が必要だったが、言語の機能改善により見かけることはなくなった
  2. 自分が普段見ないところで使われている

1であれば中の人たちありがとう!めでたしめでたし! という感じなのですが、
2について調べてみることにしました。

string型の場合

例えば string クラスを調べてみます。

すると…

~省略~
public static bool Equals(String a, String b, StringComparison comparisonType);
public static bool Equals(String a, String b);
~省略~

public override bool Equals(object obj);

public bool Equals(String value); public bool Equals(String value, StringComparison comparisonType); ~省略~

ありましたね~、 object 型。

つまり、 string.Equals を使用する場合、引数が string 型の場合は「Equals(String value)」が呼ばれ、
それ以外の型であった場合は 「Equals(object obj)」が呼ばれ、Boxingが発生する、ということですね。

なお今回は string クラスを調べましたが、 値型である float などでも同じように型が異なる場合は object 型として受け取り、
値を確認した後処理を行っています。

Boxingを避けるために

比較などを行う場合、コンパイルエラーが出る・出ないにかかわらず、
前もって型を変換しておくのが良さそうですね。

また今回はあまり取り上げていませんが、IEnumerable ではなく IEnumerable< T> のように、
ジェネリクス版が用意されているものはできるだけそちらを使う、というのも有効です。

int originalNum = 9;
string originalText = "9";
// Boxingされない.
bool result = originalText.Equals( originalNum.ToString());
// Boxingされない.
List< int> nums = new List< int>{ originalNum };

Boxingを見つける

Boxing されているかを調べる方法はないのでしょうか。

実は、IL (中間言語) では Boxing されるときに box と表示されます。

分かりやすい!

なお IL は、 ReSharper を使う場合、メニューのReSharper > Windows > IL Viewer から表示できます。
(事前にビルドしておく必要あり)

f:id:mslGt:20171118131646j:plain

計測する

それでは最後に、遅いと話題の Boxing が本当に遅いか、 Unity の Profiler を使って調べてみます。

計測するコードはこちら。

public Button BoxingButton;
public Button CastButton;

private void Start ()
{
    BoxingButton.onClick.AddListener(() =>
    {
        Profiler.BeginSample("PerformanceSampling Boxing");
        var results = GetResultsWithBoxing();
        Debug.Log(results.Count);
        Profiler.EndSample();
    });
    CastButton.onClick.AddListener(() =>
    {
        Profiler.BeginSample("PerformanceSampling Cast");
        var results = GetResultsWithCasting();
        Debug.Log(results.Count);
        Profiler.EndSample();
    });
}

private List GetResultsWithBoxing()
{
    var results = new List();
    var sampleText = "99";
    var seed = Environment.TickCount;

    for (var i = 0; i <= 100000; i++)
    {
        var random = new System.Random(seed++);
        results.Add(sampleText.Equals(random.Next(1000)));
    }
    return results;
}

private List GetResultsWithCasting()
{
    var results = new List();
    var sampleText = "99";
    var seed = Environment.TickCount;

    for (var i = 0; i <= 100000; i++)
    {
        var random = new System.Random(seed++);
        results.Add(sampleText.Equals(random.Next(1000).ToString()));
    }
    return results;
}

詳しい計測方法はこちら。

で、その結果がこちらです。

f:id:mslGt:20171118131935j:plain

・・・ん? Boxing してる方がむしろ速いんじゃね・・・(。´・ω・)?

どうも、今回のコードでは Boxing にかかるコストより ToString() にかかるコストが大きかったっぽいですね。。

という訳で、やはりパフォーマンスを向上するには計測が必須、ということのようですね(白目)。

おわりに

という訳で、結局何が言いたいのか分からない内容となりましたが、
普段何気なく使っている関数も、どのように実行されるのかをしっかり追いかけるのは重要ですね。

また、パフォーマンス向上のためには、しっかりと計測して対策が有効かどうかを検証するのも大事ですね。

あと、「なんとなく」やっていた処理の裏側をちょっと覗いてみる、というのはとても楽しいものですね(*´ω`)

参照