- 投稿日:2020-07-01T23:33:59+09:00
[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の差
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::Rreadonlyのない関数メンバーの呼び出し
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::MJIT 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: retreadonlyのない関数メンバーの呼び出し
コピーが発生してますね。
; 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
関数メンバーにする必要性はあるでしょう。
(もちろん、不意なメンバーの書き換えに対する予防という面もあります。)
- 投稿日:2020-07-01T21:11:55+09:00
Windows 10で大容量ファイルを分割する
たまにはプログラミングも良いものだ。
1.notepadで以下のテキストファイルを作成する。保存する拡張子間違えないように。
「fileName = ~」のところを分割したいファイル名に変更する。
「ReadBytes(100000000)」のところは、分割後のファイルサイズ。ここでは100MB。split.csusing 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.cs3.プログラムを実行して、ファイルを分割する。
> 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.分割後のファイルサイズ合計と、分割前のものを比較する。
エクスプローラーで分割後のファイルを全部選択して右クリック>プロパティで確認すると良い。
- 投稿日:2020-07-01T15:52:36+09:00
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}"); } } } }
- 投稿日:2020-07-01T15:16:32+09:00
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>
を呼び出す関数を追加。