最近の活動(主に業務外)
最近のブログをまとめただけの内容ですが。Qiitaの Advent Calendar はここしばらく恒例になりつつあります。
あと、勉強会など開催しました。
スライド
www.slideshare.net
教材ソースコード
個人的にはこれで「ポカーン」という人がまだ多いのはエンジニア業界にとって由々しき事態…。
Blazor の大部分がASP.NET Core 3.0 のプロジェクトに移動した
すぐにBlazorが使えるという意味ではありませんが、Razorが標準コンポーネントになるのは素直に嬉しいところです。
機械翻訳ですが、以下の通り
BlazorとRazor Componentsプログラミングモデルのほとんどすべてのソースは、ASP.NETの中央コアリポジトリに移行しました。われわれは未解決の問題をここからそこに移行する過程にもある。
これは、RazorコンポーネントをASP.NET Core 3.0の組み込み機能として出荷するための準備です。注:クライアントサイドのBlazorは実験的なままですが、WebAssemblyランタイムを完成させるために引き続き作業します。
というわけで、見るとサーバーサイドのコンポーネントは既にASP.NET Coreのリポジトリ内に移動していました。
それにしても…。[Parameter]属性は Inherited = true にしてくれないでしょうか。継承されないので大変な不便を感じている昨今です。 自分で更新して Pull Request 打ってアピールするのが正しいのでしょうね…。
Blazor で PWAを作る
最近C#も良くなってきましたが、Web開発環境も良くなってきました。 いろいろなことができるのは良いことです。
今回はBlazor 0.7.0 で PWA(プログレッシブWebアプリ)を作ってみました。
Blazor 0.7.0 の環境構築はこちらのとおりです。
Blazor 0.7.0 experimental release now available | ASP.NET Blog
おおまかには以下が必要です。
- .NET Core 2.1 SDK(2.1.500以降)
- ASP.NETおよびWeb開発ワークロードを選択したVisual Studio 2017(15.9以降)
- Visual Studio Marketplace の最新の Blazor Language Services extension
- コマンドラインで以下を実行
dotnet new -i Microsoft.AspNetCore.Blazor.Templates
Blazorのプロジェクトの立ち上げ方は前回のブログのスライドを参考にしてください。
Blazor を用いたスクリプトレスWebアプリ開発の可能性 - C#+WPFチューニング戦記
テンプレートで生成したプロジェクト *.Server に NuGetから以下を追加します。
- WebEssentials.AspNetCore.PWA
*.Server側のStartup.csのConfigureServicesを以下のように書き換えます。
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddProgressiveWebApp(); services.AddResponseCompression(options => { options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[] { MediaTypeNames.Application.Octet, WasmMediaTypeNames.Application.Wasm, }); }); }
*.Client側のwwwroot に PWA に必要な manifest.json を追加します。 以下は例です。重要なのは記載したファイルをちゃんと実在させることです。(iconsとか)
{ "name": "Progressive Web App", "short_name": "PWA", "description": "Creating PWA in ASP.NET Core.", "icons": [ { "src": "/images/icon192x192.png", "sizes": "192x192" }, { "src": "/images/icon512x512.png", "sizes": "512x512" } ], "display": "standalone", "start_url": "/" }
index.html を大体以下のように書き換えます。manifestの追加とserviceWorkerの追加です。
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width"> <title>BlazorPwaSample</title> <base href="/" /> <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" /> <link href="css/site.css" rel="stylesheet" /> <link rel="manifest" href="/manifest.json" /> </head> <body> <app>Loading...</app> <script src="_framework/blazor.webassembly.js"></script> <script>'serviceWorker' in navigator && navigator.serviceWorker.register('/serviceworker')</script> </body> </html>
PWAを期待通りにブラウザーから切り離して動かすにはhttpsでなければなりません。IIS Express上で動かしてみるとよいでしょう。 ちゃんとPWAとして動いているかどうかは、Chromeの右上のメニューに「Progressive Web App で開く」という項目が出ているかでわかります。
切り離してみると、この通りブラウザではない普通のアプリケーション風の形になります。
Blazor0.7.0の目玉機能である「カスケードパラメーター」と「デバッグ機能の強化」はまだこれから体験してみようと思っています。
Blazor を用いたスクリプトレスWebアプリ開発の可能性
Blazor 0.6.0 がリリースされましたね
Blazor 0.6.0 experimental release now available | ASP.NET Blog
Ver0.6.0ではジェネリック型やテンプレートが使えるようになったようです。
Blazorを使ってみた(3)
腰痛で倒れていました。不甲斐ない…。
どこまで再利用性や汎用性を高められるかと思って、どうせならXAMLでMVVMをするときのように書きたいと考え、 Resources.cshtml などを作り始めています。
@using Foo.Shared; @switch (DataContext) { case User user: <User DataContext=@user></User> break; } @functions { [Parameter] object DataContext { get; set; } }
丁度C#7.xでパターンマッチングがとても便利になったので、switch文でDataContextに応じた展開がきれいに書けるようになっているのです。パターンマッチング自体はDataTemplateSelector並に柔軟な記述が可能なので、DataTemplateSelectorはほぼ不要になりそうです。
これに加えてContentControl.cshtmlやContetnPresenter.cshtml のようなものを作れば、XAMLに近い記述感をHTML上で実現できる日も遠くありません。
現時点でエラーは出ていないのですが、万全に動くものとなるかどうかはこれから確認します。
JavaScriptをどこまで使わないことができるか。 いや、C#でどこまでやれるか。まだまだ挑戦できそうです。
Blazorを使ってみた(2)…再
役に立つ情報を書けていなかったので1つ前の記事は削除しました。
Blazorに取りつかれて毎晩少しずつコードを書いていると、どんどんコントロールの整理が進んでいきます。今のところ業務的なDBに対するCRUDを行なう画面を作ってもJavaScriptの記述量は0行という状態です。画面遷移等もC#で書けるので、極めて特殊な処理以外はC#のみで記述できることがハッキリしました。
ただし、デバッグ環境については出来るだけ正確に書いておくべきかと思いました。
- サーバーコードはVisualStudioでデバッグ可能です。
- クライアントコードはかなりデバッグ困難です。VisualStudioからはブレイクポイントを置けませんし、ブラウザのデバッガーでもWebAssembly内部の狙った箇所にブレイクポイントを置くのは困難なはずです。
それでも開発したい場合のために参考情報を記載します。
- Chromeでは例外発生時に.cshtml上の行番号とスタックトレースを見ることが出来ます(C#コード中でもOK)。
- 業務的なアプリケーションの場合はサーバーサイドの処理を充実させる指向にすることで従来のMVC並の開発効率は維持できます(C#を使えることが嬉しい場合は多少勝る可能性もあり。うーん苦しいかな…?)。
- コントロール群を非常に簡素に記述できます。C#のプロパティとのデータBindとイベント処理が非常に強力なおかげです。XAMLで使うみたいなコントロールもその気になれば出来そうです。
このおかげで、クライアント側に有力なデバッガーがなくても大体やっていける局面が増えています。
cshtmlは再利用性がとても高いので、共通化できるところをひたすら共通化して行ったら、ものすごく短く綺麗にまとまるのではないだろうかと考えました。
それでまずは汎用性向上のために bootstrap を綺麗にラッピングしてみようかと考えたのですが…。 ただその際にふと、自分が考えている程度の事は他の人も考えていそうだと思って探したら、居ました。 その名もBlazorStrap
Blazor Strap Documentation Site
GitHub - chanan/BlazorStrap: Bootstrap 4 Components for Blazor Framework
書いているのは.cshtmlですが、このくらいまでは整理できそうだよなと思っていたところまでを綺麗に纏めてくれていて素敵です。 ここまでは何も考えずNuGetで組み込んで良いと思います。
少し前にみんな大興奮だったBlazorを使ってみた(1)
あんまり入門記事は書かないタイプの人なのですが、たまにはですね。 触ってみたらちょっと書きたくなったのです。
まず、以下を取りそろえます。
- VisualStudio2017(最新版 15.8.2)←何でも作るのでフルインストールしてます
- .NET Core 2.1(最新版 2.1.401)
- Node.js (最新版 10.9.0)
- ASP.NET Core Blazor Language Services(VisualStudio2017のプラグイン)
一応 Blazor のドキュメントに沿って全部入れてみました。セットアップさえしてしまえば特別な設定は不要です。
VisualStudio2017のプロジェクトの新規作成で「ASP.NET Core Web アプリケーション」を選択します。 プロジェクト名を入力後、「Blazor」を選択します。スタンドアローンだそうです。
とりあえず動かそうと思って Ctrl+F5 で実行…すると 502.5 エラーが出ました。 何だろうと思ったら global.json 内の ”sdk”の ”version”が"2.1.300"になっていたせいでした。サクッと"2.1.401"に書き換えてもう一度 Ctrl+F5 。 動きました。
ひょっとして余計なものとか含まれていたらすみません。 初めて触るものはおっかなびっくり触っているので、アドバイスをいただけたら助かります。
.cshtml + Razor で書けるので思った以上に理想に近い形で記述できます。 これは楽しい。多分続きます。
ToArrayを高速化する話
IEnumerable
https://referencesource.microsoft.com/#System.Core/System/Linq/Enumerable.cs,ed118118b642d9d4
要素数が分かるICollectionと分からないIEnumerableで実装を分けるのは実に合理的です。 このやり方が最速であるかというとIEnumerable側の実装については幾つか改善余地があります。 主に配列の初期値と拡張時の倍率です。
ベンチマークの結果は以下の通りです。大体、20%高速化していると考えられます。
Method | Mean | Error | StdDev |
---|---|---|---|
ToArrayFast | 134.3 us | 0.5700 us | 0.5053 us |
ToArray | 160.7 us | 0.7790 us | 0.7287 us |
ベンチマークのソースは以下のようになります。Guidの生成時間が計算に含められないように、あらかじめ作っておきます。
private class GuidGenerator : IEnumerable<Guid> { private readonly Guid[] _guids = new Guid[10000]; public GuidGenerator() { for (int idx = 0; idx < _guids.Length; idx++) _guids[idx] = System.Guid.NewGuid(); } public IEnumerator<Guid> GetEnumerator() { for (int idx = 0; idx < _guids.Length; idx++) yield return _guids[idx]; } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } private readonly GuidGenerator _guids = new GuidGenerator(); [Benchmark] public void ToArrayFast() { var items = _guids.ToArrayFast(); } [Benchmark] public void ToArray() { var items = _guids.ToArray(); }
実装はこちらです。
public static class CollectionExtensions { public static T[] ToArrayFast<T>(this IEnumerable<T> items) where T : struct => new Buffer<T>(items).ToArray(); } internal struct Buffer<TElement> { private const int InitialBufferSize = 64; private const int ExpandRatio = 4; private readonly int _count; private readonly TElement[] _items; public Buffer(IEnumerable<TElement> source) { if (source is ICollection<TElement> collection) { _count = collection.Count; if (_count == 0) { _items = Array.Empty<TElement>(); } else { _items = new TElement[_count]; collection.CopyTo(_items, 0); } } else { _count = 0; _items = new TElement[InitialBufferSize]; foreach (var item in source) { if (_items.Length == _count) Array.Resize(ref _items, _count * ExpandRatio); _items[_count++] = item; } if (_count == 0) _items = Array.Empty<TElement>(); else if (_items.Length != _count) Array.Resize(ref _items, _count); } } public TElement[] ToArray() => _items; }
細微な部分ではまだ最適化余地が残っていますが、要点は如何にして Array.Copy や Array.Resize を呼び出す頻度を下げるか、ということになります。
言い換えると過剰なメモリを確保したりGCが過度に働かないか、ということになります。
最後に確定するバッファ以外のメモリは全てGen0であることと、メモリの確保回数は通常のToArrayより少ないことを考えれば瞬間で僅かに大きいメモリを持ったとしても問題ではありません。
なお、ここで少し考慮すべきポイントは確保したメモリは0Fillされる分だけメモリ書き込みが発生するという事です。 加えて大きすぎるとLOH(Large Object Heap / 85000バイト以上のバッファ)化してなお性質の悪いことになります。
Array.Resizeを減らしたいからといって突然大きくするとそれはそれでトータルでは性能を損ないます。
計測と分析を要する箇所です。
# C#7.3にふさわしいやり方もいろいろ検討してみたのですが、自分のやった範囲では何をしても遅くなりました。誰かアイデアありませんか?
共用体でGuidすら速くできる
CEDEC2018の傍ら、こんなベンチマークを思いつきで書いていました。 まずは結果からご覧ください。Meanの値から、だいたいSystem.Guidよりも2倍は高速です。
Method | Mean | Error | StdDev |
---|---|---|---|
ImmutableGuid_In | 20.83 us | 0.1102 us | 0.0977 us |
ImmutableGuid | 22.46 us | 0.1960 us | 0.1530 us |
SystemGuid | 48.51 us | 0.5408 us | 0.5058 us |
StructLayoutやFieldOffsetって、Win32コールやバイナリシリアライズ以外の使いみちをあまり考えたことがなかったのですが、C#7.3になって共用体の意味をちょっと見直してみた。というのが今回のベンチマークです。
Guid.GetHashCodeとGuid.Equalsのベンチマークですが、Guidの値生成を再実装するというのはいかがなものかと思ったのと、Guidは単に比較して等しいかどうかが重要という原点に立ち返りました。
そして共用体と readonly struct の合わせ技で、高速に辞書のキーにすることができるGuidというのを作ってみました。
以下、ベンチマークと実装です。簡単ですが威力絶大です。
private int[] hashSets = new int[10000]; private bool[] equals = new bool[10000]; [Benchmark] public void ImmutableGuid_In() { var guid1 = new Immutable.Guid(Guid.NewGuid()); var guid2 = new Immutable.Guid(Guid.NewGuid()); for (int idx = 0; idx < 10000; idx++) { hashSets[idx] = guid2.GetHashCode(); // 意識的に in とすることで同名の in引数のメソッドを選択できます。 equals[idx] = guid1.Equals(in guid2); } } [Benchmark] public void ImmutableGuid() { var guid1 = new Immutable.Guid(Guid.NewGuid()); var guid2 = new Immutable.Guid(Guid.NewGuid()); for (int idx = 0; idx < 10000; idx++) { hashSets[idx] = guid2.GetHashCode(); equals[idx] = guid1.Equals(guid2); } } [Benchmark] public void SystemGuid() { var guid1 = Guid.NewGuid(); var guid2 = Guid.NewGuid(); for (int idx = 0; idx < 10000; idx++) { hashSets[idx] = guid2.GetHashCode(); equals[idx] = guid1.Equals(guid2); } } namespace Immutable { [StructLayout(LayoutKind.Explicit)] public readonly struct Guid : IEquatable<Guid> { public static readonly Guid Empty = new Guid(); [FieldOffset(0)] private readonly System.Guid _guid; [FieldOffset(0)] private readonly int _a; [FieldOffset(0)] private readonly ulong _low; [FieldOffset(8)] private readonly ulong _high; [FieldOffset(4)] private readonly int _bc; [FieldOffset(8)] private readonly int _defg; [FieldOffset(12)] private readonly int _hijk; public Guid(in System.Guid guid) : this() => _guid = guid; public static implicit operator System.Guid(in Guid source) => source._guid; public static implicit operator Guid(in System.Guid source) => new Guid(source); public static Guid AsRef(in System.Guid guid) => new Guid(guid); public override int GetHashCode() => _a ^ _bc ^ _defg ^ _hijk; public bool Equals(Guid other) => _low == other._low && _high == other._high; // in も用意しておくと辞書の独自実装時により高速にできます。IDictionary<T> を拡張してみようかと思っています。 public bool Equals(in Guid other) => _low == other._low && _high == other._high; public override bool Equals(object obj) { if (obj is Guid guid) return Equals(guid); return false; } } }
なお、Guidからのキャスト、Guidへのキャストは若干コストがあります。
「Guidを得たらずっと Immutable.Guid の形で in引数でひたすら渡し続け、APIなどに戻すときにGuidにする。」というような使い方を想定しています。 Guidの辞書を1億回引くようなプロジェクトにはうってつけの拡張だと思いませんか?
Rect.Containsもreadonly struct + in で高速化できる
RectとImmutableRectのContainsのベンチマークです。 IsEmptyの判定を排除できればもっと速度が出るのですが、互換性を重視してあえて削除していません。
なんでこれほどRectに拘るかといいますと、仮想化は矩形の判定の塊だからです。仮想化を高速化したいのに矩形の判定を横着するのは勿体無い、というわけです。
Method | Mean | Error | StdDev |
---|---|---|---|
Rect_Rect_Contains | 117.81 us | 0.2385 us | 0.1862 us |
ImmutableRect_ImmutableRect_Contains | 39.04 us | 0.1703 us | 0.1593 us |
ImmutableRect_Rect_Contains | 62.09 us | 0.3684 us | 0.3446 us |
これが、今回の範囲に関係するソースコードです。
[Benchmark] public void Rect_Rect_Contains() { var rect1 = new Rect(0, 0, 5000, 5000); for (int idx = 0; idx < 10000; idx++) { _contains_results[idx] = rect1.Contains(new Rect(0, 0, idx, idx)); } } [Benchmark] public void ImmutableRect_ImmutableRect_Contains() { var rect1 = new ImmutableRect(0, 0, 5000, 5000); for (int idx = 0; idx < 10000; idx++) { _contains_results[idx] = rect1.Contains(new ImmutableRect(0, 0, idx, idx)); } } [Benchmark] public void ImmutableRect_Rect_Contains() { var rect1 = new ImmutableRect(0, 0, 5000, 5000); for (int idx = 0; idx < 10000; idx++) { _contains_results[idx] = rect1.Contains(new Rect(0, 0, idx, idx)); } } public readonly struct ImmutableRect : IEquatable<ImmutableRect> { public readonly double X; public readonly double Y; public readonly double Width; public readonly double Height; public ImmutableRect(double x, double y, double width, double height) => (X, Y, Width, Height) = (x, y, width, height); public static implicit operator ImmutableRect(Rect source) => new ImmutableRect(source.X, source.Y, source.Width, source.Height); public static implicit operator Rect(in ImmutableRect source) => new Rect(source.X, source.Y, source.Width, source.Height); // 2018/08/20 より高速なコードに更新 public bool Contains(in ImmutableRect rect) { return Width >= 0 && Height >= 0 // !IsEmpty && X <= rect.X && Y <= rect.Y && X + Width >= rect.X + rect.Width && Y + Height >= rect.Y + rect.Height; } }
C#7.3 in this ValueTuple が速い
C#7.3でこんなベンチマークをしました。 in this ValueTuple の組み合わせが出来ると本当に高速です。
[Benchmark] public void DoubleMaxLegacy() { for(int idx=0 ; idx<10000;idx++) result[idx] = Math.Max((double)(10000 - idx), (double)idx); } [Benchmark] public void DoubleMaxNew() { for(int idx=0 ; idx<10000;idx++) result[idx] = ((double)(10000 - idx), (double)idx).Max(); } // 新しいMaxはこちら。演算内容はMath.Max()と全く同じです。 public static double Max(in this ValueTuple<double, double> value) { return value.Item1 > value.Item2 || double.IsNaN(value.Item1) ? value.Item1 : value.Item2; }
こちらが結果です。Meanを見ると分かりますが、2倍近く高速です。侮れません。
Method | Mean | Error | StdDev |
DoubleMaxLegacy | 28,950.1 ns | 158.509 ns | 148.269 ns |
DoubleMaxNew | 15,761.9 ns | 113.574 ns | 106.237 ns |
暗黙のキャストができない以外は極めて優秀です。 ベンチマークは BenchmarkDotNet を使っています。
#this は速度に関係あるのかって?多分ないです。こう書きたかったから。
最速C #7.x
そんな題名で社内勉強会を実施しました。
C#7,xになってから、最速&安全なコードの書き方は変化したと思います。
値型のデフォルトコンストラクタのコスト
C#における struct は引数が無いコンストラクタを書くことができないのですが、
return new Size();
こういうのはコスト0で戻り値に格納してくれるのだろうかという疑問について、調査しました。
極めて厳密に最適化をすると、このコンストラクタはインライン展開可能で、インライン展開後にさらに最適化すれば戻り値を0フィルするだけでいいので、もしかしたらそのくらいまでなら既存のJITがやってくれるのではないか?という仮説があったためです。
結論から言いますと、現時点 .NET Framework 4.6 では32bit/64bitを問わずJITは言語規約にかなり忠実な展開をします。
つまり、スタック上に1回 0フィルした構造体を作成して戻り値に格納すべく値をコピーします。
最速で書こうと思えば、
static readonly Size _zeroSize = new Size();
:
:
return _zeroSize;
のように記述する必要があります。
こうなると、Size.Emptyが -∞ の値を持つサイズなのが恨めしく映ります。Size.Default も欲しかったなと思います。
ちなみにこの場合も最適化は若干弱いもので、転送元のアドレスをレジスタにロードする処理が含まれます。
デフォルトコンストラクタを用いた値を返す場合、戻り値を直接 0 フィルする最適化が欲しいと思います。
インライン展開時はさらに高度に扱って欲しいと思います。
転送命令1~4個分程度ではありますが、この程度をなぜ最適化できなかったのかという思いはどうしても残ります。
一応、シンプルなコードを作成して最適化の有無に無関係にこのコストはあることをアセンブラ上で確認したつもりではあります。
しかし、コードのパターンによって変わる可能性があることは否めません。
別の答えをお持ちの方がいらっしゃいましたら是非お知らせください。
なお、 new を書かないでも良いものは最適なコードになっています。リテラル値なら良いということなのでしょう。
余談ですが、 default(Size) は new Size() の糖衣構文なので、まったく同じ命令を出力します。