C#のベンチマークドリブンで同一プロジェクトの性能向上を比較する方法

ある日のこと、MessagePack for C#のTypeless Serializerがふつーのと比べて10倍遅いぞ、というIssueが来た。なるほど遅い。Typelessはあんま乗り気じゃなくて、そもそも実装も私はコンセプト出しただけでフィニッシュまでやったのは他の人で私はプルリクマージしただけだしぃ、とかいうダサい言い訳がなくもないのですが、本筋のラインで使われるものでないとはいえ、実装が乗ってるものが遅いってのは頂けない。直しましょう直しましょう。

速くするのは、コード見りゃあどの辺がネックで手癖だけで何をどうやりゃよくて、どの程度速くなるかはイメージできるんで割とどうでもいいんですが(実際それで8倍高速化した)、とはいえ経過は計測して見ていきたいよね。ってことで、Before, Afterをどう調べていきましょうか、というのが本題。

基本的にはBenchmarkDotNetを使っていきます。詳しい使い方はC#でTypeをキーにしたDictionaryのパフォーマンス比較と最速コードの実装で紹介しているので、そちらを見てくださいね、というわけでベンチマークをセットアップ。

class Program
{
    static void Main(string[] args)
    {
        var switcher = new BenchmarkSwitcher(new[]
        {
            typeof(TypelessSerializeBenchmark),
            typeof(TypelessDeserializeBenchmark),
        });

        switcher.Run(args);
    }
}

internal class BenchmarkConfig : ManualConfig
{
    public BenchmarkConfig()
    {
        Add(MarkdownExporter.GitHub);
        Add(MemoryDiagnoser.Default);

        // ダルいのでShortRunどころか1回, 1回でやる
        Add(Job.ShortRun.With(BenchmarkDotNet.Environments.Platform.X64).WithWarmupCount(1).WithTargetCount(1));
    }
}

[Config(typeof(BenchmarkConfig))]
public class TypelessSerializeBenchmark
{
    private TypelessPrimitiveType TestTypelessComplexType = new TypelessPrimitiveType("John", new TypelessPrimitiveType("John", null));

    [Benchmark]
    public byte[] Serialize()
    {
        return MessagePackSerializer.Serialize(TestTypelessComplexType, TypelessContractlessStandardResolver.Instance);
    }
}

// Deserializeも同じようなコードなので省略。

ベンチマークコードは本体のライブラリからプロジェクト参照によって繋がっています。こんな感じ。

image

というわけで、これでコード書き換えてけば、グングンとパフォーマンスが向上してくことは分かるんですが、これだと値をメモらなきゃダメじゃん。Before, Afterを同列に比較したいじゃん、という至極当然の欲求が生まれるのであった。そうじゃないと面倒くさいし。

2つのアセンブリ参照

古いバージョンをReleaseビルドでビルドしちゃって、そちらはDLLとして参照しちゃいましょう。とやると、うまくいきません。

image

同一アセンブリ名のものは2つ参照できないからです。ということで、どうするかといったら、まぁプロジェクトは自分自身で持ってるので、ここはシンプルにアセンブリ名だけ変えたものをビルドしましょう。

image

これを参照してやれば、一旦はOK。

extern alias

2つ、同じMessagePackライブラリが参照できたわけですが、今度はコード上でそれを使い分けられなければなりません。そのままでは出し分けできないので(同一ネームスペース、同一クラス名ですからね!)、次にaliasを設定します。

image

対象アセンブリのプロパティで、Aliasesのところに任意のエイリアスをつけます。今回は1_4_4にはoldmsgpack, プロジェクト参照している最新のものにはnewmsgpackとつけてみました。

あとはコード上で、extern aliasとoldmsgpack::といった::によるフル修飾で、共存した指定が可能です。

// 最上段でextern aliasを指定
extern alias oldmsgpack;
extern alias newmsgpack;

[Config(typeof(BenchmarkConfig))]
public class TypelessSerializeBenchmark
{
    private TypelessPrimitiveType TestTypelessComplexType = new TypelessPrimitiveType("John", new TypelessPrimitiveType("John", null));

    [Benchmark]
    public byte[] OldSerialize()
    {
        // フル修飾で書かなきゃいけないのがダルい
        return oldmsgpack::MessagePack.MessagePackSerializer.Serialize(TestTypelessComplexType, oldmsgpack::MessagePack.Resolvers.TypelessContractlessStandardResolver.Instance);
    }
   
    [Benchmark(Baseline = true)]
    public byte[] NewSerialize()
    {
        return newmsgpack::MessagePack.MessagePackSerializer.Serialize(TestTypelessComplexType, newmsgpack::MessagePack.Resolvers.TypelessContractlessStandardResolver.Instance);
    }
}

これで完成。実行すれば

image

最終的に、以前と比較して9倍ほど速くなりました。実際には、何度か実行していって、速くなったことを確認しながらやっています。

クソ遅かったのね!って話なのですが、Typelessは実際クソ遅かったのですが、それ以外の普通のは普通にちゃんと速かったので、一応、大丈夫です、はい、あくまでTypelessだけです、すみません……。

まとめ

ある程度完成している状態になっているならば、ベンチマークドリブンデベロップメントは割とかなり効果的ですね。改善はまずは計測から、とかいっても、結局、その数値が速いのか遅いのかの肌感覚がないとクソほども役に立たないわけですが(ただたんに漠然と眺めるだけの計測には本当に何の意味もないし、数値についての肌感覚を持っているかいないかの経験値は、ツールが充実している今でもなお重要だと思います。肌感覚に繋げていくことを意識して、経験を積みましょう)、さすがにBefore, Afterだととてもわかりやすくて、導入としてもいい感じです。

MessagePack for C#は、昨日ver 1.5.0を出しまして、最速モード(Object-Array)以外の部分(Object-Map)でも性能的にかなり向上したのと、object型のシリアライズがみんなの想像する通りのシリアライズをしてくれるようにようやくなりまして、本気で死角なし、になりました。Typelessの性能向上は次のアップデート。それと、もう一つ大型の機能追加(とても役に立ちます!特にUnityで!)を予定しているので、まだまだ良くなっていきますので期待しといてください。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(C#)
April 2011
|
July 2024

Twitter:@neuecc GitHub:neuecc

Archive