2019-03-25に更新

文字列連結処理を比較してみた

読了目安:13分

はじめに

Unityでの文字列連結のパフォーマンスについては、いつもお世話になっているコガネブログさんがたびたび記事を上げてくださっています。
ただ、いろんな方法があるので結局どれを使えばいいのか分からないな~となることが度々ありました。
そこで、今回は自分が見たことある、思いつく文字列連結の処理の速度とGCを比較してみました。

比較内容

比べた文字連結方法は以下の通りです。

エントリーNo.1:+

string s = "a" + "b";

一番ノーマルなパターンです。

エントリーNo.2:StringBuilder(New)

StringBuilder builder = new StringBuilder();
builder.Append(”a”).Append("b");
string s = builder.ToString();

毎回StringBuilderのインスタンスを作成します。

エントリーNo.3:StringBuilder(Repeat)

StringBuilder builder = new StringBuilder();
for(int i = 0; i < n; ++i)
{
builder.Clear();
builder.Append(”a”).Append("b");
string s = builder.ToString();
}

あらかじめStringBuilderのインスタンスを作成しておきます。

エントリーNo.4:Concat

string s = string.Concat("a", "b");

string.Concatで連結します。

エントリーNo.5:Format

string s = string.Format("{0}{1}", "a", "b");

string.Formatで連結します。

エントリーNo.6:$""

string a = "a", b = "b";
string s = $"{a}{b}";

C#6以降の文字列補完で連結します。

エントリーNo.7:StringBuilderTemporary

string str = StrOpe.i +"a" + "b";

こちらのブログの方法です。
【Unity】string の連結を StringBuilder に置き換えてパフォーマンスを改善できる「StringBuilderTemporary」紹介

エントリーNo.8:FastString(New)

FastString fast = new FastString(64);
fast.Append("a").Append("b");
string str = fast.ToString();

こちらのブログの方法です。毎回インスタンスを作成します。
【Unity】string や StringBuilder よりもメモリ割り当てが少なく高速な文字列クラス「FastString」紹介

エントリーNo.9:FastString(Repeat)

FastString fast = new FastString(64);
for(int i = 0; i < n; ++i)
{
fast.Clear();
fast.Append(”a”).Append("b");
string s = fast.ToString();
}

No.8の方法で、あらかじめインスタンスを生成しておきます。

エントリーNo.10:UtilsFormat

string str = StringUtils.Format("{0}{1}", "a", "b");

こちらのブログの方法です。
【Unity】ボックス化をなるべく回避して GC の発生を抑える string.Format「StringUtils」

パターン1:10個の文字列連結をN回繰り返す

検証コード

using System;
using System.Text;
using UnityEngine;
using UnityEngine.Profiling;

using StrOpe = StringOperationUtil.OptimizedStringOperation;

public class Test : MonoBehaviour
{
    const int repeat = 10000;
    const string tmp1 = "a";
    const string tmp2 = "b";
    const string tmp3 = "c";
    const string tmp4 = "d";
    const string tmp5 = "e";
    const string tmp6 = "f";
    const string tmp7 = "g";
    const string tmp8 = "h";
    const string tmp9 = "i";
    const string tmp10 = "j";

    StringBuilder _builder;
    FastString _fast = new FastString(64);

    void Start()
    {
        _builder = new StringBuilder();

        PlayTest(Plus, "+");
        PlayTest(StringBuilderNew, "StringBuilder(New)");
        PlayTest(StringBuilderRepeat, "StringBuilder(Repeat)");
        PlayTest(Concat, "Concat");
        PlayTest(Format, "Format");
        PlayTest(Interpolation, "Interpolation");
        PlayTest(StringBuilderTemporary, "StringBuilderTemporary");
        PlayTest(FastStringNew, "FastString(New)");
        PlayTest(FastStringRepeat, "FastString(Repeat)");
        PlayTest(UtilsFormat, "UtilsFormat");
    }

    void PlayTest(Func<string> func, string title)
    {
        Profiler.BeginSample(title);

        string s = string.Empty;
        for (int i = 0; i < repeat; ++i)
        {
            s = func();
        }

        Profiler.EndSample();
    }

    string Plus()
    {
        string str = tmp1 + tmp2 + tmp3 + tmp4 + tmp5 + tmp6 + tmp7 + tmp8 + tmp9;
        return str;
    }

    string StringBuilderNew()
    {
        StringBuilder builder = new StringBuilder();
        builder.Append(tmp1).Append(tmp2).Append(tmp3).Append(tmp4).Append(tmp5).Append(tmp6).Append(tmp7).Append(tmp8).Append(tmp9);
        string str = builder.ToString();

        return str;
    }

    string StringBuilderRepeat()
    {
        _builder.Clear();
        _builder.Append(tmp1).Append(tmp2).Append(tmp3).Append(tmp4).Append(tmp5).Append(tmp6).Append(tmp7).Append(tmp8).Append(tmp9);
        string str = _builder.ToString();

        return str;
    }

    string Concat()
    {
        string str = string.Concat(tmp1, tmp2, tmp3, tmp4, tmp5, tmp6, tmp7, tmp8, tmp9);
        return str;
    }

    string Format()
    {
        string str = string.Format("{0}{1}{2}{3}{4}{5}{6}{7}{8}", tmp1, tmp2, tmp3, tmp4, tmp5, tmp6, tmp7, tmp8, tmp9);
        return str;
    }

    string Interpolation()
    {
        string str = $"{tmp1}{tmp2}{tmp3}{tmp4}{tmp5}{tmp6}{tmp7}{tmp8}{tmp9}";
        return str;
    }

    string StringBuilderTemporary()
    {
        string str = StrOpe.i + tmp1 + tmp2 + tmp3 + tmp4 + tmp5 + tmp6 + tmp7 + tmp8 + tmp9;
        return str;
    }

    string FastStringNew()
    {
        FastString fast = new FastString(64);
        fast.Append(tmp1).Append(tmp2).Append(tmp3).Append(tmp4).Append(tmp5).Append(tmp6).Append(tmp7).Append(tmp8).Append(tmp9);
        string str = fast.ToString();

        return str;
    }

    string FastStringRepeat()
    {
        _fast.Clear();
        _fast.Append(tmp1).Append(tmp2).Append(tmp3).Append(tmp4).Append(tmp5).Append(tmp6).Append(tmp7).Append(tmp8).Append(tmp9);
        string str = _fast.ToString();

        return str;
    }

    string UtilsFormat()
    {
        string str = StringUtils.Format("{0}{1}{2}{3}{4}{5}{6}{7}{8}", tmp1, tmp2, tmp3, tmp4, tmp5, tmp6, tmp7, tmp8, tmp9);
        return str;
    }

検証結果

N=10

image.png

N=100

image.png

N=1000

image.png

N=10000

image.png

N=100000

image.png

「+」「$""] が速度、GC両方ともに優れているという結果になりました。なんで?
どのサイトを見ても、+で連結するよりStringBuilder使った方がいいよって書いてあるのになんでこうなった?

パターン2:N個の文字列を連結する

検証コード

using System;
using System.Text;
using UnityEngine;
using UnityEngine.Profiling;

using StrOpe = StringOperationUtil.OptimizedStringOperation;

public class Test : MonoBehaviour
{
    const int repeat = 100000;
    const string tmp1 = "a";
    const string tmp2 = "b";
    const string tmp3 = "c";
    const string tmp4 = "d";
    const string tmp5 = "e";
    const string tmp6 = "f";
    const string tmp7 = "g";
    const string tmp8 = "h";
    const string tmp9 = "i";
    const string tmp10 = "j";

    StringBuilder _builder;
    FastString _fast = new FastString(repeat + 1);

    void Start()
    {
        _builder = new StringBuilder();

        PlayTest(Plus, "+");
        PlayTest(StringBuilderNew, "StringBuilder(New)");
        PlayTest(StringBuilderRepeat, "StringBuilder(Repeat)");
        PlayTest(Concat, "Concat");
        PlayTest(Format, "Format");
        PlayTest(Interpolation, "$\"\"");
        PlayTest(StringBuilderTemporary, "StringBuilderTemporary");
        PlayTest(FastStringNew, "FastString(New)");
        PlayTest(FastStringRepeat, "FastString(Repeat)");
        PlayTest(UtilsFormat, "UtilsFormat");
    }

    void PlayTest(Func<string> func, string title)
    {
        Profiler.BeginSample(title);

        string s = func();

        Profiler.EndSample();
    }

    string Plus()
    {
        string str = string.Empty;

        for (int i = 0; i < repeat; ++i)
        {
            str += tmp1;
        }

        return str;
    }

    string StringBuilderNew()
    {
        StringBuilder builder = new StringBuilder();

        for (int i = 0; i < repeat; ++i)
        {
            builder.Append(tmp1);
        }

        string str = builder.ToString();

        return str;
    }

    string StringBuilderRepeat()
    {
        _builder.Clear();

        for (int i = 0; i < repeat; ++i)
        {
            _builder.Append(tmp1);
        }

        string str = _builder.ToString();

        return str;
    }

    string Concat()
    {
        string str = string.Empty;

        for (int i = 0; i < repeat; ++i)
        {
            str = string.Concat(str, tmp1);
        }

        return str;
    }

    string Format()
    {
        string str = string.Empty;

        for (int i = 0; i < repeat; ++i)
        {
            str = string.Format("{0}{1}", str, tmp1);
        }

        return str;
    }

    string Interpolation()
    {
        string str = string.Empty;

        for (int i = 0; i < repeat; ++i)
        {
            str = $"{str}{tmp1}";
        }

        return str;
    }

    string StringBuilderTemporary()
    {
        string str = string.Empty;

        for (int i = 0; i < repeat; ++i)
        {
            str = StrOpe.i + str + tmp1;
        }

        return str;
    }

    string FastStringNew()
    {
        FastString fast = new FastString(repeat + 1);

        for (int i = 0; i < repeat; ++i)
        {
            fast.Append(tmp1);
        }

        string str = fast.ToString();

        return str;
    }

    string FastStringRepeat()
    {
        for (int i = 0; i < repeat; ++i)
        {
            _fast.Append(tmp1);
        }

        string str = _fast.ToString();

        return str;
    }

    string UtilsFormat()
    {
        string str = string.Empty;

        for (int i = 0; i < repeat; ++i)
        {
            str = StringUtils.Format("{0}{1}", str, tmp1);
        }

        return str;
    }
}

検証結果

N=10

image.png

N=100

image.png

N=1000

image.png

N=10000

image.png

N=100000

image.png

今回は 「FastString」「StringBuilder」 の2強です。
なるほど、1つのstringに繰り返し連結させる場合はStringBuilderの方が優れているんですね。

結論

10数個の文字列を一度に連結して、複数の文字列を生成する場合は 「$""」「+」 を(可読性を考えるなら「$""」でしょう)、
1つのstringに繰り返し連結して長文を作りたいときは 「FastString」「StringBuilder」 で連結するのがよさそうです。

「$""」が一番使う機会が多そうですね。


長野別荘

福岡の片隅で友人と何か作ってます。 Steamでマッチ3パズルゲーム「TAVERN GUARDIANS: BANQUET」配信中。

Crieitは個人で開発中です。 興味がある方は是非記事の投稿をお願いします! どんな軽い内容でも嬉しいです。
なぜCrieitを作ろうと思ったか

また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!

こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください!

ボードとは?

関連記事

コメント