【C++】shared_ptrとOpenMPの相性が最悪な件

2025-06-20

久しぶりにC++でプログラムを書いて、その時に生ポインタの代わりにshared_ptrを使うように変更したところとんでもなく遅くなってしまうことがあり、導入できなかった。

いきさつ

いじっていたのはsmallptというパストレーサーのコードで、元からOpenMPを使ってY座標ごとにprivate forで並列化されていた。 smallptでは行数を短くするために配置できるのは球だけになっているのを、拡張していろいろな形状を扱えるようにオブジェクトのベースクラスを作成して継承させて扱おうとした:

class Object {...};

class Sphere : public Object {...};

class Box : public Object {...};

class World {
vector<Object*> objects;
...
};

球だけじゃなくなるので交差判定の情報を保持するクラスを追加し、光の反射計算用にオブジェクトではなく交差点のマテリアル情報を保持するようにした:

class Material {...};

class Object {
protected:
Material* mtrl;
...
};

class HitInfo {
public:
Material* mtrl;
...
};

最初は上記のごとくオブジェクトやマテリアルの管理を生ポインタで扱っていて、deleteせずプログラム自体終了という後先考えない方法にしていたが、行儀がよろしくないだろうということでスマートポインタを使おうと変更してみた。

class Object {
public:
shared_ptr<Material> mtrl; // ← 変更
...
};

class World {
vector<shared_ptr<Object>> objects; // ← 変更
...
};

class HitInfo {
public:
shared_ptr<Material> mtrl; // ← 変更
...
};

しかしとてつもなく遅くなり、初期の変更では実行時間が14.2倍も時間がかかるようになってしまった。

ミスりポイント:コンテナからの取り出しに誤ってautoで受けてしまう

コンテナ要素をforで取り出す際に、生ポインタのときのままautoで受け取っているととんでもないことになる:

void World::intersect(const Ray& r, HitInfo *hitinfo) {
for (auto obj : objects) { // ←
...

コンテナからの取り出しで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は引っかからずわからなかった
  • これって他のGCじゃない言語(Rustとか)でも同じようにマルチスレッドの場合は問題になるんじゃないのかなぁ?(要検証)

あとがき

  • weak_ptrにしても、より時間がかかるだけ
  • unique_ptrの利用はほぼ性能劣化なし(とはいえ機能が違うので単純に切り替えることはできない)
  • 管理はshared_ptrで、交差判定は一時処理としてshared_ptr.get()で生ポインタで扱うということもできなくはないが、苦しい
  • 気になって他のソースを見たところ、Ray Tracing in One Weekendhit_recordもマテリアルをshared_ptrで保持していた