> 作業効率UP!! オススメのモバイルモニターを紹介

【C#】高速でファイルの末尾を読み取りする方法

当ページのリンクには広告が含まれています。
  • URLをコピーしました!

C#でファイルを読み取りする場合は、StreamReaderクラスFileStreamクラスFileクラスを使います。

このクラスにはファイル内のテキストを読み取りするメソッドが用意されています。

クラスメソッド
StreamReaderReadLine()、ReadToEnd()
FileStreamRead()、ReadByte()
FileReadAllLine()、ReadAllText()

これらのメソッドを使って、ファイルの末尾を読み取りする方法はいくつかありますが、使用するメソッド・使い方によって処理速度が異なります。

今回は Benchmark.NET を利用して、ファイルの末尾を読み取りするまでの処理速度を測定して、高速で行える手法を検証してみたいと思います。

どのメソッドを使うか迷っている。。。

高速で最後の行を読み取りしたい方はもちろん、上述したようにファイルのテキストを読み取りする方法は何種類かあるので、どれを使おうか迷っているという方にも参考になる記事になっています。

ぜひ本記事を最後まで読んでみてください。

オススメの参考書

C#の使い方が丁寧に解説しており、「基礎からしっかりと学びたい」という初心者の方にオススメの一冊です。サンプルコードも記載してあり、各章の最後に復習問題があるので理解度を確認しながら読み進めることができます。新しい C# のバージョンにも対応している書籍です。

Benchmark.NETで測定する

今回は Benchmark.NET を利用して、ファイルの末尾を読み取りするまでの処理速度を測定します。

Benchmark.NET について

Benchmark.NET とは、C# や F#、VB など利用できるベンチマークテストを行う為のツールです。

このツールを使うことで、パフォーマンスの測定、比較、分析を簡単に行う事ができます。

BenchmarkDotNetは、メソッドをベンチマークに変換し、そのパフォーマンスを追跡し、再現性のある測定実験を共有するのに役立ちます。 

GitHub:BenchmarkDotNet

Benchmark.NET のインストール

Benchmark.NET は、NuGet から簡単にプロジェクトへインストールできます。

手順は次の通りです。

STEP
プロジェクトを開く

お使いのパソコンにインストールされている Visual Studio 2022 で、プロジェクトを開きます。

統合開発環境である Visual Studio のインストールがまだの方は、次の記事を参考にしてインストールします。


STEP
NuGet パッケージの管理を開く

メニューバーから [ツール] -> [NuGet パッケージ マネージャー] -> [ソリューションの NuGet パッケージの管理] の順に選択します。


STEP
Benchmark.NET をインストールする

検索欄に「Benchmark.NET」を入力して、検索結果の一覧から「BenchmarkDotNet」をインストールします。(2023年5月現在、バージョンは0.13.5)


検証で使用するファイルを作成する

今回、検証で使用するファイルは、 1~ 100までの数値をカンマ区切りした行が1万行ある csv ファイルを用意します。

ファイルの中身(イメージ
1,2,3,4,5,6,7,8,9,10,11, … ,100
1,2,3,4,5,6,7,8,9,10,11, … ,100
1,2,3,4,5,6,7,8,9,10,11, … ,100

使用するファイルのサイズは、画像にあるように 28,614KB です。

独自メソッドを作成する

C#でファイルの読み取りをする際に使用されるメソッドを使って、ファイルの末尾の行を取得する独自メソッドを何種類か作成して検証を行います。

独自メソッド備考
ReadFileStream クラス のRead()を使用する。
ReadLineStreamReader クラスのReadLine()を使用する。
ReadToEndStreamReader クラスのReadToEnd()を使用する。
ReadLinesFile クラスのReadLines()を使用する。
ReadLinesLinqFile クラスのReadLines()を使用する。末尾の行を取得する際に Linq を使用する。
ReadAllLinesFile クラスのReadAllLines()を使用する。
ReadAllLinesLinqFile クラスのReadAllLines()を使用する。末尾の行を取得する際に Linq を使用する。
ReadAllTextFile クラスのReadAllText()を使用する。

以下に作成した独自メソッドのコードを紹介しています。

Read メソッド

このメソッドは、FileStream クラス のRead()を使用しています。

ファイルの読み取り開始位置を末尾に移動して、Read()でファイルの末尾からバイトの配列データを取得します。取得した配列のデータを文字列へ変換し、改行コードを区切り文字として分割します。

分割した要素の1つ目が末尾の行になります。

コードを表示(ここをクリックしてください)
[Benchmark]
public void Read()
{
    using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
    {
        var bufferSize = 1024;
        var buffer = new byte[bufferSize];
        var lastLine = string.Empty;
        var position = fs.Seek(0, SeekOrigin.End);

        while (position > 0)
        {
            var offset = Math.Min(bufferSize, (int)position);
            fs.Seek(-offset, SeekOrigin.Current);

            var bytesRead = fs.Read(buffer, 0, offset);

            var text = encording.GetString(buffer, 0, bytesRead);
            var lines = text.Split(new[] { Environment.NewLine }, StringSplitOptions.None);

            if (lines.Length > 1)
            {
                lastLine = lines[lines.Length - 1];
                break;
            }

            position -= offset;
            fs.Seek(-offset, SeekOrigin.Current);
        }

        Console.WriteLine($"ファイルの末尾の行: {lastLine}");
    }
}
メソッド説明
Seekこのストリームの現在位置を特定の値に設定します。
Readストリームからバイトのブロックを読み取り、そのデータを特定のバッファーに書き込みます。
使用しているメソッドの説明
構造体名説明
SeekOriginシークに使用するストリームの場所を指定します。
・SeekOrigin.Begin → 先頭
・SeekOrigin.Current → 現在の位置
・SeekOrigin.End  → 末尾
使用している構造体の説明

ReadLine メソッド

StreamReader クラスのReadLine()を使用します。

Peek()で読み取り対象の文字列があるかどうかを監視しながら、ReadLine()で1行分の文字列を読み取りします。これをファイルの末尾に達するまで実行します。

最後に読み取りした文字列が末尾の行になります。

コードを表示(ここをクリックしてください)
[Benchmark]
public void ReadLine()
{
    using (StreamReader sr = new(fileName, encording))
    {
        var lastLine = string.Empty;

        while (0 <= sr.Peek())
        {
            lastLine = sr.ReadLine();
        }

        Console.WriteLine($"ファイルの末尾の行: {lastLine}");
    }
}
メソッド説明
Peek読み取り対象の文字列がある場合は整数を返します。読み取り対象の文字列がない場合は-1を返します。
ReadLine現在のストリームから 1 行分の文字を読み取り、そのデータを文字列として返します。
使用しているメソッドの説明

ReadToEnd メソッド

StreamReader クラスのReadToEnd()を使用します。

ReadToEnd()でファイルの先頭から末尾までの全ての行を読み取りします。改行コードを区切り文字として取得した文字列を分割します。

分割した要素の最後が末尾の行になります。

コードを表示(ここをクリックしてください)
[Benchmark]
public void ReadToEnd()
{
    using (StreamReader sr = new(fileName, encording))
    {
        var lastLine = string.Empty;

        var text = sr.ReadToEnd();
        var lines = text.Split(new[] { Environment.NewLine }, StringSplitOptions.None);

        if (lines.Length > 1)
        {
            lastLine = lines[lines.Length - 1];
        }

        Console.WriteLine($"ファイルの末尾の行: {lastLine}");
    }
}
メソッド説明
ReadToEndストリームの現在位置から末尾までのすべての文字を読み込みます。
使用しているメソッドの説明

ReadLines メソッド

File クラスのReadLines()を使用します。

ReadLines()はファイルの全ての行を取得するので、foreachで1行ずつ呼び出します。

最後に取得した行が末尾の行になります。

コードを表示(ここをクリックしてください)
[Benchmark]
public void ReadLines()
{
    var lastLine = string.Empty;

    var lines = File.ReadLines(fileName, encording);
    foreach (var line in lines)
    {
        lastLine = line;
    }

    Console.WriteLine($"ファイルの末尾の行: {lastLine}");
}
メソッド説明
ReadLinesファイルの行を読み取ります。
使用しているメソッドの説明

ReadLinesLinq メソッド

File クラスのReadLines()を使用します。

ReadLines()はファイルの全ての行を取得するので、Linq のLastOrDefault()で最後の行を取得します。

コードを表示(ここをクリックしてください)
[Benchmark]
public void ReadLinesLinq()
{
    var lastLine = File.ReadLines(fileName, encording).LastOrDefault();
    Console.WriteLine($"ファイルの末尾の行: {lastLine}");
}
メソッド説明
ReadLinesファイルの行を読み取ります。
LastOrDefaultシーケンスの最後の要素を返します。要素が見つからない場合は既定値を返します。
使用しているメソッドの説明

ReadAllLines メソッド

File クラスのReadAllLines()を使用します。

ReadAllLines()はファイルの全ての行を配列で取得します。要素の最後が末尾の行になります。

コードを表示(ここをクリックしてください)
[Benchmark]
public void ReadAllLines()
{
    var lastLine = string.Empty;
    var lines = File.ReadAllLines(fileName, encording);
    if (lines.Length > 1)
    {
        lastLine = lines[lines.Length - 1];
    }

    Console.WriteLine($"ファイルの末尾の行: {lastLine}");
}
メソッド説明
ReadAllLinesテキスト ファイルを開き、ファイルのすべての行を文字列配列に読み取った後、ファイルを閉じます。
使用しているメソッドの説明

ReadAllLinesLinq メソッド

File クラスのReadAllLines()を使用します。

ReadAllLines()はファイルの全ての行を取得するので、Linq のLastOrDefault()で最後の行を取得します。

コードを表示(ここをクリックしてください)
[Benchmark]
public void ReadAllLinesLinq()
{
    var lastLine = File.ReadAllLines(fileName, encording).LastOrDefault();
    Console.WriteLine($"ファイルの末尾の行: {lastLine}");
}
メソッド説明
ReadLinesテキスト ファイルを開き、ファイルのすべての行を文字列配列に読み取った後、ファイルを閉じます。
LastOrDefaultシーケンスの最後の要素を返します。要素が見つからない場合は既定値を返します。
使用しているメソッドの説明

ReadAllText メソッド

File クラスのReadAllText()を使用します。

ReadAllText()でファイルの先頭から末尾までの全ての行を読み取りします。改行コードを区切り文字として取得した文字列を分割します。

分割した要素の最後が末尾の行になります。

コードを表示(ここをクリックしてください)
[Benchmark]
public void ReadAllText()
{
    var lastLine = string.Empty;

    var text = File.ReadAllText(fileName, encording);
    var lines = text.Split(new[] { Environment.NewLine }, StringSplitOptions.None);

    if (lines.Length > 1)
    {
        lastLine = lines[lines.Length - 1];
    }

    Console.WriteLine($"ファイルの末尾の行: {lastLine}")
メソッド説明
ReadAllTextテキスト ファイルを開き、そのファイル内のすべてのテキストを文字列に読み取った後、ファイルを閉じます。
使用しているメソッドの説明

測定結果

上述したメソッドの処理時間を測定した結果が次の通りです。処理速度が速い順に並べています。

順位独自メソッド名処理速度(msec)
Read0.332
ReadLine6.690
ReadLines6.963
ReadLinesLinq7.212
ReadAllLinesLinq18.318
ReadAllLines18.327
ReadToEnd27.720
ReadAllText28.538

この結果から、独自メソッドのRead()が圧倒的に高速であることが判断できます。

他のメソッドは全ての行を読み取りしていますが、このメソッドは末尾の行からファイルの読み込みをしているので、無駄の行は読み取りしていない点がやはり処理速度に大きな差の要因になっているのでしょう。

下図は Benchmark.NET の出力結果です。

まとめ

この記事ではファイルの最後の行を取得する処理を測定して、高速に読み取りをする方法について紹介しました。

結論としては、先頭から全てのテキストを取得するより、ファイルの最後の行から読み取りした方が遥かに高速であることが分かりました。

大容量のファイルから最後の行だけ高速に読み込みたい場合には、参考になると思います。

以上、最後まで読んで頂きありがとうございました。

付録

Benchmark.NET で測定した時のコードを記載しておきます。

Benchmark.NETの特徴で[デバッグ]で実行するとエラーになります。必ず[リリース]で実行をしましょう。

using BenchmarkDotNet.Running;
using ConsoleApp1;

public class Program
{
    public static void Main(string[] args)
    {
        //Benchmark.NETによる測定処理
        var summary = BenchmarkRunner.Run<FileEndLineTest>();
    }
}
using BenchmarkDotNet.Attributes;
using System.Text;

namespace ConsoleApp1
{
    [ShortRunJob]
    public class FileEndLineTest
    {
        private string fileName = @"C:\Sample.csv";
        private Encoding encording = Encoding.GetEncoding("utf-8");

        [Benchmark]
        public void Read()
        {
            // 記事内のコードを参照ください。
        }

        [Benchmark]
        public void ReadLine()
        {
            // 記事内のコードを参照ください。
        }

        [Benchmark]
        public void ReadToEnd()
        {
            // 記事内のコードを参照ください。
        }

        [Benchmark]
        public void ReadLines()
        {
            // 記事内のコードを参照ください。
        }

        [Benchmark]
        public void ReadLinesLinq()
        {
            // 記事内のコードを参照ください。
        }

        [Benchmark]
        public void ReadAllLines()
        {
            // 記事内のコードを参照ください。
        }

        [Benchmark]
        public void ReadAllLinesLinq()
        {
            // 記事内のコードを参照ください。
        }

        [Benchmark]
        public void ReadAllText()
        {
            // 記事内のコードを参照ください。
        }
    }
}
よかったらシェアしてね!
  • URLをコピーしました!