読者です 読者をやめる 読者になる 読者になる

手続き型音楽の日常

関数型音楽に乗り換えたい

StringBuilderをstringと同じだけ作ると、どれほど遅いのか。

C# くだらない実験

この間、以下のようなコードを見かけました。

StringBuilder strSql = new StringBuilder();
strSql.Append(@"SELECT * FROM ...
                WHERE ..."); // SQLは長いので省略。
this.ExecuteNonQuery(strSql.ToString());

フォームのメソッドに書かれていたのですが、フォーム自体がユーザコントロールになっていて、データベース接続の機能をすでに実装しているんですね。

なので、それにSQLを投げれば完了というすごくシンプルなものなのですが。

StringBuilderにする必要あったのかなって。

StringBuilderを必要とする理由

StringBuilder は、 string 型が演算で毎回新しいインスタンスを生成したり破棄したりするのに対して、最初のインスタンスを使いまわすために使う、というものだと思います。

具体的には次のコード。

string s = "";
s = s + "a";
s = s + "b";
s = s + "c";

上記のコードでは、最終的に変数 s に入るのは "abc" という文字列ですが、その文字列を得るまでにインスタンスを生成する回数は7回です。

それに対し、

StringBuilder s = new StringBuilder();
s.Append("a");
s.Append("b");
s.Append("c");

上記のコードでも同様に、最終的に変数 s に入るのは "abc" という文字列ですが、インスタンスの生成は4回で済みます。

インスタンスの生成、という観点からみると、 string 型より StringBuilder 型のほうが同じインスタンスを使いまわすためオーバーヘッドが少ないと考えられます。

しかし。

最初のコードって、はじめに StringBuilder 型の変数を確保しているので、毎回インスタンスを生成していることになるんですよね。

これではstring型を直で触るのと変わらないのでは…。

ということで調べてみました。

検証コード

次の検証コードを用意しました。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication1
{
    class Program
    {
        /// <summary> 出力用ダミー変数 </summary>
        static string d_out;

        /// <summary>
        /// メインエントリポイント
        /// </summary>
        static void Main(string[] args)
        {
            try
            {
                int count1;
                int count2;
                List<double> measuredList = new List<double>();

                //------------------------------//
                // 入力
                //------------------------------//

                do
                    Console.WriteLine("1回の計測で確保を行う回数を指定してください...");
                while (!int.TryParse(Console.ReadLine(), out count1));

                do
                    Console.WriteLine("計測を行う回数を指定してください...");
                while (!int.TryParse(Console.ReadLine(), out count2));

                //------------------------------//
                // 計測 string
                //------------------------------//

                measuredList.Clear();
                Console.WriteLine("-------------------------------------------------------");
                Console.WriteLine("stringクラスで計測します。");

                GC.Collect(GC.MaxGeneration);
                for (int i = 0; i < count2; i++)
                {
                    measuredList.Add(MeasureTime_String(count1));
                    Console.WriteLine((i + 1).ToString().PadLeft((int)Math.Log10(count2) + 1) + "回目 : " + measuredList[i]);
                }
                Console.WriteLine("平均 : " + measuredList.Average());

                //------------------------------//
                // 計測 StringBuilder
                //------------------------------//

                measuredList.Clear();
                Console.WriteLine("-------------------------------------------------------");
                Console.WriteLine("StringBuilderクラスで計測します。");

                GC.Collect(GC.MaxGeneration);
                for (int i = 0; i < count2; i++)
                {
                    measuredList.Add(MeasureTime_StringBuilder(count1));
                    Console.WriteLine((i + 1).ToString().PadLeft((int)Math.Log10(count2) + 1) + "回目 : " + measuredList[i]);
                }

                Console.WriteLine("平均 : " + measuredList.Average());

                //------------------------------//
                // 計測 StringBuilder + Append
                //------------------------------//

                measuredList.Clear();
                Console.WriteLine("-------------------------------------------------------");
                Console.WriteLine("StringBuilderクラス + Appendメソッドで計測します。");

                GC.Collect(GC.MaxGeneration);
                for (int i = 0; i < count2; i++)
                {
                    measuredList.Add(MeasureTime_StringBuilderAppend(count1));
                    Console.WriteLine((i + 1).ToString().PadLeft((int)Math.Log10(count2) + 1) + "回目 : " + measuredList[i]);
                }
                Console.WriteLine("平均 : " + measuredList.Average());

            }
            finally
            {
                Console.WriteLine("何かキーを押してください...");
                Console.ReadKey();
            }
        }

        /// <summary>
        /// stringクラスを指定された回数確保し、その時間を計測します。
        /// </summary>
        /// <param name="count">回数</param>
        /// <returns>時間(秒)</returns>
        static double MeasureTime_String(int count)
        {
            DateTime strT = DateTime.Now;

            for (int i = 0; i < count; i++)
            {
                string s = "";
                s = s + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
                Program.d_out = s.ToString();
            }

            DateTime endT = DateTime.Now;

            return (endT - strT).TotalSeconds;
        }

        /// <summary>
        /// StringBuilderクラスを指定された回数確保し、その時間を計測します。
        /// </summary>
        /// <param name="count">回数</param>
        /// <returns>時間(秒)</returns>
        static double MeasureTime_StringBuilder(int count)
        {
            DateTime strT = DateTime.Now;

            for (int i = 0; i < count; i++)
            {
                StringBuilder s = new StringBuilder("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890");
                Program.d_out = s.ToString();
            }

            DateTime endT = DateTime.Now;

            return (endT - strT).TotalSeconds;
        }

        /// <summary>
        /// StringBuilderクラスを指定された回数確保し、Appendメソッドで文字列を指定します。その時間を計測します。
        /// </summary>
        /// <param name="count">回数</param>
        /// <returns>時間(秒)</returns>
        static double MeasureTime_StringBuilderAppend(int count)
        {
            DateTime strT = DateTime.Now;

            for (int i = 0; i < count; i++)
            {
                StringBuilder s = new StringBuilder();
                s.Append("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890");
                Program.d_out = s.ToString();
            }

            DateTime endT = DateTime.Now;

            return (endT - strT).TotalSeconds;
        }
    }
}

汚いとか言わない。

実際にやってみた。

実際に動かしました。スペックは以下の通り。

  • CPU: Atom Z3775 (1.46GHz 4core)
  • メモリ: 2GB

f:id:yuzutan_hnk:20170226021055p:plain:w300

やっぱり、string型のほうがオーバーヘッドは少ないみたいですね。

追記 2017/02/26 実験に使ったスペックを記述していなかったので追記。