KENTEM TechBlog

建設業のDXを実現するKENTEMの技術ブログです。

パフォーマンス検証:バイナリファイル vs テキストファイルの速度比較

はじめに

こんにちは。 快測Scanという製品の開発に携わってるプログラマです。
快測Scanは、手軽に点群データを取得、確認ができるモバイルアプリです。

点群をUnityEditor上で表示しようとすると実機に比べて処理が非常に遅く、ストレスに感じる場面がありました。

原因の一つとして、データをテキストファイルから読み込んでいることが挙げられます。
そこで、テキストファイルとバイナリファイルからの読み込みで、どれほどの差がでるのか検証してみることにしました。

本記事では以下について検証します:

  • UnityEditor上でのテキストファイルとバイナリファイルの読み込み速度の比較

なぜテキストファイル読み込みは遅い?

そもそもテキストファイル読み込みが遅くなる原因はなんでしょうか?
主には、テキストファイルの読み込み時に必要となる 「Split」 や「Parse」 処理に時間がかかるためです。
バイナリファイルから読み込めば、これらの処理は必要なくなるため高速化できるというわけです。

検証

環境

Mac mini 2023 (M2)
メモリ: 16GB
OS: macOS Sequoia 15.5
Unityバージョン: 6000.0.34f1

準備

まずは仮想の点群データを作成します。
点一つは以下のような情報を保持することとします。座標はfloat型、色はbyte型です。
x, y, z, r, g, b
これをランダムに作成し、テキストファイル、バイナリファイルそれぞれに保存します。
以下検証で使用したコードです。
(検証のため、バリデーションなどを行っていないのはご愛嬌)

点を表すクラス

public class PointData
{
    public PointData(float x, float y, float z, byte r, byte g, byte b)
    {
        X = x; ; Y = y; Z = z;
        R = r; G = g; B = b;
    }
    public float X { get; }
    public float Y { get; }
    public float Z { get; }

    public byte R { get; }
    public byte G { get; }
    public byte B { get; }
}

ランダムな点群を生成するクラス

public static class PointCloudGenerator
{
    public static List<PointData> GenerateRandomPointCloud(int pointCount)
    {
        var random = new Random();
        var pointCloud = new List<PointData>();
        for (int i = 0; i < pointCount; i++)
        {
            pointCloud.Add(GenerateRandomPoint(random));
        }

        return pointCloud;
    }

    private static PointData GenerateRandomPoint(Random random)
    {
        var x = (float)(random.NextDouble() * 1000);
        var y = (float)(random.NextDouble() * 1000);
        var z = (float)(random.NextDouble() * 1000);
        var r = (byte)random.Next(0, 256);
        var g = (byte)random.Next(0, 256);
        var b = (byte)random.Next(0, 256);

        return new PointData(x, y, z, r, g, b);
    }
}

テキスト、バイナリファイルへ書き込むクラス

public static class PointCloudSaver
{
    public static void SaveToTextFile(IEnumerable<PointData> pointCloud, string filePath)
    {
        using (StreamWriter write = new StreamWriter(filePath))
        {
            foreach (var point in pointCloud)
            {
                write.WriteLine($"{point.X} {point.Y} {point.Z} {point.R} {point.G} {point.B}");
            }
        }
    }

    public static void SaveToBinaryFile(IEnumerable<PointData> pointCloud, string filePath)
    {
        using (BinaryWriter writer = new BinaryWriter(File.Open(filePath, FileMode.Create)))
        {
            foreach (var point in pointCloud)
            {
                writer.Write(point.X);
                writer.Write(point.Y);
                writer.Write(point.Z);
                writer.Write(point.R);
                writer.Write(point.G);
                writer.Write(point.B);
            }
        }
    }
}

読み込み処理

作成したそれぞれのファイルを読み込みます。
読み込みでは、ファイルからデータを読み込み、PointDataをインスタンス化するまで行います。(点の描画などは行いません)

public static class PointCloudLoader
{
    public static void LoadTextFile(string textPath)
    {
        using (StreamReader reader = new StreamReader(textPath, Encoding.GetEncoding("UTF-8")))
        {
            string line;
            while ((line = reader.ReadLine()) != null)
            {
                string[] parts = line.Split(' ');

                if (parts.Length < 6)
                {
                    continue;
                }

                float x = float.Parse(parts[0]);
                float y = float.Parse(parts[1]);
                float z = float.Parse(parts[2]);
                byte r = byte.Parse(parts[3]);
                byte g = byte.Parse(parts[4]);
                byte b = byte.Parse(parts[5]);

                new PointData(x, y, z, r, g, b);
            }
        }
    }

    public static void LoadBinaryFile(string binaryPath)
    {
        using (BinaryReader reader = new BinaryReader(File.Open(binaryPath, FileMode.Open)))
        {
            while (reader.BaseStream.Position + 15 < reader.BaseStream.Length)
            {
                float x = reader.ReadSingle();
                float y = reader.ReadSingle();
                float z = reader.ReadSingle();
                byte r = reader.ReadByte();
                byte g = reader.ReadByte();
                byte b = reader.ReadByte();

                new PointData(x, y, z, r, g, b);
            }
        }
    }
}

計測

private double BenchmarkLoadTextFile()
{
    var stopwatch = new Stopwatch();
    stopwatch.Start();
    PointCloudLoader.LoadTextFile(_textFilePath);
    stopwatch.Stop();
    return stopwatch.ElapsedMilliseconds;
}

private double BenchmarkLoadBinaryFile()
{
    var stopwatch = new Stopwatch();
    stopwatch.Start();
    PointCloudLoader.LoadBinaryFile(_binaryFilePath);
    stopwatch.Stop();
    return stopwatch.ElapsedMilliseconds;
}

結果

速度の検証は、点数100万点、1000万点でそれぞれ行いました。
以下に示す結果は、読み込みを10回繰り返し、平均を算出したものです。

100万点

テキストファイル バイナリファイル
1.6514秒 0.6747秒

バイナリファイルの方が約2.5倍高速

1000万点

テキストファイル バイナリファイル
17.0531秒 6.7276秒

バイナリファイルの方が約2.5倍高速

まとめ

今回の検証で、テキストファイルとバイナリファイルの読み込み速度にどれほどの差があるかを検証し、想像以上にSplitやParse処理に時間がかかっているかを認識できました。

実際はメッシュの作成やレンダリング処理なども含まれるため、ここまでの差がそのまま適用されるとは限りません。実行環境によって変わる可能性もあります。
しかし、ファイル形式の選択がパフォーマンスに大きく影響することが改めて確認できました。

補足として

今回は読み込み速度に注目しましたが、バイナリファイルは他にもメリットがあります。

  • データ書き込み時の速度(1000万点)
テキストファイル バイナリファイル
28.474秒 3.526秒

約8倍の差

  • 作成されたファイルサイズ(1000万点)
テキストファイル バイナリファイル
373.8MB 150MB

約2.5倍の差

書き込み処理はより大きな差が開いています。
こうした結果を見ると、サイズが大きく、構造化されたデータであればバイナリファイルを選択しない手はないですね。

おわりに

KENTEMでは、様々な拠点でエンジニアを大募集しています! 建設×ITにご興味頂いた方は、是非下記のリンクからご応募ください。 recruit.kentem.jp career.kentem.jp