設計を逆転させたら、ウェーハマップ描画が3秒から1.6ミリ秒になった話
はじめに
.NETでビットマップ描画をループで組んでいるなら、この記事は参考になるかもしれません。
数年前、.NET Framework でウェーハマップ画像生成ツールを作ったとき、10万ダイ・1024×1024 ピクセルの画像を生成するのに約 3秒 かかっていました。UIに組み込むと明らかにもたつく数字です。
今回 .NET 10 で同等の処理を書き直す機会があり、設計をシンプルに絞ったら 2000×2000 ピクセル・10万ダイで 1.6ミリ秒 まで短縮できました。この記事では、その考え方と実装を紹介します。

上記はウェーハマップ画像の例(200 x 200 ピクセル)
旧実装が遅かった理由
以前の実装は「ダイの位置を計算する」「ダイ値から色を求めて塗る」「枠線を重ねる」「(必要に応じて)ダイの中に数値を描く」という処理をダイごとにループする設計でした。元々ある実装に手を加える方式だったため、大きな変更ができない状況でした。
ダイ数が少ないうちは全く問題ありませんでしたが、ダイの数と作成する画像サイズに比例して処理時間が増加する構造になっていました。
設計の方針
1. ピクセルからダイを求める
今回の書き直しでは、発想を逆にしました。
「ダイを1つずつ描く」のではなく「各ピクセルがどのダイに属するかを計算して1回だけ書く」
出力画像の全ピクセルをスキャンし、それぞれのピクセル座標をウェーハ座標に変換して、対応するダイの色を1度だけ書き込みます。
重ね描きが構造的に発生しないため、ダイ数や画像サイズが増えても書き込み回数は「出力ピクセル数」で固定されます。
2. 事前にダイの値から色に変換する
ウェーハマップ描画に先立って、ダイの値から色を決めておきます。描画ループの中で都度カラーマップを参照する処理がなくなるため、ループ内の処理を最小限に保てます。
3. 画像の1行ごとに並列処理する
最近のCPUはコア数(スレッド数)が多いため、並列化の効果が大きいです。
1行ごとに並列化したのは、CPUのキャッシュ効率を意識しつつ、スレッド間で完全に書き込み部分を分けるためです。行単位であれば、各スレッドの書き込み範囲が重複しないためロックが不要になります。
ダミーデータの準備
描画ロジックだけでも速度の確認はできますが、出来上がったウェーハマップ画像がそれっぽい方が検証として納得感があります。
以下の仕様でダミーデータを作成しました。
- 値の範囲:-100.0 〜 +100.0
-
double[]に格納 - 分布:中心ほど値が小さく、外周に向かうほど大きくなるグラデーション
- ノイズ:ランダムなばらつきを乗せる
実装の核心部分:unsafe ポインタ + Parallel.For
描画の核心部分は次のメソッドです。LockBits で取得した Bitmap のポインタと、事前に用意した色テーブルのポインタを受け取り、並列で書き込みます。
pColorList は、ダミーデータの値を元に事前に色(RGBAの4バイト)へ変換したものです。
private unsafe static void RenderWaferMapParallelForDieColorList(
uint* pColorList, int waferW, int waferH,
uint* pDest, int imgW, int imgH)
{
// 画像座標からウェーハ座標への変換係数
float scaleX = (float)waferW / imgW;
float scaleY = (float)waferH / imgH;
Parallel.For(0, imgH, y =>
{
// 書き込み先ピクセル行の先頭ポインタ
uint* rowPtr = pDest + (y * imgW);
// このY行に対応するウェーハY座標
int waferY = (int)(y * scaleY);
// 色テーブル上の対応行の先頭ポインタ
uint* pDataRow = pColorList + (waferY * waferW);
for (int x = 0; x < imgW; x++)
{
// 画像X座標 → ウェーハX座標
int waferX = (int)(x * scaleX);
// 色テーブルから色を取得して1回だけ書き込む
rowPtr[x] = pDataRow[waferX];
}
});
}
ポイント①:unsafe ポインタによる直接書き込み
Bitmap クラスの SetPixel は呼び出しごとにオーバーヘッドがあり、大量のピクセルを扱う用途では使い物になりません。LockBits でメモリを固定し、uint* ポインタで直接書き込むことでそのオーバーヘッドを排除しています。
ポイント②:行単位の Parallel.For
Parallel.For の単位を「行(Y座標)」にしています。各タスクは rowPtr と pDataRow の2本のポインタさえ確定すれば独立して動けるため、スレッド間の競合が構造的に発生しません。ロックが不要なのでオーバーヘッドもゼロです。
計測結果
環境は .NET 10、Windows 11、AMD Ryzen AI Max+ 395 の開発機です。
| 条件 | 時間 |
|---|---|
| .NET Framework 旧実装(約10万ダイ、1024×1024) | 約 3,000 ms |
| .NET 10 新実装(約10万ダイ、2000×2000) | 約 1.6 ms |
画像サイズが旧実装より大きいにもかかわらず、約 1,875倍 の速度差になりました。
これは単純なCPUの性能差以上の差があります。

実際に作成したウェーハマップ画像(2000 x 2000 ピクセル、ダイ数:101,250)
計測時のログ
ダイのランダム値作成完了
有効ダイ数: 101250
色テーブル作成完了
計測回数を指定してください(デフォルト 10):
Loop : 1 / 11 Time(ms) : 7.6511
Loop : 2 / 11 Time(ms) : 1.6604
Loop : 3 / 11 Time(ms) : 1.6233
Loop : 4 / 11 Time(ms) : 1.8316
Loop : 5 / 11 Time(ms) : 1.5607
Loop : 6 / 11 Time(ms) : 1.383
Loop : 7 / 11 Time(ms) : 1.6461
Loop : 8 / 11 Time(ms) : 1.5514
Loop : 9 / 11 Time(ms) : 1.3836
Loop : 10 / 11 Time(ms) : 1.5099
Loop : 11 / 11 Time(ms) : 1.5474
(初回はウォームアップとして除外します)
Avg. = 1.5697 ms
まとめ
速度改善のポイントは4点でした。
- 1ピクセル1書き込み ― 出力ピクセルから逆算し、重ね描きをゼロにする
-
unsafe ポインタ ―
SetPixelのオーバーヘッドを排除してメモリを直接操作する - Parallel.For(行単位) ― 競合なしで並列化し、ロックコストをゼロにする
- 事前に色変換 ― 描画ループの外で重い処理を済ませておく
結局のところ、「描画ループの中に余計なものを持ち込まない」という設計判断が、この数字を出した本質やと思います。機能追加のたびにループ内が太っていく構造は、後から手を入れるほど厄介になります。最初から「ループの外で済ませられる処理は外に出す」という意識を持って設計しておくと、後々の改修コストも含めて楽になります。
同様のビットマップ高速描画の課題を抱えていたり、ウェーハマップ関連のシステム開発でお困りのことがあれば、お気軽にご相談ください。
Discussion