C# UTF-8とUnicode(UTF-16 LE)とASCIIでReadAllLinesの処理時間を比較してみた(Windows10)


# やってみたこと

`File.ReadAllLines()`の入力テキストファイルを
Windows10 (C#でつくるexe含む)の内部エンコード(UTF-16 LE)に合わせたら
早くなるのか調べてみた。

<b>結論：</b>（少なくとも今回の条件・環境では）<b>遅くなった。</b>
おそらくファイルが大きくなるせい(※UTF-16は1文字を2byteで扱うので、ASCIIに対して2倍のファイルサイズになる。)

# 準備

今回の入力テキストファイルは、ASCIIで表現できる文字のみを対象とする。

## 入力ファイル(ASCII)の作成

下記で入力ファイルを作成。
暗号ライブラリの乱数生成を使用して、byte配列を生成し、
base64エンコーディングでASCII文字にする。
1行あたり78文字で改行はCRLFです。

<details><summary>ソースコード</summary><div>

```csharp

using System;
using System.IO;
using System.Security.Cryptography;

class RandGen
{
    [STAThread]
    static void Main()
    {
        //Int32と同じサイズのバイト配列にランダムな値を設定する
        //byte[] bs = new byte[sizeof(int)];
        byte[] bs = new byte[78];
        string[] strs = new string[10000];
        var rng = new RNGCryptoServiceProvider();
        for ( int i=0;i<strs.Length;i++ ) {
            rng.GetBytes(bs);
            strs[i] = Convert.ToBase64String(bs);
        }
        File.WriteAllLines("tmp_ascii.txt", strs); // UTF-8 (Base64はASCII文字しか使ってないので、実質ASCII) で保存
    }
}

```

</div></details>

約1MB(1,060,000 byte)のファイル`tmp_ascii.txt`が出来上がりました。


## 入力ファイル(UTF-16 LE)の作成

Windows10(C#/.NET含む)の内部エンコードであるUTF-16(LE)のファイルの作成は、
上記で生成したASCIIのテキストファイルをSakuraエディタで開いてUnicodeでBOMなしを選択して`tmp_utf16le.txt`として保存。
(ちなみにメモ帳で保存するとBOMありとなるようである。)

ちょうど2倍の2,120,000 byteのファイルができあがります。



# 計測用プログラム

```csharp

using System;
using System.IO; // Fileクラス使用のため
using System.Diagnostics; // Stopwatchクラス使用のため
using System.Runtime.CompilerServices; // MethodImpl使用のため


class LoadTest
{
    const int TestTimes = 1001; // 中央値を取りたいので奇数回にする。

    [MethodImpl(MethodImplOptions.NoInlining)]
    static void TestMeasureTime()
    {
        var measuredTicks = new long[TestTimes];
        Stopwatch sw = new Stopwatch();
        sw.Start();
        for ( int i=0;i<TestTimes;i++ ) {
            // 明示的にGCを呼び出すことで、途中でGCが発生しにくくする。
            GC.Collect();
            GC.WaitForPendingFinalizers();
            
            // 計測開始
            sw.Restart();
            {
                // 下行のコメントアウトを外してLoadTimeTest_ascii.exe として保存 ・・・ 表中の「ASCII」
                // File.ReadAllLines("tmp_ascii.txt",System.Text.Encoding.ASCII);

                // 下行のコメントアウトを外してコンパイルし LoadTimeTest_no_opt.exe として保存 ・・・ 表中の「no_opt (UTF-8)」
                // File.ReadAllLines("tmp_ascii.txt");

                // 下行のコメントアウトを外してLoadTimeTest_utf16le.exe として保存 ・・・ 表中の「UTF-16 LE」
                // File.ReadAllLines("tmp_utf16le.txt",System.Text.Encoding.Unicode);
            }
            sw.Stop();

            // 計測結果を取得
            measuredTicks[i] = sw.ElapsedTicks;
        }

        for ( int i=0;i<TestTimes;i++ ) {
            Console.WriteLine(measuredTicks[i]);
        }

        // 中央値を取得
        Array.Sort(measuredTicks);
        Console.WriteLine("Median[Ticks]:" + measuredTicks[TestTimes/2].ToString());
        double medianInNs = (measuredTicks[TestTimes/2]*1000.0*1000.0*1000.0)/Stopwatch.Frequency;
        Console.WriteLine("Median[ns]:" + medianInNs.ToString());
    }

    [STAThread]
    static void Main()
    {
        TestMeasureTime();
    }
}

```

## 念のための確認

別のプログラム(下記に一部抜粋)で`File.ReadAllLines`で読み込んだ文字列が同一であることを確認済み。

```csharp

const int TestTimes = 1;

string[] s1 = File.ReadAllLines("tmp_ascii.txt");
string[] s2 = File.ReadAllLines("tmp_ascii.txt",System.Text.Encoding.ASCII);
string[] s3 = File.ReadAllLines("tmp_utf16le.txt",System.Text.Encoding.Unicode);
Console.WriteLine(s1[0]);
Console.WriteLine(s2[0]);
Console.WriteLine(s3[0]);
```

# 測定結果

1回のexe実行で、1001回ファイルを読み込み、その中央値を計測した。
(平均だと他のアプリとかGCとかキャッシュとかの影響を受けやすそうなので、中央値を使用した。)

<b>測定結果(中央値)</b>
単位はms

<table>
<tr><th>exe実行回</th><th>ASCII</th><th>no_opt<br>(UTF-8)</th><th>UTF-16 LE</th></tr>
<tr><td>1回目</td><td>4.0317</td><td>2.9995</td><td>4.5074</td></tr>
<tr><td>2回目</td><td>4.1147</td><td>3.0521</td><td>4.7191</td></tr>
<tr><td>3回目</td><td>4.2551</td><td>3.1007</td><td>4.3768</td></tr>
</table>

## 結論

何もしないの(no_opt)が一番早かった。

## キャッシュ(?)されるまでの処理時間の傾向

「exe実行1回目」[^1] における、読み込み1001回のうち、最初の10回程度を取り出すと、下記のようになった。
ファイルとかもろもろ(？)がキャッシュされるのか、徐々に早くなる。

<table>
<tr><th>回目/1001回</th><th>ASCII</th><th>no_opt<br>(UTF-8)</th><th>UTF-16 LE</th></tr>
<tr><td>1(i=0)</td><td>157871</td><td>100893</td><td>189679</td></tr>
<tr><td>2</td><td>108993</td><td>72674</td><td>117979</td></tr>
<tr><td>3</td><td>102807</td><td>69735</td><td>122891</td></tr>
<tr><td>4</td><td>102341</td><td>69005</td><td>116488</td></tr>
<tr><td>5</td><td>99202</td><td>69604</td><td>123357</td></tr>
<tr><td>6</td><td>92871</td><td>69354</td><td>102751</td></tr>
<tr><td>7</td><td>73894</td><td>69355</td><td>78776</td></tr>
<tr><td>8</td><td>60606</td><td>68971</td><td>64050</td></tr>
<tr><td>9</td><td>54991</td><td>68982</td><td>62492</td></tr>
<tr><td>10(i=9)</td><td>51871</td><td>53997</td><td>51491</td></tr>
</table>

実用上、同じファイルを連続で複数回読み込むシーンはあまり無いと思うので、
中央値や平均を見ても意味がないかもしれない。


## １回読むだけなら
<b>初回(i=0)の抜粋</b>
中央値と違い、安定しない。
N数(サンプル数)が少ないが、何もしないの(no_opt)が一番早かった。

<table>
<tr><th>exe実行回</th><th>ASCII</th><th>no_opt<br>(UTF-8)</th><th>UTF-16 LE</th></tr>
<tr><td>1回目</td><td>15.7871</td><td>10.0893</td><td>18.9679</td></tr>
<tr><td>2回目</td><td>18.3172</td><td>13.7212</td><td>19.1679</td></tr>
<tr><td>3回目</td><td>17.9538</td><td>14.0408</td><td>28.4987</td></tr>
</table>


## 実行した順序

<details><summary>実行した順序(コマンドプロンプトでの実行内容)</summary><div>

```

LoadTimeTest_ascii.exe > time_ascii.txt
LoadTimeTest_no_opt.exe > time_no_opt.txt
LoadTimeTest_utf16le.exe > time_utf16le.txt

LoadTimeTest_ascii.exe > time_ascii2.txt
LoadTimeTest_no_opt.exe > time_no_opt2.txt
LoadTimeTest_utf16le.exe > time_utf16le2.txt

LoadTimeTest_utf16le.exe > time_utf16le3.txt
LoadTimeTest_no_opt.exe > time_no_opt3.txt
LoadTimeTest_ascii.exe > time_ascii3.txt
```

</div></details>

# おまけ

GCをとめる、ましな方法（一定メモリ容量を使用するまで停止できる）：
[.NET 4.6で追加された - GC.TryStartNoGCRegion～GC.EndNoGCRegionを使ってみる](https://qiita.com/Temarin/items/749ad661fd13d7402794)

(自分の環境では)デフォで入っているのは .NET 4.0.30319 なので、デフォ環境では使えない。
(※Visual Studio入れれば使える。)

# デコード処理の実体

ILSpy.exeで追いかけてみたところ、下記がデコード処理の本体のよう。色々チェック処理が入っているのでそれが処理時間に影響しているのかも。
[`System.Text.ASCIIEncoding.GetChars`](https://referencesource.microsoft.com/#mscorlib/system/text/asciiencoding.cs)
[`System.Text.UTF8Encoding.GetChars`](https://referencesource.microsoft.com/#mscorlib/system/text/utf8encoding.cs)
[`System.Text.UnicodeEncoding.GetChars`](https://referencesource.microsoft.com/#mscorlib/system/text/unicodeencoding.cs)


[^1]: 実際にはデバッグのため動作させていたので、厳密には「1回目」ではない。