ドナドナされるプログラマのメモ

Windows用アプリのプログラミングメモ

mozjpegをつかった画像変換ソフトの開発 その6

マルチスレッド関連の実装方法に迷いが生じたので、ちょっと考えを整理する。

■やりたいことは何か

やりたいことはスレッドからスレッドへの通知?いや違う。それは手段であって、目的ではない。やりたいのは、

  1. 処理Aが終了したあと、その結果を使う処理Bを起動する
  2. 処理Aが終了したあと、次の同様の処理A'を起動する
  3. 処理A'、処理Bはリソースが空くまで動作しない

である。つまり条件付きで処理を開始したい、ということになる。

これを実装するシンプルな方法は、こんな感じだろうか。

ThreadA(){ WaitForResource(); Do(); BeginThread(ThreadA); BeginThread(ThreadB);}

ThreadB(){ WaitForResource(); Do();}

この方式の問題点は、ThreadBの速度がThreadAよりも遅い場合である。リソースの空きを待つThreadBがどんどん増えてしまう。最悪、WindowsのHANDLE上限を超えるかもしれない。となると、ThreadBの実行状況に応じてThreadAの実行を制御する必要がある。ThreadAの実行制御はWaitForResource()が担っているので、これがThreadBの状況も監視するようになっていれば良さそうだ。

では、WaitForResource()はどのようなものなのだろうか?要件は以下だろう。

  1. 実行に必要なリソースの空きができるまで実行を停止する
  2. 後続処理の処理状況に空きができるまで実行を停止する
  3. 異なる複数のスレッドからの呼び出しに対応する

これはCSemaphore(セマフォ)で実現できそうだなあ。

■書き込みと読み込みの同時実行防止

書き込みと読み込みを同時に実行すると、遅くなる気がする。なので排他処理にしたい。そして、できれば書き込みを優先したい。これをセマフォでやるには、どうすればいいのだろう?まずは、優先を考えずに擬似コードを書いてみる。

CSemaphore hdd(initial=0,max=1);

CSemaphore CPU(initial=0,max=4);

ReadThread(){ Lock(hdd); ReadFile(); UnLock(hdd); BeginThread(CPUThread); BeginThread(ReadThread);}

CPUThread(){ Lock(CPU); Calc(); UnLock(CPU); Lock(hdd); WriteFile(); UnLock(hdd);}

このコードはデッドロックは起きなさそうだがWriteFile待ちのCPUThreadが大量にできそうだ。あまりよくない。

ならば、ReadThreadの開始に制限を追加したこれならどうか?

ReadThread(){ Lock(CPU); Lock(hdd); ReadFile(); UnLock(hdd); BeginThread(ReadThread); Calc(); Lock(hdd); WriteFile();UnLock(hdd); UnLock(CPU);}

 なお、CPUの数だけReadThreadができうるので、CPUThreadはReadThreadと統合してみた。すっきりはしたが、CPUの空きができるまでファイルを読み込まないというのは悲しい。となると、Lock(CPU)の位置を調整すればいいのか?ファイルを読み込んでからCPU実行待ち状態にしよう。次のファイル読み込みスレッドも、CPUリソースが空くまで待つか。

ReadThread(){ Lock(hdd); ReadFile(); UnLock(hdd); Lock(CPU); BeginThread(ReadThread); Calc(); Lock(hdd); WriteFile();UnLock(hdd); UnLock(CPU);}

 だいぶ良くなった。だがしかし、複数のスレッドがたまたま同時に終了した場合、こんどはReadFileが間に合わなくなりCPUが暇してしまう。できればCPUコア数分ぐらいは先読みしておきたい。ということは、読み込みスレッドのBeginThread実行条件とCPU計算実行条件を分ける必要があるのか。こんな感じ?

CSemaphore ReadBuff(initial=0, max=4+4);

ReadThread(){ Lock(hdd); ReadFile(); UnLock(hdd); Lock(ReadBuff); BeginThread(ReadThread); Lock(CPU); Calc(); Lock(hdd); WriteFile();UnLock(hdd); UnLock(CPU); UnLock(ReadBuff); }

 UnLock(ReadBuff)の位置はもっと前でもデッドロックせずに動作するが、Lock(hdd);より後ろとすることでWriteFileがReadFileよりも先に実行されるはず。うん、これでOKかな。あとは実装だ。

SendMessage / PostMessageにかかる時間

SendMessageやPostMessageにてメッセージを投げてから受け取るまでにかかる時間を調べてみた。Ryzen3700X @ Windows10 Prof. 1909では、SendMessage: 20us, PostMessage: 34usだった。SendMessageのほうが早いのは、WindowProcedureを速やかに呼ぶかららしい(公式ドキュメントのRemarksより)。

SendMessage / PostMessage speed test

f:id:donadonasan:20200208115818p:plain

SendMessageの所要時間測定結果

 

f:id:donadonasan:20200208115924p:plain

PostMessageの所要時間測定結果

 

mozjpegをつかった画像変換ソフトの開発 その5

どういうふうにスレッドの制御をするか考え中。今回のソフトは、タスクごとにスレッドを作ってしまうという富豪方針である。ということは、リソースに見合った数のスレッドが常に走っているようにスレッドを生成し続ければよい。これを実現するには、ワーカースレッドが完了した際には制御クラスに通知する必要がある。

通知は、いくつかの方法がある。一つはSendMessage / PostMessageでメッセージを投げること。あとはシグナルを駆使する方法。一番ラクなのはメッセージを投げつける方法だけど、処理時間が心配。

mozjpegをつかった画像変換ソフトの開発 その4

どのようにマルチスレッド化するかを考え中。

考えられる方針は大きく2つ。

  1. ファイルを1個開くたびに1スレッド作り、1変換をするたびに1スレッドを作り、1個書き出すたびに1スレッド作る。ブルジョワ方針。
  2. 物理メディアごとに1スレッド作り、当該メディアで読み書きするファイルはすべてそのスレッドで処理。CPUコアごとに1スレッド作り、当該コアで処理するデータは(略)。けちんぼ方針。

2.のほうがかっこいい実装だし、スレッドの生成はリソースを食うので極力しないほうがよい。一方、この実装は面倒でもある。そこで、2.の実装が努力に値するのか軽く検討してみた。

今回のソフトは、とにかく大量の画像を変換することを目標にしている。そのため、処理の速さは大事な指標だ。そこで、2.の実装によりどのくらい処理が早くなるかを考えてみた。もしとても早くなるなら、努力に値するはずだ。

1.の方法で生成するスレッド数は、画像ファイル数の3倍。なので1万ファイルを処理するならば3万スレッドほど生成することになる。一方、スレッドの生成にかかる時間はかんたんなテストの結果368.8us程度 @ Ryzen7 3700Xだった。ということは、1.の方式では368.8×10^-6 × 3×10^4 = 1106.4×10^-2 ≒ 11秒ほど遅くなるのか。

f:id:donadonasan:20200204001514p:plain

スレッド生成にかかる時間の計測結果 @ 10000サンプルの平均

1ファイルあたりにかかる合計処理時間が1秒だとすると、処理時間が0.1%ほど伸びる計算だ。これは誤差だなあ。2.は手間がかかる割に早くないようだ。というわけで1の方式にする。

LibPNGの速度比較(32bit, 64bit)

libPNGについて、win32とx64で速度が違うのか調べてみた。

■使用環境

OS: Windows10 64bit バージョン1909

CPU:Ryzen 7 3700X 3.6GHz

メモリ: PC10700 (1088MHz)

■計測方法

libPNGのRUN_TESTS所要時間を3回計測する。ビルドはいずれもRelease。

■計測結果

win32:72.17, 72.16, 71.84 (平均:72.05秒)

x64: 64.1, 63.86, 63.48 (平均:63.81秒)

■結論

なんか知らないけどx64のほうが1割以上早いようだ。

mozjpegをつかった画像変換ソフトの開発 その3

画像ファイルのロード、変換、保存をする部分の構造はどうしようかなあ。最近のPCはCPUのコア数が多いので、できればマルチスレッド対応としたい。その場合、どういうプログラム構成とすればいいのだろう?

データ処理の流れはシンプルだ。

  1. 二次記憶からデータを読み出す。
  2. CPUにて処理する。
  3. 二次記憶にデータを書き出す。

1. の二次記憶からデータを読み出す処理は、下手に複数スレッドにて同時処理すると特にHDDで遅くなる可能性がある。また、3.の処理との同時実行も却って遅くする恐れがある。しかし一方で、異なる物理ドライブに対する同時アクセスならば問題ない。

ということは、読み込み・書き出しを管理するクラス(CIOManager)を用意して、同一の物理ドライブに対する同時アクセスを制限しつつ並列化すればいいのかな。んで、読み込めたファイルからどんどん2.のCPU処理(CCodec)に投げて、処理し終わったデータはCIOManagerに投げて書き込ませる、と。つまり、以下の図みたいな感じかな。

f:id:donadonasan:20200203000216p:plain

ソフトウェア構造

けっこうコーディング量がありそうだな、これ。

ツールチップに複数行の文字を表示させる

CToolTipCtrlのAddTool()を使ってツールチップを表示させようとしたときに、文字列が長い場合は指定した位置で改行したくなる・・よね?でも、テキストに\nを加えても改行してくれず、ちょっと苦労したのでメモ。

CToolTipCtrlを使って複数行にわたるテキストを表示したい場合は、ツールチップに対して幅の上限を指定する必要がある。改行指示をしたときだけ改行してほしいならば、とても大きな幅・・例えば0x7fff ffffあたりを設定すれば大丈夫じゃないかなあ。

#下手に32768とかを設定すると、10年後とかに痛い目を見そうな気がする

幅の上限指定はメッセージによる。以下、例。

m_TipCtrl.SendMessage(TTM_SETMAXTIPWIDTH, 0, 0x7fffffff);

ちなみに、幅を-1にすると無制限と見做され、改行してくれなくなる。そして、多分これがデフォルトになっている。

以下、マイクロソフトの参考記事。

docs.microsoft.com