【Unity】MasterMemoryの文字列自動インターン化の挙動を確認する

UnityでMasterMemoryの文字列自動インターン化の挙動を確認しました。

MasterMemory 2.4.4

はじめに

MasterMemoryには、マスタデータに同じ文字列がある場合にそれらを同じアドレスに配置することでメモリを節約する自動インターン化という仕組みがあります。

tech.cygames.co.jp

本記事ではこの挙動を実際にMasterMemoryのバイナリをビルドして確認してみます。

なお、MasterMemoryの基本的な使い方は以下の記事にまとめています。
本記事ではこれを前提知識としますので、必要に応じて参照してください。

light11.hatenadiary.com

挙動確認用のマスタデータクラスを作る

自動インターン化の挙動を確認するために、まずは挙動確認用のマスタデータクラスを作成します。

using MasterMemory;
using MessagePack;

[MemoryTable("Stage")]
[MessagePackObject(true)]
public sealed class StageMaster
{
    public StageMaster(string id, string name, string stageGroupName, string[] tags)
    {
        Id = id;
        Name = name;
        StageGroupName = stageGroupName;
        Tags = tags;
    }

    [PrimaryKey]
    public string Id { get; }

    public string Name { get; }

    public string StageGroupName { get; }

    public string[] Tags { get; }
}

いくつかの文字列と、文字列の配列を持つクラスを作成しました。

MessagePackとMasterMemoryのコード生成をする

次にMessagePackとMasterMemoryのコード生成を行います。

dotnet-mmgen -inputDirectory . -outputDirectory ./Generated -usingNamespace "Example"
mpc -input . -output ./Generated

MasterMemoryのバイナリをビルドする

次に適当なデータを入れた MasterMemory のバイナリをビルドします。

using System.IO;
using Example;
using MessagePack;
using MessagePack.Resolvers;
using UnityEditor;

public static class StageMasterBinaryBuilder
{
    [MenuItem("Example/Generate Binary")]
    private static void Run()
    {
        var messagePackResolvers = CompositeResolver.Create(
            MasterMemoryResolver.Instance,
            GeneratedResolver.Instance,
            StandardResolver.Instance
        );
        var options = MessagePackSerializerOptions.Standard.WithResolver(messagePackResolvers);
        MessagePackSerializer.DefaultOptions = options;

        var stageMasters = new StageMaster[]
        {
            new("stage-01-001", "初心者の森", "エリア1", new[] { "森", "初級" }),
            new("stage-01-002", "迷いの湿地帯", "エリア1", new[] { "湿地", "初級" }),
            new("stage-01-003", "炎の山脈", "エリア1", new[] { "山", "初級" }),
            new("stage-02-001", "氷結の洞窟", "エリア2", new[] { "洞窟", "中級" }),
            new("stage-02-002", "幽霊の墓地", "エリア2", new[] { "墓地", "中級" }),
            new("stage-02-003", "竜の城塞", "エリア2", new[] { "城", "中級" }),
        };

        var databaseBuilder = new DatabaseBuilder();
        databaseBuilder.Append(stageMasters);
        var binary = databaseBuilder.Build();

        var path = "Assets/MasterMemoryInterningTest/StageMaster.bytes";
        var directory = Path.GetDirectoryName(path);
        if (!Directory.Exists(directory))
            Directory.CreateDirectory(directory);
        File.WriteAllBytes(path, binary);
        AssetDatabase.Refresh();
    }
}

バイナリをロードしてインターン化を確認する

さて、これで準備が終わったので前節でビルドした MasterMemory のバイナリをロードして、インターン化されているか確認します。

using System;
using Example;
using MessagePack;
using MessagePack.Resolvers;
using UnityEditor;
using UnityEngine;

public static class BinaryLoader
{
    [MenuItem("Example/Load Binary")]
    private static void Run()
    {
        var messagePackResolvers = CompositeResolver.Create(
            MasterMemoryResolver.Instance,
            GeneratedResolver.Instance,
            StandardResolver.Instance
        );
        var options = MessagePackSerializerOptions.Standard.WithResolver(messagePackResolvers);
        MessagePackSerializer.DefaultOptions = options;

        var path = "Assets/MasterMemoryInterningTest/StageMaster.bytes";
        var asset = AssetDatabase.LoadAssetAtPath<TextAsset>(path);
        var binary = asset.bytes;

        var memoryDatabase = new MemoryDatabase(binary);
        var stage1 = memoryDatabase.StageMasterTable.FindById("stage-01-001");
        var stage2 = memoryDatabase.StageMasterTable.FindById("stage-01-002");
        var stage3 = memoryDatabase.StageMasterTable.FindById("stage-01-003");

        unsafe
        {
            // 同じ文字列が保存されている変数のポインタを取得
            fixed (void* p1 = stage1.StageGroupName)
            fixed (void* p2 = stage2.StageGroupName)
            fixed (void* p3 = stage3.StageGroupName)
            {
                // アドレスが同じことを確認
                Debug.Log((IntPtr)p1); // 16891223796
                Debug.Log((IntPtr)p2); // 16891223796
                Debug.Log((IntPtr)p3); // 16891223796
            }
        }
    }
}

結果はコメントに記述しました。
コメントの通り同じ文字列のポインタが同じアドレスを指していることを確認できました。

string配列の要素はインターン化されない

次に、string配列の各要素がインターン化されるかを調べてみます。

using System;
using Example;
using MessagePack;
using MessagePack.Resolvers;
using UnityEditor;
using UnityEngine;

public static class BinaryLoader
{
    [MenuItem("Example/Load Binary")]
    private static void Run()
    {
        var messagePackResolvers = CompositeResolver.Create(
            MasterMemoryResolver.Instance,
            GeneratedResolver.Instance,
            StandardResolver.Instance
        );
        var options = MessagePackSerializerOptions.Standard.WithResolver(messagePackResolvers);
        MessagePackSerializer.DefaultOptions = options;

        var path = "Assets/MasterMemoryInterningTest/StageMaster.bytes";
        var asset = AssetDatabase.LoadAssetAtPath<TextAsset>(path);
        var binary = asset.bytes;

        var memoryDatabase = new MemoryDatabase(binary);
        var stage4 = memoryDatabase.StageMasterTable.FindById("stage-02-001");
        var stage5 = memoryDatabase.StageMasterTable.FindById("stage-02-002");
        var stage6 = memoryDatabase.StageMasterTable.FindById("stage-02-003");

        unsafe
        {
            // 同じ文字列が保存されている変数のポインタを取得
            fixed (void* p1 = stage4.Tags[1])
            fixed (void* p2 = stage5.Tags[1])
            fixed (void* p3 = stage6.Tags[1])
            {
                // アドレスが異なることを確認
                Debug.Log((IntPtr)p1); // 15144682964
                Debug.Log((IntPtr)p2); // 15144682868
                Debug.Log((IntPtr)p3); // 15144682740
            }
        }
    }
}

string配列の各要素は同じ文字列であってもアドレスが異なり、インターン化されないことを確認できました。

配列要素をクラスにすればインターン化される

配列要素が文字列だとインターン化されないので、最後に、配列の要素を文字列型のプロパティを持つクラスの配列にしてみます。

まずマスタデータクラス定義です。ここまでの内容に対して、コメントの部分に変更を加えています。

using MasterMemory;
using MessagePack;

[MemoryTable("Stage")]
[MessagePackObject(true)]
public sealed class StageMaster
{
    public StageMaster(string id, string name, string stageGroupName, Tag[] tags)
    {
        Id = id;
        Name = name;
        StageGroupName = stageGroupName;
        Tags = tags;
    }

    [PrimaryKey]
    public string Id { get; }

    public string Name { get; }

    public string StageGroupName { get; }

    // 文字列からクラスに変更
    public Tag[] Tags { get; }
}

[MessagePackObject(true)]
public sealed class Tag
{
    public string Name { get; }

    public Tag(string name)
    {
        Name = name;
    }
}

次にバイナリビルド用スクリプトです。こちらもマスタデータクラスの変更に伴い少し修正しています。

using System.IO;
using Example;
using MessagePack;
using MessagePack.Resolvers;
using UnityEditor;

public static class StageMasterBinaryBuilder
{
    [MenuItem("Example/Generate Binary")]
    private static void Run()
    {
        var messagePackResolvers = CompositeResolver.Create(
            MasterMemoryResolver.Instance,
            GeneratedResolver.Instance,
            StandardResolver.Instance
        );
        var options = MessagePackSerializerOptions.Standard.WithResolver(messagePackResolvers);
        MessagePackSerializer.DefaultOptions = options;

        var stageMasters = new StageMaster[]
        {
            new("stage-01-001", "初心者の森", "エリア1", new[] { new Tag("森"), new Tag("初級") }),
            new("stage-01-002", "迷いの湿地帯", "エリア1", new[] { new Tag("湿地"), new Tag("初級") }),
            new("stage-01-003", "炎の山脈", "エリア1", new[] { new Tag("山"), new Tag("初級") }),
            new("stage-02-001", "氷結の洞窟", "エリア2", new[] { new Tag("洞窟"), new Tag("中級") }),
            new("stage-02-002", "幽霊の墓地", "エリア2", new[] { new Tag("墓地"), new Tag("中級") }),
            new("stage-02-003", "竜の城塞", "エリア2", new[] { new Tag("城"), new Tag("中級") }),
        };

        var databaseBuilder = new DatabaseBuilder();
        databaseBuilder.Append(stageMasters);
        var binary = databaseBuilder.Build();

        var path = "Assets/MasterMemoryInterningTest/StageMaster.bytes";
        var directory = Path.GetDirectoryName(path);
        if (!Directory.Exists(directory))
            Directory.CreateDirectory(directory);
        File.WriteAllBytes(path, binary);
        AssetDatabase.Refresh();
    }
}

最後にロードして挙動を確認します。

using System;
using Example;
using MessagePack;
using MessagePack.Resolvers;
using UnityEditor;
using UnityEngine;

public static class BinaryLoader
{
    [MenuItem("Example/Load Binary")]
    private static void Run()
    {
        var messagePackResolvers = CompositeResolver.Create(
            MasterMemoryResolver.Instance,
            GeneratedResolver.Instance,
            StandardResolver.Instance
        );
        var options = MessagePackSerializerOptions.Standard.WithResolver(messagePackResolvers);
        MessagePackSerializer.DefaultOptions = options;

        var path = "Assets/MasterMemoryInterningTest/StageMaster.bytes";
        var asset = AssetDatabase.LoadAssetAtPath<TextAsset>(path);
        var binary = asset.bytes;

        var memoryDatabase = new MemoryDatabase(binary);
        var stage4 = memoryDatabase.StageMasterTable.FindById("stage-02-001");
        var stage5 = memoryDatabase.StageMasterTable.FindById("stage-02-002");
        var stage6 = memoryDatabase.StageMasterTable.FindById("stage-02-003");

        unsafe
        {
            // 同じ文字列が保存されている変数のポインタを取得
            fixed (void* p1 = stage4.Tags[1].Name)
            fixed (void* p2 = stage5.Tags[1].Name)
            fixed (void* p3 = stage6.Tags[1].Name)
            {
                // アドレスが一致することを確認
                Debug.Log((IntPtr)p1); // 16881088084
                Debug.Log((IntPtr)p2); // 16881088084
                Debug.Log((IntPtr)p3); // 16881088084
            }
        }
    }
}

このケースではアドレスが一致してインターン化されていることを確認できました。

関連

light11.hatenadiary.com

参考

tech.cygames.co.jp

engineering.grani.jp