sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

C# パフォーマンス高速化に関するメモ

この記事は「C# Advent Calendar 2024」に参加しています。シリーズ2の12月13日の内容です。

C# のパフォーマンス高速化に関することを学習した内容です。下記のような Tips があったので、個人的にサンプルコードを書いたり調べたりして学習した内容になります。(内容は加筆あり未記載もあり)

ポイントは「ソフトウェアは速くて困ることはない」

  • ヒープアロケーション(割り当て)を(避けれるところは)避ける
    • メモリの再利用とスタック領域の活用を徹底
  • 非同期 I/O を使う
    • CPU は高価なリソースなので、待ち時間を与えず使い切る
  • 計算効率を上げる

C# におけるメモリ管理方法は値型「スタック」と参照型「ヒープ」の2種類があります。一般的にヒープの領域の "確保" はスタックと比べると重たい処理になります。

別の言い方でのポイント:

  • 実装時からやることやって損しない
    • よく通るコードパス/共通ライブラリは特に目を向ける
  • 速いコードを書く癖をつける
    • 練習と思って取り組むほうがいい

読むときのための用語メモ:

値型の利点は、スタックに値を置く(ヒープを使わない)ことによる性能向上です。ボックス化(ヒープ領域の確保)が起きると、値型の利点は失われます。

パフォーマンスに影響の大きい絶対に守るべきこと

コレクションの型あわせ

List 型を配列型に変換する。不可避な理由がないなら無駄なメモリコピーが発生するため適切ではない。

List<int> values = [1, 2, 3, 4];

DoSomething(values.ToArray());

void DoSomething(int[] values)
{
    ...
}

ただし、後述の AsSpan()(Span<T>)は、逆に最適化になることがあります。

int[] array = { 0, 1, 2, 3, 4, 5, 6, 7, 8 };
foreach (var i in array.AsSpan()[2..6]))
{
    ...
}

ループ内の線形探索

コレクションのデータ数を考えて、たくさんデータがあるときはどうすれば取得が速いのか。First は線形探索なので効率とかじゃないよね。

foreach (var sample in samples)
{
    // BAD
    var bad = sample.First(p => p.id == id);

    // GOOD
    var good = sample[id];
}

線形探索……先頭から順番に比較を行い、見つかれば終了する。

コレクションの初期容量を指定

動的なバッファ拡張は、なるべく避ける。

初期容量 (capacity) を指定しなければ、最後の要素を追加するときに2倍の内部バッファ確保と全要素のコピーが発生するため非効率。

パフォーマンスに悪影響があるのは、 Count 3 のタイミング。コレクションの容量を上げるために、既存コレクションから新しいコレクションにコピーが発生する。

int[] values = [1, 2, 3, 4];
var list = new List<int>(capacity: values.Length / 2);

foreach (var x in values)
{
    list.Add(x);
    Console.WriteLine($"Capacity: {list.Capacity}, Count: {list.Count}");
}
Capacity: 2, Count: 1
Capacity: 2, Count: 2
Capacity: 4, Count: 3
Capacity: 4, Count: 4

Result と Wait はやめる

スレッドの処理をブロックするので、スレッドが占有される。想像しているよりもデッドロックしやすい。

結果的にリソース効率が低下する。

public int DoBadSomething()
{
    var result = CallAsync().Result;
    return result + 1;
}

public async Task<int>  DoGoodSomething()
{
    var result = await CallAsync();
    return result + 1;
}

本質的ではないけど、デッドロックを避ける対策の一例。処理するスレッドが変えて待つけど、スレッドが効率的に利用されているとは言えない。

public int DoBadSomethingSafe()
{
    var result = CallAsync().ConfigureAwait(false).GetAwaiter().GetResult();
    return result + 1;
}

環境

最速の開発/実行環境

最新の .NET を利用しておくこと。

.NET Framework よりも何倍も速い。C# 7.x 以降は、パフォーマンスを意識した改善が多い。基本的には後方互換があるので更新して最新の言語機能を使う。

  • MemoryPack
  • MessagePack for C#
  • FastEnum

構造体

Understanding C# Struct All Things より

構造体の優位性

ガベージコレクションの頻度を下げる

  • GC の実行時はパフォーマンスが瞬間的に大きく低下する
  • 特に Unity のようなフレームベースのアプリは気をつける

メンバーへのアクセスが高速になる

  • 値型:スタック領域にあるので、そのまま直にアクセス可能
  • 参照型:ヒープ領域から値をたどってアクセスする

思ったよりも構造体の挙動は、言語仕様の知識が必要なところがあります。いくつかの作法を知っていることで、効率化される感じです。

構造体の欠点

高頻度で値のコピーが発生する

  • 引数/戻り値/変数への代入/関数呼び出し
  • サイズの大きい構造体の場合は、逆にコストになる(16 bytes 目途)

継承/多態ができない

  • 現状は interface + 拡張メソッドを駆使する

インターフェース以外の継承ができない理由を考えると、struct は「メモリをマッピングした構造だから」という考え方がわかりやすいと思います。裏付けとして StructLayout はデフォルトだと Sequential です。(class のように)データの並びをアライメントにあわせた最適化はされません。値型として、固定サイズを守ることが優先されると考えることができます。

インターフェースを付与できるのは、メモリ構造に影響を与えずに多様性を追加するだけだからです。もともと、C言語の構造体は完全な値型でメソッドを作ることができませんでした。しかし、C#の構造体は最初から、値型としての特性を持ちつつメソッドを持てます。同じ理由だと考えています。

参照渡し

性能劣化に直結するコピーを抑制する。構造体のサイズが大きいときは検討する。

具体的にすると以下の例だと a は PassThrough メソッドに参照を渡していて a のメモリ上のアドレスが共有される動きになります。

さらに ref var b は a を参照しているから a と b は同じ値であり、b を変更すると a の値も書き換わる。

private void TestRef()
{
    var a = 1; 
    ref var b = ref PassThrough(ref a);

    // b を変更すると a にも影響する
    b = 2;

    Console.WriteLine(a); // output 2
}

private ref int PassThrough(ref int b)
{
    ref var c = ref b;
    return ref c;
}

ref 戻り値は C# 7.2 から利用可能になったので 2017 年末からの機能です。また ref readonly のような戻り値の書き方もできる。(が、後述のようにパフォーマンスの観点では注意が必要)

このほかにも「参照渡しだけど読み取り専用」なら in を使えるので、大きい構造体を渡すときはパフォーマンス改善につながる引数の指定になる。

従来の out は「参照渡し」。あるメソッドの中で、メソッド外の変数の値を書き換えることができるのに対して in は「入力用」を明示します。

コンパイラの動作としては in/out は ref なので、引数違いのオーバーロードはできない。

補足として、構造体ではなくクラスの場合はあまりパフォーマンスの改善に効果がない。理由は参照型なのでポインタのような動きをするヒープ領域を利用しているため、データそのものがコピーされることがない。

ref readonly/in の難しさ

Defensive Copy が発生してしまう。

引数の場合は in で、戻り値の場合は ref readonly として、この問題がどちらの場合でも発生してしまう。(後述の Generics は別)

プログラムの都合として、呼び出し側は、メソッドの内部で値が書き換わっていないことを保証する方法を持たないので、あらかじめメソッドを呼んだ時点で無条件にコピーを作ってしまう動きのことを Defensive Copy という。

対策:

  • 構造体を readonly にしておく
  • 関数内でフィールドの書き換えがないことを保証しておく
readonly struct ReadOnly
{
    public readonly int X;
}

struct Foo
{
    ..
    public readonly int Add() => X + Y; // GOOD
    public int Sub() => X - Y; // BAD コピー発生
    ..
}

コピー抑制

構造体の拡張メソッドを作るといは、参照渡しにする。Generics における in 引数はできない。

これはコンパイラが型は値型か参照型かを判断できていないので、仕様として in 引数にできない。(参照型のときは参照渡しである必要がない)

static void Process<T>(in this T data) // エラー
{
}

static void Process<T>(ref this T data) : where T : struct // OK
{
}

in を使った overload をするときも、struct 自体を readonly にしておく。

readonly struct Complex
{
    public double R { get; }
    public double I { get; }

    // in 引数が認められるようになった
    public static Complex operator +(in Complex x, in Complex y)
        => new Complex(x.R + y.R, x.I + y.I);
}

というより、基本的には in は readonly な struct に使うもの、という考え方になるはず。

ValueTuple

匿名型の歴史みたいなもの。LINQ といった局所的なところで使うとよいもの。ValueTuple は、値を変更できる構造体として考えておく。

コレクションからコレクションを選択する動作なうえ、ValueTuple はヒープを使わないから、すごくパフォーマンス上の相性がいい設計だと思う。

// ValueTuple : スタック利用
var q1 = collection.Select(x => (value: x, power: x * x));

// 匿名型: ヒープ利用
var q2 = collection.Select(x => new { Value = x, Power = x * x });

// Tuple : ヒープ利用(名前を付けられない)
var q3 = collection.Select(x => Tuple.Create(x, x * x));

Span

連続したメモリ領域を直接参照できる。unsafe じゃない managed な状態でアクセスできる。配列や文字列から、メモリの再確保をしない部分的な参照ができる。

const string text = “ab123cd”;
var span = text.AsSpan(2, 3);
var sub = text.Substring(2, 3); // 123

中身を read するだけなら span を上手く活用することで、無駄なコピーを避けて高速化させることができる。

でも、内部的にコピーが発生しているかどうかなんて、言語の理解が深まらないと最適化できない。

string.Create

string.Create は Span を使っているので、無駄なオブジェクトを生成させることなく文字列を作成することができる。

以下は byte 型の値を文字列に変換する(例えば、文字列 "01101100" にする)もので buffer が span なので StringBuilder のような中間変数が存在しない。とてもテクニカルな技術だと思った。

static string ToBitString(byte value)
    => string.Create(8, value, (buffer, state) =>
{
    const byte on = 0b_0000_0001;

    for (var i = 0; i < buffer.Length; i++)
    {
        buffer[buffer.Length - 1 - i]
        = ((state >> i & on) == on) ? '1' : '0';
    }
});

ボックス化を回避する

古い産廃を使ってはいけない

  • System.Collections
    • ダメ絶対
  • System.Collections.Generics
    • 変更先

非 generics コレクションなので使わないほうがいい。名前空間が出ていたら注意したほうがいい。

System.Enum の問題点

値を代入するとボックス化してしまう。

// ボックス化する
static void Foo(Enum value)
{}

// Generics 制約をするとボックス化しない
static void Foo<T>(T value) where T : struct, Enum
{}

構造体を interface 型として扱う

正直、構造体の高速化テクニックはマニアックだと思う。

struct は object するポイントとしてインターフェースを経由すると Box 化が発生してしまう。が、そこで generics を使うと脱仮想化になる。

// 引数でbox 化が発生して遅い
static void Interface(IDisposable x)
    => x.Dispose();

// .NET Core 2.1 以降の場合
// 脱仮想化という最適化がかかる
static void NonGeneric(X x)
    => ((IDisposable)x).Dispose();

// 安定して高速
static void Generic<T>(T x)
    where T : IDisposable
    => x.Dispose();

一番よく言われるようなのが、比較の equal 関係なので、そういったところは record を使うことでも回避できると思います。

知らない間に作られるインスタンスに気を配る

クロージャの変数生成

変数のキャプチャー = 隠しクラスの生成ということになります。なので、ヒープ確保と利便性はトレードオフの関係です。

具体的には、ラムダ式や匿名メソッド内で外部スコープの変数を参照するとき、変数がクロージャにキャプチャされることを指しています。この「クロージャ」はヒープ上にオブジェクトとして生成されることになります。

以下は id がキャプチャされる例です。Visual Studio では "=>" の部分にカーソルを合わせると「キャプチャされているかどうか」を知ることができます。

static async Task<Person> GetAsync(int id)
{
    var people = await QueryFromDbAsync();
    return people.FirstOrDefault(x => x.Id == id);
    }

なので、フィルタリング部分条件をラムダ式の外に置く、拡張メソッドにして自作をしたりすることになる。

static async Task<Person> GetAsync(int id)
{
    var people = await QueryFromDbAsync();
    foreach (var person in people)
    {
        if (person.Id == id) return person;
    }
    return null;
}
public static T? FirstOrDefault<T, TState>(
this IEnumerable<T> source, TState state, Func<T, TState, bool> predicate)
{
    foreach (var x in source)
    {
        if (predicate(x, state))
        return x;
    }
    return default;
}

また、ラムダ式や匿名メソッドは外の変数をキャプチャできてしまうため、変数名として x や y や i を使ったつもりが、外の変数をキャプチャしてしまってバグになることがあります。そういうときは、静的ラムダ式を使うことで、外部スコープの変数が入り込まないように(ある種)明示することになります。

var x = 10;
var result
    = Enumerable.Range(0, 10)
    .Where(static x => x % 2 is 0)
    .ToDictionary(static x => x, static y => x * x); // 警告になる

時間とリソースを有効に使う

非同期 IO を使う

await している間にできることがある。

// これなら通信待ちを別処理に有効活用できる
var url = "....";
var client = new HttpClient();
var rss = await client.GetStringAsync(url);
var node = XElement.Parse(rss);

並列処理

それぞれの処理が独立しているなら、Parallel にすると処理時間を短縮できるかも。

var t1 = LoadFile1Async();
var t2 = LoadFile2Async();
var t3 = LoadFile3Async();

await Task.WhenAll(t1, t2, t3);

キャッシング

まず考え方として Enum.GetValues はリフレクションを内部的に利用しているため、比較的コストが大きい操作になっています。

なので、この処理を繰り返し実行することは避けたい。そこで、キャッシングを利用することで、一度だけ値を取得して、以降は保存された値を使うことでパフォーマンスを上げる。

public static class FastEnum
{
    public static IReadOnlyList<T> GetValues<T>()
        where T : struct, Enum
        => Cache<T>.Values; // キャッシュを直接参照

    // 静的 Generics 型のフィールドにキャッシュを持つ
    private static class Cache<T>
    where T : struct, Enum
    {
        public static readonly T[] Values;
        static Cache()
            => Values = (T[])Enum.GetValues(typeof(T));
    }
}

ただし、キャッシュするサイズが大きくなるとメモリ消費が大きくなる。キャッシュされた値が変更される場合があると、キャッシュの無効化や更新が必要になるのでよくない。

List の高速イテレーション

コレクションのデータを Add/Remove/Clear などの操作をせずに、大量のデータにアクセスする場面では Span が有効なことがある。リストを変更しないことは重要。

List<int> list = [0, 1, 2, 3, 4];
var span = CollectionsMarshal.AsSpan(list);

foreach (var x in span)
{
// Do something with x
}

Frozen Collections

Frozen Collection はコレクションを作成した後は変更ができません。スレッドセーフでマルチスレッド環境で安全に利用できるのも特徴。

データが固定化されたことで、検索が最適化されています。Contains なども高速化。

単純なことなので、Frozen できることを覚えていれば OK だと思います。

var data = new Dictionary<int, string>
{
    { 1, "One" },
    { 2, "Two" },
    { 3, "Three" },
};

var frozenData = data.ToFrozenDictionary();

構造体は yield return しない

これもおそらく構造体を利用することでヒープアロケーションを回避するテクニックだと思います。値型なので、キャッシュ効率が挙がって、頻繁なイテレーション部分で性能を向上させるテクニックのはずです。

foreach や LINQ は GetEnumerator を使ってイテレーションを開始するので、構造体が返されるときは、ここまでの例のようにインターフェース型 IEnumerator<T> として扱われると、暗黙的にボクシングが発生する効率の悪さもある。

yield return は状態マシンなので言わんとするところは同じはず。

class Program
{
    static void Main()
    {
        var range = new RangeEnumerable(1, 10);

        foreach (var num in range)
        {
            Console.WriteLine(num);
        }
    }
}
public struct RangeEnumerable : IEnumerable<int>
{
    private readonly int _start;
    private readonly int _end;

    public RangeEnumerable(int start, int end)
    {
        _start = start;
        _end = end;
    }

    public Enumerator GetEnumerator() => new Enumerator(_start, _end);

    // 明示的に IEnumerator<int> を使いたい場合
    IEnumerator<int> IEnumerable<int>.GetEnumerator() => GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

    // 内部構造体で IEnumerator<int> を実装
    public struct Enumerator : IEnumerator<int>
    {
        private readonly int _end;
        private int _current;

        public Enumerator(int start, int end)
        {
            _current = start - 1;
            _end = end;
        }

        public int Current => _current;

        object IEnumerator.Current => Current;

        public bool MoveNext()
        {
            if (_current < _end)
            {
                _current++;
                return true;
            }
            return false;
        }

        public void Reset() => throw new NotSupportedException();

        public void Dispose(){}
    }
}

補足:C# のコンパイラは、特定のインターフェース型を直接指定しなくても、対応するメソッドやプロパティが存在していれば、コードを正しく動作させます。これは型が厳密にインターフェースを実装している必要が無いという点で "duck typing" と呼ばれるものです。

参考

WinGet を利用した Python の更新(windows 編)

Windows 環境で Python を更新するときの流れを記録しました。

更新の必要有無を確認

とりあえず、デフォルトの Windows 環境で Python のチェック方法を記録します。コマンドプロンプトを開いて、バージョンをチェックする。

python -V

ここで「python3- V」というコマンドが紹介されていることもあります。Windows 環境では Python 3.X をインストールすると python コマンドが割り当てられるみたいです。python3 は自動的に割り当てられないことがあります。

自分の環境では以下のようになっていました。Windows Store App の Python だけだと、コマンドプロンプトで「python3 -V」を入れたときに「Python」が返ってくる状態になるみたいです。

where python3
> C:\Users\#user-name#\AppData\Local\Microsoft\WindowsApps\python3.exe
where python
> C:\Users\#user-name#\AppData\Local\Programs\Python\Python312\python.exe
> C:\Users\#user-name#\AppData\Local\Microsoft\WindowsApps\python.exe
py -0p
-V:3.12 *        C:\Users\#user-name#\AppData\Local\Programs\Python\Python312\python.exe
コマンド python バージョン 説明
python 環境依存 (3.X or 2.X) デフォルトの python を実行
python3 3.X Python 3.X を明示的に実行(主に Linux 用)
py 最新の python バージョン管理が可能 (主に Windows 用)

なんで、Windows 環境では py コマンドが python3 コマンドみたいなものなので、便利だと思います。とはいえ、バージョン更新までしてくれるわけではない。

一般的には、それは「役割が違うよね」ということになるので、やはり「パッケージマネージャ」に頼る必要があります。しかし、Windows はデフォルトだとパッケージマネージャの用意がない(はずな)ので、結局マニュアルで HP からダウンロードしてアップデートすることになる……のですが、今回は「winget」を利用してアップデートする例を示します。

winget

すこし前までは windows 環境でパッケージマネージャーをしようとするとサードパーティの Chocolatey や Scoop を利用することになったと思います。

winget は microsoft が提供しているパッケージマネージャーで、徐々に使われるようになってきていると思います。

winget のバージョン確認は、以下のどちらかのコマンドをコマンドプロンプトで実行してください。もしも、インストールされていなかったら、Windows Store からインストールします。Windows Store アプリであれば、winget 本体のバージョン管理は自動的に更新されるようになるはずなので「楽」です。

winget
winget -info

コマンド:

コマンド 説明
install 指定されたパッケージをインストール
show パッケージに関する情報を表示します
source パッケージのソースの管理
search アプリの基本情報を見つけて表示
list インストール済みパッケージを表示する
upgrade 利用可能なアップグレードの表示と実行
uninstall 指定されたパッケージをアンインストール
hash インストーラー ファイルをハッシュするヘルパー
validate マニフェスト ファイルを検証
settings 設定を開くか、管理者設定を設定する
features 試験的な機能の状態を表示
export インストールされているパッケージのリストをエクスポート
import ファイル中のすべてのパッケージをインストール
pin パッケージ ピンの管理
configure システムを適切な状態に構成します
download 指定されたパッケージからインストーラをダウンロードする
repair 選択したパッケージを修復します

オプション:

オプション 説明
-v,--version ツールのバージョンを表示
--info ツールの一般情報を表示
-?,--help 選択したコマンドに関するヘルプを表示
--wait 終了する前に任意のキーを押すプロンプトをユーザーに表示
--logs,--open-logs 既定のログの場所を開く
--verbose,--verbose-logs WinGet の詳細ログを有効にする
--nowarn,--ignore-warnings 警告出力を非表示にする
--disable-interactivity 対話型プロンプトを無効にします
--proxy この実行に使用するプロキシを設定
--no-proxy この実行に対するプロキシの使用を無効にする

æ›´æ–°

winget upgrade --all
winget upgrade --all --silent

初回はサイレントにしなくてもいいかもと思います。サイレント実行をすると、管理者権限が必要になる場合もあってこのときは Powershell の管理者モードで実行するといい。

サイレントにしていない場合は、各ソフトウェアのポップアップが表示されるので、邪魔に思うかもしれないけど初回はこれくらいのほうが安全じゃないかな、という意見です。

これで python もまとめてアップデートできる。正直、アップデートをしない半端なバージョンを許さないほうがいいと思う考えで、バージョンを固定したいときは、下記の pin であらかじめ固定しておいて、他は --all ですべて更新がよいと思っています。

どうしても個別にやりたいときは python を例に挙げると、最初に list で ID を確認します。

winget list python

確認できた ID に対して upgrade を実行する。

winget upgrade --id Python.Python.3

念のため、最後にコマンドプロンプトでバージョンが変わっているかどうかもチェックしておこう。

python -V

バージョンの固定

アップデートしないパッケージは pin を使ってバージョンを固定します。

winget pin add <package>

こうしておくと upgrage --all といったアップデートの対象外になる。pin に追加した一覧を確認したいときは以下。

winget pin list
コマンド 説明
add 新しい PIN を追加する
remove パッケージ PIN の削除
list 現在のピンを一覧表示する
reset PIN をリセット

おまけ:インストールのバッチ化

winget で管理できるアプリは「winget.run」で探す。

簡単な .bat ファイルを作成することで、ソフトウェアのインストール状態をまとめることが可能なので便利だと思います。

echo off
winget install -e --id RARLab.WinRAR
winget install -e --id Google.Chrome
winget install -e --id Google.Drive
winget install -e --id OBSProject.OBSStudio
winget install -e --id Dropbox.Dropbox
winget install -e --id Greenshot.Greenshot
winget install -e --id Notion.Notion
winget install -e --id KeePassXCTeam.KeePassXC
echo on

参考

2024 年 会社勤めの買ってよかったモノ、まとめ

12月の風物詩。毎年やっているけど、買って損していないのかチェックみたいな気もする。

QOL

文具

ぬいぐるみは、ドラキー、ももんじゃ、スライムの3匹を購入して本棚を守ってもらっています。おばけきのこを追加したい。

水出しポットは、会社で飲むコーヒーを作り置きするスタイルに。薄い味のコーヒーが好きなので、人の倍は豆が長持ちしてしまう。

マグカップもコーヒー関係で同じ。今年はコーヒーの参考書も読んで、すこし勉強もしていました。

レバーレスコントローラー作成

友人にレバーレスコントローラーを作成してみたりもして、ゲームのモチベーション自体はあるつもり。

PC 関係、スマートフォン

スピーカーは主に風呂場で髪を切ったり、本を読んだりするときに利用しています。冬の長風呂はヒートショックのリスクがあるみたいですが、すきなんです。

雷ガードは合計8つは購入したのかな。とりあえず、コンセント大本に装着してまわって雷対策を実施。電源タップが必要なところは、埃ガードのものに更新。私室の電源タップは、5~6年くらい経過していたのでこっちも更新しました。

ノイキャンイヤホンは、前に使っていたものが触ったときのイベントが検知できなくなったので更新。前のやつは家専用に。

運動

ハンドグリップはついに「1」になった。毎年くらいのペースでひとつずつ重さを上げています。暇なときにギコギコしてる手遊び。

ホエイプロテインは、今年けっこう買ってた。ブラックフライデーのセールでも1万円分購入。

本

分野がぐちゃぐちゃ。今はメンタルヘルスの資格試験の参考書と MBTI に興味がわいたので、こっちも読んでいます。

マンションに関する情報はプライベートでいろいろあって、すこしずつ情報収集をはじめました。日本の不動産の仕組みは同意できないところが多くて、イヤな気持ちにさせられる。

漫画は、あと「ポンコツ魔王の田舎暮らし」が気になっているので購入しようかなぁって気持ちになっています。おもしろいのは報われてほしいし、ちゃんと続いてほしい。

総括

2024年は、久しぶりに資格試験を受験して「第一種衛生管理者」を取得しました。このあともメンタルヘルス関係の民間資格の受験を検討しています。そのほか、専門分野外の参考書を読む機会もあり、学習を例年よりもがんばることができたと思います。

プログラミング関係も、興味とやることに悩むところがあったけど、すこしずつ学習して成長を目指すことができた。

「プライベート」では、有馬温泉の旅行は結構よかったし、ちょこちょこと遊びにでかけた。格闘ゲームの流行に乗りたい気持ちがあったものの、学習のタイミングもあって時間を上手くつくれなかった一方で、後半はモンスターハンターの新作に備えて時々1~2Hの時間を作って友達と IB で操作練習を開始していたり、勉強・仕事・遊びのバランスが未熟ながらいくらか取れていたと思う。

「投資」に関しては、NISA の制度が新しくなったけど、速やかに1月から即対応できたのはよかったと思う。個別銘柄を長期で保有する予定のものは現状追加していないつもりだけど、任天堂やDNAといった銘柄をスポット的なタイミングで短期の売買したりまだ保有をしており、年間で数十回の売買をした。配当金以外のところでも配当金以上の黒字をだせたのは、経済的な動きを知るうえでもよかったと思う。ここは、去年書いたことを守ることができた。

ビジネス面は、忙しくなる予定だったものが半年以上ずれ込んだこともあって、忙しさの最中にまだいます。年始からも忙しい状態が継続するはずなので、体調やメンタルに気を付けて健康的にやっていきたい。(このあたりは、予感があったので衛生管理者やメンタルヘルスの学習で対策をすすめています)

来年は、「学習」だとメンタルヘルスの資格試験を受験したい。「投資」に関しては継続と参考書を1冊はしっかり読む。「仕事」は嫌なことも増えてるけど考えすぎずに、やれることをやっていく。あと、「脱毛」はとりあえず自宅用のやつからでもいいので取り組んでいく。

「遊び」は MONSTER HUNTER Wilds にまず注力やな。

履歴

第一種衛生管理者試験の受験(合格)

2024 年 10 月15 日 (火) に第一種衛生管理者試験を受けてきました。(試験範囲が狭い)第二種衛生管理者は受験していないので、上位資格といっていいだろう第一種だけ受験した形です。

結果は「合格」

よかったです。試験結果の得点、各問題の正解/不正解の記載は、ありませんでした。(別途費用で結果を請求できるみたいですが……)

受験に際しての勉強は、以下の通りだったとメモ。参考書を1冊読むのに(通勤時間などを利用して)ダラダラと1か月。試験の3週間前から、過去問と YouTube を使った勉強を(ようやく)はじめた感じでした。

豆知識として、コロナ禍の影響で受験者が増えていた試験みたいです。なので、人が増えたことで問題は少し以前より難化傾向にあったらしい?

第一種衛生管理者とは

労働安全衛生法において定められている、労働環境の衛生的改善と疾病の予防処置等を担当し、事業場の衛生全般の管理をする者、またはその国家資格。

50人以上の中小企業から1人以上の設置が必要になる資格なので、国家資格のわりに合格率は高く第一種衛生管理者はだいたい45%前後くらい。

一応、第一種衛生管理者は「受験資格」の確認が必要です。一番のおススメはコード番号「8」で、会社で10年以上労働衛生の実務に従事したことがあれば、会社の「事業証明書」だけで受験可能です。大学の卒業証明書とかも必要なくなるから雑費も下がるし簡単。受験申請は WEB システムからできるようになったので、受験まですることは「事業証明書」等をまとめて特定記録郵便で送ることだけだったと思います。(試験料+特定記録郵便費用)

私は、出張試験を WEB システムから予約して利用しました。受付期間(開始日)をリマインドしておいて、席が埋まる前に、ササっと予約するのがおススメです。

特定記録郵便は、郵便局でしかできないはずなので、ポストではなく郵便局に直接行かないといけないのはすこし手間だった。

受験した理由

私は、普段のブログの内容のとおりエンジニア(プログラマ)として仕事をしているので、衛生管理者としてどれだけ直接活動するかというと微妙なところです。

ただ、会社に所属する人なので。所属する会社の社内規則とは、どうしても付き合わないといけない。(もちろん労働基準法も)第一種衛生管理者は、会社の安全/衛生委員会などでも使えるし、会社の居心地のよさを自分で上げるために使える小ネタだと思いました。(内申点も調整できる)

また、基本的な労働基準法を学習できるし、その周辺知識や労働安全についても全般的にひろく学べる。労働者としての防御力があがる資格だと考えました。

勉強に使ったもの

4つ。申請から約2か月弱の短期間だったので、これだけでした。

試験の参考書は、なんでもいいと思っています。第一種衛生管理者は、色彩検定とかと同じで、毎回の試験内容は抜本的に変更されるようなものではありません。毎回そう違わない。なので、私が読んだ参考書は、近所の古本屋に置いてあったやつで、2022年用の参考書(100円)でした。

たしかに試験の内容は法律やガイドラインの変更によって、問題変更なされるのは事実だと思います。ただし、法律やガイドライン等の変更があった直後に新傾向の問題が出題される傾向は少ない or 無いみたいです。不文律としておよそ1~2年後に新傾向の問題として出題される雰囲気みたい。

補足として、私の(今回の試験の)参考書は、しっかりと隅々まで読み込むことはしていない。試験の過去問を解いたり、動画を視聴するときに、どの辺のページにあった内容だな、というのがわかればよし。あとで、過去問(公表問題)を解いたときに間違った問題や気になった問題は、参考書を開いて、ササっと再学習しやすいようにしておく感じ。

第一種衛生管理者は、過去問を解かないと話にならない、とよく言われています。なのでどのような方法でもいいから過去問は解いたほうがいいと思う。実際、安全衛生技術試験協会が公開している公表問題と本番の試験問題は、かなり似ていたと思います。(公表に無い問題も当然ながら出題されます)

自分はネット上に公開されていた公表問題(過去問)を過去3回分をしっかり解いた。参考書をザっと読んだだけだと、初回はかなり間違ったので50%以下だったと思う。間違った問題に限らず、すべての問題の選択肢をあらった。なぜ正解でなぜ不正解なのか選択肢をひとつひとつ完璧にした。結構時間はかかったけど、公表問題でわからないところは無いくらいはできたと思う。(暗記不足はあったかもしれないけど)

「第1種衛生管理者試験問題」は、この公表問題の学習をサポートしてくれている。過去の類似問題もチェックしやすいので重宝した。

YouTube は「 YouTube - 第1種衛生管理者合格チャンネル」に絞って視聴し、他はどこも見ていない。塾講師っぽい話し方で嫌味がなく丁寧でよかったと思います。ゆっくり話してくれるので1.5倍速くらいでも大丈夫。問題の文章もしっかりと言葉にして読んでくれるので、主に通勤の電車の時間やランニングやウォーキングをしながら聞いていました。思ったよりも効果的なのでおススメ。(有料プランは未加入でした。すいません)

学習期間

  • 参考書の読書:2024/08/19 ~ 2024/09/17
  • 勉強:2024/09/28 ~2024/10/14
  • 勉強の内訳:
    • 参考書(10%)
    • 公表問題(65%)
    • YouTube 動画(25%)

期間中、9月の上旬と下旬は、有馬温泉などの複数の旅行があって勉強していない。たのしかった。

第一種衛生管理者の試験は「関係法令」「関係法令(有害)」「労働衛生」「労働衛生(有害)」「労働生理」の5つの科目(区分)があり、それぞれで40%以上の点数であること、かつ、合計点が60%以上であるなら合格。仮に合計得点が合格点以上でも、どこかの科目が死んでると不合格になるのは油断ならない点だと思います。

問題は、10問 or 7問(有害)で合計44問だったはずです。なので、7問しかない(有害)は40%足切りに注意が必要だと思います。苦手問題や引っ掛け問題で死にやすいと思う。

また、このほか自分に関係のありそうなガイドラインは直接資料を開いて確認することもあった。たとえば「情報機器作業における労働衛生管理のためのガイドライン」など。この種のガイドラインの内容は、職場の同僚と確認したりした。

費用

  • 受験料 8,800 円(※会社の資格取得補助)
  • 参考書 100 円
  • 郵便(受験票取得:特定記録郵便244円+免許書取得:特定記録郵便490円=734円)
  • 免許書発行手続き、(返送用切手、収入印紙、写真)(約460+1500+1000=3960円)
  • 交通費 - 円

合計:約13,594 円

個人的には、受験料と免許発行手続き等は、会社の資格取得補助金を利用したので、自己負担は実質 1,000~2,000 円くらいだと思う。とはいえ、完全に自力で取得する場合でも 1.5 万円で高くはない。

受験の検討時点の話だと、受験票の取り寄せ、免許書発行手続きが「面倒そうだな」という予想は、正しい予想でした。実際けっこうめんどくさい。心を無にしてやる雑務の感じがある。どうしてもわからなかったら、安全衛生技術試験協会か郵便局に電話して聞こう。

参考

C# Prism ライブラリ 9.0 更新とライセンス変更

Prism は主に WPF などで利用できる、XAML アプリケーションを構築するためのフレームワークです。

Prism は Microsoft MVVM Toolkit と似た役割を担うことができるライブラリで、特徴として MVVM +α のような機能を提供しています。

これまでは Prism 8 が最新でしたが 2024 年に Prism 9 に更新していることに気が付きました。(9.0 Preview 自体は 2023 年に公開があったようです)

9 への更新は、これまでのアップデートとは違う点があります。期間も空いたし、色々と更新に際してトラブルもあったようです。ライセンスの変更など、扱いにはすこし注意が必要だと思ったので気になった点をメモ。

個人的な調べなので、内容に間違いがあったらすいません。(ご指摘は歓迎! お手柔らかにお願いします)

ライセンスについて

まず、従来の Prism は MIT ライセンスでしたが 9.0 になってデュアルライセンスになりました。

無料のコミュニティライセンスと、有償の商用ライセンスがある、という状態です。

ただし、HP には以下のように説明があるため、大きな収入、または、大きな資本提供が無い場合なら、無料の Community ライセンスを引き続き利用することができますが、「登録」が必要になっています。

*Companies and individuals with less than 1 million USD in annual gross revenue, or have never received more than 3 million USD in capital from an outside source, such as private equity or venture capital, are eligible for the Community License.

この内容を補足をすると Prism の(主となる)スポンサーは以下の2名になります。

dansiegel のブログは現在もギリギリ更新されており、Prism 9.0 についても言及がありました。おそらく、この内容が一番状況を掴むうえで参考になるのではないか、と(私は)考えています。

dansiegel はライセンス変更の理由として ".NET Foundation issue" と挙げており、また、GitHub Sponsors を利用して Prism の開発を持続できるようにしたかったが、おそらく収益の面でうまくいかなかったようです。

同じような話だと、過去に、Unity Container も v5.11 以降のリリースは(サポートがなければ)無いということが HP に掲載がありました。(Internet Archive などで確認できます)実際のところ HP は綺麗になっていましたが、今もリリースはなさそうです。

Prism は今回のアップデートで、ライブラリを利用するための動画による学習用コンテンツも展開していますが、こちらも有料になっているので、収益化に苦労している状況が伺えます。

個人的な考え

Prism ライブラリは NuGet から 43M のダウンロードがあったということで、とても成熟したよいライブラリであることは私も疑いありません。

ただ、今回のライセンス変更を見ると(Unity Container とおそらく似たような問題があり)経済的な理由から今後も継続可能な(or メンテナンスされ続ける)ライブラリであるかどうかの部分は以前より警戒が必要になっているのではないか、と私は思いました。

幸い、dansiegel は GitHub のタグを調べてみても v7 以前のころからメンテナンスされているので v9.0 から参入した人物ではありません。急にライブラリの方向性が変わったり、おかしくなるような恐れは低いと思います。

今回のライセンスまわりの動きは、Microsoft MVVM Toolkit とは雰囲気が違うかもしれません。

参考

ID (UUID, GUID) の利用についての学習メモ

特に SQL などで、利用するユニークな採番(各データに番号を割り当てるプロセス)で、いわゆる ID を利用することがあります。

一般的には(単純な DB では)primary key に整数型で auto increment の設定を選択すると、DB 側で自動的に採番してくれるので、アプリケーションはあまり気にすることがありません。

ただし、この方法だと挿入と更新の操作は冪等 (idempotent) ではなくなるケースが発生することがあります(すべてのケースで起こるわけではない)。

冪等(べきとう):ある操作を1回行っても複数回行っても結果が同じであることをいう概念

冪等の例

具体的な例をとして、DB に採番を任せた場合を挙げることにします。関連する2,3つのテーブルにも同時にデータを入れたいとします。

ある人物の情報を記録する DB があるとします。User テーブルに(主たる)データを挿入して、Address テーブルにもデータを挿入する場合、User テーブルに登録したときのデータにある primary key が Address テーブルの登録にも必要ではないでしょうか。(または、参照したい)

CREATE TABLE Users (
    UserID INT AUTO_INCREMENT PRIMARY KEY,
    UserName VARCHAR(100) NOT NULL
);

CREATE TABLE Addresses (
    AddressID INT AUTO_INCREMENT PRIMARY KEY,
    UserID INT NOT NULL,
    Address VARCHAR(255) NOT NULL,
    FOREIGN KEY (UserID) REFERENCES Users(UserID)
);

-- Step 1: Users テーブルに新しいユーザーを挿入
INSERT INTO Users (UserName) VALUES ('John Doe');

-- Step 2: 採番された UserID を取得
SET @NewUserID = LAST_INSERT_ID();

-- Step 3: Addresses テーブルに関連するアドレスを挿入
INSERT INTO Addresses (UserID, Address) VALUES (@NewUserID, '123 Main St');

Step 2 のように採番された結果を待って ID を受け取り、残りのテーブルに割り振られた ID を利用して挿入の指示をしなくてはいけません。そのため、冪等ではなくなるケースがあります。

このケースでは、1つ目のテーブルで ID を採番したデータを挿入し、2つ目のテーブル Addresses にデータを挿入したタイミングで、なんらかのエラーが発生したとします。

1つ目の採番をしたデータはすでに登録しているはずなので、データの矛盾が生じてしまいます。このエラーの対処方法としては、トランザクション処理とロールバックが必要になったり INSERT ON DUPLICATE KEY UPDATE の書き方をしたりしますが、DB によって書き方が違ったり、冪等な処理の構文に非対応だったり、アプリケーションで採番しないことで、かえって複雑になってしまうことがあります。

一人で作っていると矛盾は作りづらいかもしれないけど、DB とアプリケーション側で構築する人が違ったりすると、すり合わせがめんどくさいことになる(ことがある)

結局、アプリケーション側で primary key を生成しておいたほうが単純だったよね、っていうケースはあります。

ただし、ケースバイケースの話であって、UUID だとパフォーマンスが低下する恐れがあります。DB が UUID を文字列のように扱うことになると INTEGER 型と比較して計算コストが高くなることがあります。SQLite では INTEGER PRIMARY KEY だと内部的に「ROWID」になっています。(参考「ROWIDs and the INTEGER PRIMARY KEY」なので、UUID を利用すると計算コストは高くなる可能性がある)

UUID と GUID

UUID は universally unique identifier のこと。なので、用語の示す意味的にも ID と同じようにふるまえることがわかります。基本はユニークな識別子です。

GUID とは Globally Unique IDentifier のこと。Microsoft Learn で「NewGuid」の説明を読んでみます。

The method creates a Version 4 Universally Unique Identifier (UUID) as described in RFC 4122, Sec. 4.4.

なので名前から UUID と GUID はすこし違うモノのように見ますが、実際は(現在は)UUID を代替する名前として GUID が使われています。(DB などでも GUID という用語が利用されることがあります)

C# の GUID の生成

Guid uuid = Guid.NewGuid();
Console.WriteLine(uuid.ToString());
c33db100-0779-4192-91af-043516a343d1

NanoID

UUID で基本的にはいいんだけど ID について調べていると nanoid はわりと知っていてもよいと思いました。

   public void Run(string[] args)
    {
        var defaultID = Nanoid.Generate();
        var shortID = Nanoid.Generate(size:10);

        Console.WriteLine($"ID: {defaultID}, shortID: {shortID}");

        var id1 = Nanoid.Generate("23456789" + "CFGHJMPQRVWX", size: 8);
        var id2 = Nanoid.Generate("23456789" + "CFGHJMPQRVWX", size: 8);
        var id3 = Nanoid.Generate("23456789" + "CFGHJMPQRVWX", size: 8);
        var id4 = Nanoid.Generate("23456789" + "CFGHJMPQRVWX", size: 8);
        var id5 = Nanoid.Generate("23456789" + "CFGHJMPQRVWX", size: 8);

        Console.WriteLine($"ID: {id1}");
        Console.WriteLine($"ID: {id2}");
        Console.WriteLine($"ID: {id3}");
        Console.WriteLine($"ID: {id4}");
        Console.WriteLine($"ID: {id5}");
    }

nanoid のよいところは、文字列の長さを指定したり、ID に使用できる文字を指定したりできるところです。なので、実際のユニークな ID のほかに、表示用の簡易的なユニーク ID を用意するときなんかは便利だと思います。

表示用の ID を用意する理由としては、人の読みづらい文字を回避した ID を用意すること。まぎらわしい文字、たとえば I 1 l みたいな違いは人の目には微妙ってこと。

一例として、OPEN LOCATION CODE などがあります。

TODO

今年の .NET 9 で UUID 7 がサポート予定になっているようです。

これは、気になるので加筆したい。

参考

WPF メインウィンドウ、子ウィンドウのインスタンス取得

ボタンなどの GUI イベントを実行したときに、メインウィンドウのインスタンス(ハンドル)が欲しいことがあります。

本来、MVVM で設計をするとこういうことは無いように設計するのかもしれませんが、現実的に VM から子ウィンドウを(親ウィンドウの)ダイアログとして表示したい、のようなことくらいはサクッと実装したい。あまり原理原則を大切にしすぎても、View はどこまでいっても XAML なので深く付き合ってもどうなのかな、という気持ちはある。

WindowService の利用

というわけで WindowService を実装例。ウィンドウのインスタンスを渡すとインターフェース経由でウィンドウ(でやりたいこと)を操作できる。

ウィンドウのインスタンスそのものを渡してしまってもいいんだけど、さすがに MVVM の矜持に悖る気がするので、インターフェースでラップしておこう、という提案が根本。なので Window インスタンスを返却してしまうのは微妙とも感じる。子ウィンドウを返却するメソッドに置き換えてもいいと思う。

public interface IWindowService
{
    Window Window { get; }
    IntPtr WindowHandle { get; }

    void Close();
}
public class WindowService : IWindowService
{
    private readonly Window _window;

    public Window Window => _window;
    public IntPtr WindowHandle => new System.Windows.Interop.WindowInteropHelper(_window).Handle;

    /// <summary>
    /// <see cref="WindowService"/> クラスの新しいインスタンスを初期化します。
    /// </summary>
    /// <param name="window">ウィンドウのインスタンス。</param>
    public WindowService(Window window)
    {
        if (window == null)
        {
            throw new ArgumentNullException(nameof(window));
        }

        _window = window;

        // WindowHandle のアドレス値は、Window の非表示前だと 0x00 が返却される。
        // Window 表示後のボタン押下のタイミングなどで取得されることが望ましい
    }

    public void Close() => _window.Close();
}

使い方

DI と連携させると、こんな感じになると思う。

protected override void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);

    var serviceCollection = new ServiceCollection();
    var messenger = WeakReferenceMessenger.Default;

    var v = new MainWindow();

    ConfigureServices(serviceCollection, v);

    var serviceProvider = serviceCollection.BuildServiceProvider();
    var vm = new MainWindowViewModel(messenger, serviceProvider);

    v.DataContext = vm;
    v.Show();
}

    private void ConfigureServices(IServiceCollection services, MainWindow v)
{
    // DIコンテナにサービスを登録
    services.AddSingleton<IWindowService>(new WindowService(v));
}
...

public MainWindowViewModel(IMessenger messenger, IServiceProvider serviceProvider)
{
    _messenger = messenger;
    _serviceProvider = serviceProvider;
}

public void ShowChildWindow()
{
    var windowService = _serviceProvider.GetRequiredService<IWindowService>();

    var v = new SampleWindow
    {
        Owner = windowService.Window,
        WindowStartupLocation = System.Windows.WindowStartupLocation.CenterOwner,
    };

    var childWindowService = new WindowService(v);
    var vm = new SampleWindowViewModel(childWindowService);

    v.DataContext = vm;
    v.ShowDialog();
}

ポイント

ViewModel で View を弄るのはあまり推奨されたことではない。なので、アクセスできる部分を限定するためのインターフェース。

でも、使いたいシーンがあるときは:

  • DI でウィンドウのインスタンスを管理
  • 管理しやすく、アクセスしやすい
  • 子ウィンドウにも転用、応用しやすい

参考