ファイルをバイト配列に読み込む実験「FileStream.ReadとParallel.For」

C# コンピュータ
C#

SSDに保存されたファイルサイズが216MBのPNG形式画像ファイルがありまして、これをWinFormsのPictureBoxで表示するプログラムを書きます。

            // パターン0
            using FileStream fs = new(fileName, FileMode.Open, FileAccess.Read);

            sw.Start();
            var img = Image.FromStream(fs);
            sw.Stop();

            PictureBox picbox = new()
            {
                Dock = DockStyle.Fill,
                Image = img,
            };
            Controls.Add(picbox);

var img = Image.FromStream(fs);でFileStreamからImageオブジェクトを生成しています。
多分読み込み⇒デコード処理をしていると思われますが、処理時間を計測すると1165msほどでした。
昔からパソコンを弄っていた身としては、これでも十分速いと思うのですが、さらに速くできないか実験してみます。

まず初めに、FileStreamのReadメソッドでbyte配列に指定する位置(offset)から指定サイズ分、データを読み出すことが出来ます。
次にbyte配列からMemoryStreamを生成し、それからImageオブジェクトを生成してみます。

            // パターン1
            using FileStream fs = new(fileName, FileMode.Open, FileAccess.Read);
            

            sw.Start();

            long length = fs.Length;
            byte[] buffer = new byte[length];

            fs.Read(buffer, 0, (int)length);

            using MemoryStream ms = new(buffer);
            
            var img = Image.FromStream(ms);
            sw.Stop();

            PictureBox picbox = new()
            {
                Dock = DockStyle.Fill,
                Image = img,
            };
            Controls.Add(picbox);

ファイルの読み込みとデコードを分離し手順化した感じです。予想ですがMemoryStreamの処理分パフォーマンスは悪化するはずです。
時間を計測したところ1210msと予想通り遅くなりました。数回繰り返しても概ね同じような結果になります。

先ほどはファイルから読み出す量をファイルサイズを指定しているので、一度のReadでファイル全体を呼び出していました。
これを数回に分けて呼び出してみます。余分な手順が増えますのでパフォーマンスはさらに悪化すると思われます。

            // パターン2

            sw.Start();

            FileInfo info = new(fileName);
            long fileLength = info.Length;
            const int n = 2;
            long sz = fileLength / n; // 11 / 2 = 5
            byte[] buffer = new byte[fileLength];

            for (int i = 0; i <= n; i++)
            {
                FileStream fs = new(fileName, FileMode.Open, FileAccess.Read);

                long readSize = sz;
                if (i >= n)
                {
                    readSize = fileLength - (sz * n);
                }
                if (readSize > 0)
                {
                    long offset = sz * i;
                    fs.Read(buffer, (int)offset, (int)readSize);
                }
                fs.Close();
            }

            using MemoryStream ms = new(buffer);
            
            var img = Image.FromStream(ms);
            sw.Stop();

            PictureBox picbox = new()
            {
                Dock = DockStyle.Fill,
                Image = img,
            };
            Controls.Add(picbox);

forループで2回(2で割り切れない場合3回)に分けて読み込んでいます。ループ内でFileStreamを開いたり閉じたりしていますので、そこの部分見ても結構処理が遅くなると思われます。
実行してみると694msと、なぜか処理時間が短縮してしまいました。

予想外のことが起きてしまいましたが、ループする回数を増やせば余分な処理が増えて遅くなるはずです。
ループ回数を8回に増やしてみます。

            // パターン3

            sw.Start();

            FileInfo info = new(fileName);
            long fileLength = info.Length;
            const int n = 8;
            long sz = fileLength / n; // 11 / 2 = 5
            byte[] buffer = new byte[fileLength];

            for (int i = 0; i <= n; i++)
            {
                FileStream fs = new(fileName, FileMode.Open, FileAccess.Read);

                long readSize = sz;
                if (i >= n)
                {
                    readSize = fileLength - (sz * n);
                }
                if (readSize > 0)
                {
                    long offset = sz * i;
                    fs.Read(buffer, (int)offset, (int)readSize);
                }
                fs.Close();
            }

            using MemoryStream ms = new(buffer);
            
            var img = Image.FromStream(ms);
            sw.Stop();

            PictureBox picbox = new()
            {
                Dock = DockStyle.Fill,
                Image = img,
            };
            Controls.Add(picbox);
        }

実行した結果は310msと予想に反して処理時間が短縮化されてしまいました。

プログラムの作り方が悪いのか、筆者の知識が誤っているのか、多分その両方だと思われます。

この時点で筆者には、ほぼお手上げなのですが、可能性としてForの処理をコンピュータ側が最適化してくれて並列処理をしてくれているかもしれません。

            // パターン4

            sw.Start();

            FileInfo info = new(fileName);
            long fileLength = info.Length;
            int n = Environment.ProcessorCount; // 16
            long sz = fileLength / n; // 11 / 2 = 5
            byte[] buffer = new byte[fileLength];

            ParallelOptions options = new()
            {
                MaxDegreeOfParallelism = n
            };
            Parallel.For(0, (n+1), options, i =>
            {
                FileStream fs = new(fileName, FileMode.Open, FileAccess.Read);

                long readSize = sz;
                if (i >= n)
                {
                    readSize = fileLength - (sz * n);
                }
                if (readSize > 0)
                {
                    long offset = sz * i;
                    fs.Read(buffer, (int)offset, (int)readSize);
                }
                fs.Close();
            });

            using MemoryStream ms = new(buffer);
            
            var img = Image.FromStream(ms);
            sw.Stop();

            PictureBox picbox = new()
            {
                Dock = DockStyle.Fill,
                Image = img,
            };
            Controls.Add(picbox);

Forの部分をParallel.Forに置き換えて並列処理化してみました。
実行したところ、n=16(CPUのスレッド数)で処理時間は279msとさらに短縮され、その半分n=8の処理時間は315msとForで実行した数値とほぼ同一となりました。

結果速くはなりましたが、理由がわからないという、なんとも締まらない結末となりました。

ソースコード全文

ファイル名:Form1.cs

using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Reflection;

namespace FileReadByteArray1;

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();

        const string fileName = @"F:\csharp\dotnet8\winforms\FileReadByteArray1\IMG_20240329_0006.png";

        const int pattern = 4; // 実験するパターン
        Stopwatch sw = new();

        if (0 == pattern)
        {
            // パターン0
            using FileStream fs = new(fileName, FileMode.Open, FileAccess.Read);

            sw.Start();
            var img = Image.FromStream(fs);
            sw.Stop();

            PictureBox picbox = new()
            {
                Dock = DockStyle.Fill,
                Image = img,
            };
            Controls.Add(picbox);
        }
        if (1 == pattern)
        {
            // パターン1
            using FileStream fs = new(fileName, FileMode.Open, FileAccess.Read);
            

            sw.Start();

            long length = fs.Length;
            byte[] buffer = new byte[length];

            fs.Read(buffer, 0, (int)length);

            using MemoryStream ms = new(buffer);
            
            var img = Image.FromStream(ms);
            sw.Stop();

            PictureBox picbox = new()
            {
                Dock = DockStyle.Fill,
                Image = img,
            };
            Controls.Add(picbox);
        }
        if (2 == pattern)
        {
            // パターン2

            sw.Start();

            FileInfo info = new(fileName);
            long fileLength = info.Length;
            const int n = 2;
            long sz = fileLength / n; // 11 / 2 = 5
            byte[] buffer = new byte[fileLength];

            for (int i = 0; i <= n; i++)
            {
                FileStream fs = new(fileName, FileMode.Open, FileAccess.Read);

                long readSize = sz;
                if (i >= n)
                {
                    readSize = fileLength - (sz * n);
                }
                if (readSize > 0)
                {
                    long offset = sz * i;
                    fs.Read(buffer, (int)offset, (int)readSize);
                }
                fs.Close();
            }

            using MemoryStream ms = new(buffer);
            
            var img = Image.FromStream(ms);
            sw.Stop();

            PictureBox picbox = new()
            {
                Dock = DockStyle.Fill,
                Image = img,
            };
            Controls.Add(picbox);
        }
        if (3 == pattern)
        {
            // パターン3

            sw.Start();

            FileInfo info = new(fileName);
            long fileLength = info.Length;
            const int n = 8;
            long sz = fileLength / n; // 11 / 2 = 5
            byte[] buffer = new byte[fileLength];

            for (int i = 0; i <= n; i++)
            {
                FileStream fs = new(fileName, FileMode.Open, FileAccess.Read);

                long readSize = sz;
                if (i >= n)
                {
                    readSize = fileLength - (sz * n);
                }
                if (readSize > 0)
                {
                    long offset = sz * i;
                    fs.Read(buffer, (int)offset, (int)readSize);
                }
                fs.Close();
            }

            using MemoryStream ms = new(buffer);
            
            var img = Image.FromStream(ms);
            sw.Stop();

            PictureBox picbox = new()
            {
                Dock = DockStyle.Fill,
                Image = img,
            };
            Controls.Add(picbox);
        }

        if (4 == pattern)
        {
            // パターン4

            sw.Start();

            FileInfo info = new(fileName);
            long fileLength = info.Length;
            int n = Environment.ProcessorCount; // 16
            //n = 8;
            long sz = fileLength / n; // 11 / 2 = 5
            byte[] buffer = new byte[fileLength];

            ParallelOptions options = new()
            {
                MaxDegreeOfParallelism = n
            };
            Parallel.For(0, (n+1), options, i =>
            {
                FileStream fs = new(fileName, FileMode.Open, FileAccess.Read);

                long readSize = sz;
                if (i >= n)
                {
                    readSize = fileLength - (sz * n);
                }
                if (readSize > 0)
                {
                    long offset = sz * i;
                    fs.Read(buffer, (int)offset, (int)readSize);
                }
                fs.Close();
            });

            using MemoryStream ms = new(buffer);
            
            var img = Image.FromStream(ms);
            sw.Stop();

            PictureBox picbox = new()
            {
                Dock = DockStyle.Fill,
                Image = img,
            };
            Controls.Add(picbox);
        }
        Debug.Print($"パターン{pattern}: {sw.ElapsedMilliseconds}ms");
    }
}

追記

より読み込みが遅いネットワークドライブでも試してみる。

SSD
0:1165ms
1:1210ms
2:694ms
3:310ms
4:279ms

ネットワークドライブ(1000Base-T)
0:3090ms
1:3128ms
2:1693ms
3:589ms
4:420ms

順当に遅くはなりましたが、概ねに同様な結果になりました。
(予想では4<<0<1<2<3になると思ったのですが…)

コメント