20200701のC#に関する記事は4件です。

[C#] 構造体に readonly関数メンバーを使用した時のIL/JIT Asmの違い

C# 8.0で、Mutableな構造体にreadonly関数メンバーを追加できるようになりました。
それにより、readonlyな構造体のインスタンス(変数、または参照)の防御的なコピーがなくなりますが、ILやJIT Asmがどう変わるかのメモです。

サンプルコード

  • readonlyのあるStruct.ReadOnlyToString()を呼び出すProgram.R
  • readonlyのないStruct.ToString()を呼び出すProgram.M

の差を見てみましょう。

using System;
using System.Runtime.CompilerServices;
public struct Struct
{
    public long n;
    public readonly Guid guid;

    [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
    public readonly string ReadOnlyToString() => n.ToString();

    [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
    public string ToString() => n.ToString();
 }

public static class Program
{
    [MethodImpl(MethodImplOptions.NoInlining)]
    public static void R(in Struct s) =>
        Console.WriteLine(s.ReadOnlyToString());

    [MethodImpl(MethodImplOptions.NoInlining)]
    public static void M(in Struct s) => 
        Console.WriteLine(s.ToString());

    public static void Main()
    {
        var s = default(Struct);
        R(s);
        M(s);
    }
}

Program.RおよびProgram.Mの引数には inを付けているので、readonlyの参照になります。
readonlyの変数でも同様になるはずです。

サンプルコードの注意事項

はじめに断って起きますが、わかりやすい結果になるようにサンプルコードは結構恣意的です。
例えば、構造体のサイズを変えたりするとJIT Asmの結果が結構変わります。

CoreCLRのToString()には既にreadonlyが付いているので、直接呼び出すと思ったような結果にはなりません。

ILの差

sharplabで見る

readonlyのある関数メンバーの呼び出し

    .method public hidebysig static 
        void R (
            [in] valuetype Struct& s
        ) cil managed noinlining 
    {
        .param [1]
            .custom instance void [System.Private.CoreLib]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = (
                01 00 00 00
            )
        // Method begins at RVA 0x205d
        // Code size 12 (0xc)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: call instance string Struct::ReadOnlyToString()
        IL_0006: call void [System.Console]System.Console::WriteLine(string)
        IL_000b: ret
    } // end of method Program::R

readonlyのない関数メンバーの呼び出し

ldobj Struct がありコピーしている感じがありますね。

    .method public hidebysig static 
        void M (
            [in] valuetype Struct& s
        ) cil managed noinlining 
    {
        .param [1]
            .custom instance void [System.Private.CoreLib]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = (
                01 00 00 00
            )
        // Method begins at RVA 0x206c
        // Code size 20 (0x14)
        .maxstack 1
        .locals init (
            [0] valuetype Struct
        )

        IL_0000: ldarg.0
        IL_0001: ldobj Struct
        IL_0006: stloc.0
        IL_0007: ldloca.s 0
        IL_0009: call instance string Struct::ToString()
        IL_000e: call void [System.Console]System.Console::WriteLine(string)
        IL_0013: ret
    } // end of method Program::M

JIT Asmの差

sharplabで見る

readonlyのある関数メンバーの呼び出し

非常にシンプルですね。

; Core CLR v4.700.20.26901 on amd64

Program.R(Struct ByRef)
    L0000: sub rsp, 0x28
    L0004: call Struct.ReadOnlyToString()
    L0009: mov rcx, rax
    L000c: call System.Console.WriteLine(System.String)
    L0011: nop
    L0012: add rsp, 0x28
    L0016: ret

readonlyのない関数メンバーの呼び出し

コピーが発生してますね。

; Core CLR v4.700.20.26901 on amd64

Program.M(Struct ByRef)
    L0000: sub rsp, 0x38
    L0004: vzeroupper
    L0007: xor eax, eax
    L0009: mov [rsp+0x20], rax
    L000e: mov [rsp+0x28], rax
    L0013: mov [rsp+0x30], rax
    L0018: vmovdqu xmm0, [rcx]
    L001c: vmovdqu [rsp+0x20], xmm0
    L0022: mov rax, [rcx+0x10]
    L0026: mov [rsp+0x30], rax
    L002b: lea rcx, [rsp+0x20]
    L0030: call Struct.ToString()
    L0035: mov rcx, rax
    L0038: call System.Console.WriteLine(System.String)
    L003d: nop
    L003e: add rsp, 0x38
    L0042: ret

結論

readonlyな参照の構造体に対しては、readonly関数メンバーを使用すると効率的なアセンブリが生成されました。

余談ですが、inを付けたreadonly参照ではなく単にコピーにしても、今回のサンプルコードでは効率的なアセンブリになっていました。

Program.M(Struct)
    L0000: sub rsp, 0x28
    L0004: call Struct.ToString()
    L0009: mov rcx, rax
    L000c: call System.Console.WriteLine(System.String)
    L0011: nop
    L0012: add rsp, 0x28
    L0016: ret

構造体でリソースを管理するような設計では値渡しにできないので、readonly関数メンバーにする必要性はあるでしょう。
(もちろん、不意なメンバーの書き換えに対する予防という面もあります。)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Windows 10で大容量ファイルを分割する

たまにはプログラミングも良いものだ。

1.notepadで以下のテキストファイルを作成する。保存する拡張子間違えないように。
「fileName = ~」のところを分割したいファイル名に変更する。
「ReadBytes(100000000)」のところは、分割後のファイルサイズ。ここでは100MB。

split.cs
using System;
using System.IO;

class ConsoleApplication
{
    const string fileName = "14393.0.160911-2111.RS1_Refresh_SERVER_OEMRET_X64FRE_JA-JP.ISO";

    static void Main()
    {
        if (File.Exists(fileName))
        {
            Console.WriteLine("Opening file: " + fileName);
            using (BinaryReader reader = new BinaryReader(File.Open(fileName, FileMode.Open)))
            {
                int i = 0;
                byte[] buff;
                while ((buff = reader.ReadBytes(100000000)).Length != 0) {
                    String wfname = i.ToString() + ".dat";
                    Console.WriteLine("Writing file: " + wfname);
                    using (BinaryWriter writer = new BinaryWriter(File.Open(wfname, FileMode.CreateNew, FileAccess.Write)))
                    {
                        writer.Write(buff, 0, buff.Length);
                    }
                    i++;
                }
            }
            Console.WriteLine("Done!");
        }
    }
}

2.コマンドプロンプトで、プログラムをコンパイルする。

> C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe split.cs

3.プログラムを実行して、ファイルを分割する。

> split.exe
Opening file: 14393.0.160911-2111.RS1_Refresh_SERVER_OEMRET_X64FRE_JA-JP.ISO
Writing file: 0.dat
Writing file: 1.dat
...
Writing file: 57.dat
Writing file: 58.dat
Done!

4.分割後のファイルサイズ合計と、分割前のものを比較する。
エクスプローラーで分割後のファイルを全部選択して右クリック>プロパティで確認すると良い。
split.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C#でマルチスレッド

マルチスレッドをC#で実装してみよう

スレッドkとはプログラムを実行する処理の最小単位です。
アプリはメインスレッドといわれる単一のスレッド(シングルスレッド)で動作しています。

Mainスレッドで始まるスレッドがそうです。

シングルスレッドでは、通信が終わるまで他の操作はできません。

そこで出てくるのがマルチスレッドというものです。
マルチスレッドを利用することで、ネットワーク通信のように時間のかかる処理はバックグラウンドで実施し、アプリ利用者はメインスレッド上で操作を継続で気できるようになります。

ちなみに、スレッドと同じく処理単位を表す概念として、プロセスもあります。
相互の関係としては、一つのプロセスに対して、一つ以上のスレッドが属するという関係です。

プロセスはいわゆるプログラムのインスタンスそのものであり、新たなプロセスの動作にはCPUとメモリの割り当てが必要になります。その性質上、独立したメモリ空間を必要としない状況ではメモリの消費効率がよくありません。

それに対して、スレッドとはメモリ空間を共有しながら、処理だけを分離する仕組みともいえるため、その性質上メモリの消費効率は良くなりますが、反面。スレッド間でデータを共有している場合に、同時アクセス(競合)を意識しなければなりません。

新たなスレッドを生成するクラシカルな方法として、まずThreadクラスを地用することです。
しかし、後から述べる理由から現在は利用すべきではありません。

あくまで、原始的なスレッド生成実行の方法です。

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

namespace MultiThread
{
    class Program
    {
        static void Main(string[] args)
        {
            var t1 = new Thread(Count);
            var t2 = new Thread(Count);
            var t3 = new Thread(Count);

            t1.Start(1);
            t2.Start(2);
            t3.Start(3);

            t1.Join();
            t2.Join();
            t3.Join();

            Console.WriteLine("すべての処理が完了しました");
        }

        static void Count(object n)
        {
            for (int i = 0;i < 50; i++)
            {
                Console.WriteLine($"Thread{n}:{i}");
            }
        }
    }
}


スレッドの生成実行はプロセスに比べればオーバーヘッドの小さな処理です。
しかし、通常の命令に比べると十分に重たい処理であり、スレッドの生成破棄を繰り返すことはアプリ全体のパフォーマンスを劣化させます。

そこで、いったん生成したスレッドを破棄せずに保持しておいて、別な処理で再利用する仕組みが導入されました。
それがスレッドプールです。

スレッドプールを利用することで、スレッドを大量に利用するようなアプリでもスレッドの生成破棄のオーバーヘッドを軽減できます。

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

namespace MultiThread
{
    class Program
    {
        static void Main(string[] args)
        {
            Task t1 = Task.Run(() => Count(1));
            Task t2 = Task.Run(() => Count(2));
            Task t3 = Task.Run(() => Count(3));

            t1.Wait();
            t2.Wait();
            t3.Wait();


            Console.WriteLine("すべての処理が完了しました");
        }

        static void Count(object n)
        {
            for (int i = 0;i < 50; i++)
            {
                Console.WriteLine($"Thread{n}:{i}");
            }
        }
    }
}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Linq の ~OrDefault 拡張

これはなに

Linq の FirstOrDefault 系メソッドに、見つからなければ返される default 値を指定できるようにした拡張メソッド。

モチベーション

列挙体 eHoge があるときに、

return new eHoge[]{...}.FirstOrDefault(_ => {...}) ?? OnNotFound();

で、コンパイルエラーになる。列挙体は値型なので、あたりまえっちゃ当たり前なんだけど、そうすると

var r = new eHoge[]{...}.FirstOrDefault(_ => {...});
return r != default ? r : OnNotFound();

とか核ハメになって、それよりも

return new eHoge[]{...}.FirstOrDefault(_ => {...}, OnNotFound());

のように書けたほうが便利じゃねってなった。

コード

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

namespace System.Linq
{
    public static partial class Enumerable
    {
        public static TSource FirstOrDefault<TSource>(this IEnumerable<TSource> source, TSource defaultValue)
        {
            if (source == null) throw new ArgumentNullException(nameof(source));
            //Error.ArgumentNull("source");
            IList<TSource> list = source as IList<TSource>;
            if (list != null)
            {
                if (list.Count > 0) return list[0];
            }
            else
            {
                using (IEnumerator<TSource> e = source.GetEnumerator())
                {
                    if (e.MoveNext()) return e.Current;
                }
            }
            return defaultValue;
        }

        public static TSource FirstOrDefault<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate, TSource defaultValue)
        {
            if (source == null) throw new ArgumentNullException(nameof(source));
            if (predicate == null) throw new ArgumentNullException(nameof(predicate));
            //if (source == null) throw Error.ArgumentNull("source");
            //if (predicate == null) throw Error.ArgumentNull("predicate");
            foreach (TSource element in source)
            {
                if (predicate(element)) return element;
            }
            return defaultValue;
        }

        public static TSource LastOrDefault<TSource>(this IEnumerable<TSource> source, TSource defaultValue)
        {
            if (source == null) throw new ArgumentNullException(nameof(source));
            //if (source == null) throw Error.ArgumentNull("source");
            IList<TSource> list = source as IList<TSource>;
            if (list != null)
            {
                int count = list.Count;
                if (count > 0) return list[count - 1];
            }
            else
            {
                using (IEnumerator<TSource> e = source.GetEnumerator())
                {
                    if (e.MoveNext())
                    {
                        TSource result;
                        do
                        {
                            result = e.Current;
                        } while (e.MoveNext());
                        return result;
                    }
                }
            }
            return defaultValue;
        }

        public static TSource LastOrDefault<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate, TSource defaultValue)
        {
            if (source == null) throw new ArgumentNullException(nameof(source));
            if (predicate == null) throw new ArgumentNullException(nameof(predicate));
            //if (source == null) throw Error.ArgumentNull("source");
            //if (predicate == null) throw Error.ArgumentNull("predicate");
            TSource result = defaultValue;
            foreach (TSource element in source)
            {
                if (predicate(element))
                {
                    result = element;
                }
            }
            return result;
        }


        public static TSource SingleOrDefault<TSource>(this IEnumerable<TSource> source, TSource defaultValue)
        {
            if (source == null) throw new ArgumentNullException(nameof(source));
            //if (source == null) throw Error.ArgumentNull("source");
            IList<TSource> list = source as IList<TSource>;
            if (list != null)
            {
                switch (list.Count)
                {
                    case 0: return defaultValue;
                    case 1: return list[0];
                }
            }
            else
            {
                using (IEnumerator<TSource> e = source.GetEnumerator())
                {
                    if (!e.MoveNext()) return defaultValue;
                    TSource result = e.Current;
                    if (!e.MoveNext()) return result;
                }
            }
            throw new InvalidOperationException("MoreThanOneElement");
            // throw Error.MoreThanOneElement();
        }

        public static TSource SingleOrDefault<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate, TSource defaultValue)
        {
            if (source == null) throw new ArgumentNullException(nameof(source));
            if (predicate == null) throw new ArgumentNullException(nameof(predicate));
            //if (source == null) throw Error.ArgumentNull("source");
            //if (predicate == null) throw Error.ArgumentNull("predicate");
            TSource result = defaultValue;
            long count = 0;
            foreach (TSource element in source)
            {
                if (predicate(element))
                {
                    result = element;
                    checked { count++; }
                }
            }
            switch (count)
            {
                case 0: return defaultValue;
                case 1: return result;
            }
            throw new InvalidOperationException("MoreThanOneMatch");
            // throw Error.MoreThanOneMatch();
        }

        public static TSource ElementAtOrDefault<TSource>(this IEnumerable<TSource> source, int index, TSource defaultValue)
        {
            if (source == null) throw new ArgumentNullException(nameof(source));
            //if (source == null) throw Error.ArgumentNull("source");
            if (index >= 0)
            {
                IList<TSource> list = source as IList<TSource>;
                if (list != null)
                {
                    if (index < list.Count) return list[index];
                }
                else
                {
                    using (IEnumerator<TSource> e = source.GetEnumerator())
                    {
                        while (true)
                        {
                            if (!e.MoveNext()) break;
                            if (index == 0) return e.Current;
                            index--;
                        }
                    }
                }
            }
            return defaultValue;
        }
    }
}

github のソースコードから ~OrDefault 系のメソッドを取ってきて、default(T) ってなっているところを defaultValue に変更しただけ。

正確に言えば defaultValue 引数の型を TSource にしてしまっていることで、上記モチベにおける OnNotFound() の評価順序が Source からの検索後から検索前に移ってしまっている。
それが問題になる場合は defaultValue を受け取る代わりに Func<TSource> を受け取って defaultValue を返す代わりに受け取った Func<TSource> を呼び出す関数を追加。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む