久しぶりにC++でプログラムを書いて、その時に生ポインタの代わりにshared_ptr
を使うように変更したところとんでもなく遅くなってしまうことがあり、導入できなかった。
いきさつ
いじっていたのはsmallptというパストレーサーのコードで、元からOpenMPを使ってY座標ごとにprivate for
で並列化されていた。
smallptでは行数を短くするために配置できるのは球だけになっているのを、拡張していろいろな形状を扱えるようにオブジェクトのベースクラスを作成して継承させて扱おうとした:
class Object {...}; |
球だけじゃなくなるので交差判定の情報を保持するクラスを追加し、光の反射計算用にオブジェクトではなく交差点のマテリアル情報を保持するようにした:
class Material {...}; |
最初は上記のごとくオブジェクトやマテリアルの管理を生ポインタで扱っていて、delete
せずプログラム自体終了という後先考えない方法にしていたが、行儀がよろしくないだろうということでスマートポインタを使おうと変更してみた。
class Object { |
しかしとてつもなく遅くなり、初期の変更では実行時間が14.2倍も時間がかかるようになってしまった。
ミスりポイント:コンテナからの取り出しに誤ってauto
で受けてしまう
コンテナ要素をfor
で取り出す際に、生ポインタのときのままauto
で受け取っているととんでもないことになる:
void World::intersect(const Ray& r, HitInfo *hitinfo) { |
コンテナからの取り出しでshared_ptr
が複製されることになるため、無駄が発生する。
なのでauto&
に修正する必要がある。
shared_ptrの複製が重い
上のケースを修正してもまだ元の生ポインタの場合に比べて3.0倍実行時間がかかる。 交差判定での情報の更新でマテリアルのスマートポインタを複製することが避けられない。 結局のところ、スマートポインタの複製が重いらしい。
シングルスレッドでは微々たる差
「スマートポインタが重い」と思って絶望したのだが、確認のためOpenMPを無効にしてシングルスレッドの場合には、スマートポインタを使っても実行時間は大差なかった。 実のところOpenMPだかマルチスレッド自体との相性が悪いっぽい。
結果の実行時間の表
方法\環境 | Mac (MT) | Mac (ST) | WSL2 (MT) | WSL2 (ST) |
---|---|---|---|---|
生ポインタ | 81.76 | 363.04 | 33.107 | 392.698 |
shared_ptr | 242.73 | 372.88 | 113.540 | 391.123 |
- 数値は実行時間(秒)
- 環境:
- Mac: M1Mac 8コア
- WSL2: Intel Core i7-12700 12コア20スレッド
- MT=マルチスレッド、ST=シングルスレッド
思うこと
- 3倍実行時間がかかるとかいうのは許容できないので、
shared_ptr
は断念… - 性能が超絶劣化するとはいえ、マルチスレッドでも破綻なく動くところは丸
shared_ptr
は共通のオブジェクトを参照カウントで管理すると思うのだけど、そのアクセスを排他制御するのが重いってこと?- でも排他制御が必要なのはスマートポインタ複製時(カウンタ操作間)だけでポインタ利用時はかからないだろうが、実行時間が何倍もかかるようになるほど影響出るとは思えんのだが…
- とはいえシングルスレッドでは生ポインタと大差ないのも謎
- プロファイラで重い箇所を調べる:
- Macで調べたが、計測されるのはユーザ関数のみで、
shared_ptr
は引っかからずわからなかった
- Macで調べたが、計測されるのはユーザ関数のみで、
- これって他のGCじゃない言語(Rustとか)でも同じようにマルチスレッドの場合は問題になるんじゃないのかなぁ?(要検証)
あとがき
weak_ptr
にしても、より時間がかかるだけunique_ptr
の利用はほぼ性能劣化なし(とはいえ機能が違うので単純に切り替えることはできない)- 管理は
shared_ptr
で、交差判定は一時処理としてshared_ptr.get()
で生ポインタで扱うということもできなくはないが、苦しい - 気になって他のソースを見たところ、Ray Tracing in One Weekendの
hit_record
もマテリアルをshared_ptr
で保持していた