https://twitter.com/yukicoder/status/1703772579511836745 という話がTwitter、あいやX(旧Twitter)あった。
これはLinux環境でbrewで入れたDMDはdmd.confに-fPICがついていないのが原因だった、っぽい。
DMDはコンパイル時の初期化設定ファイルを読み込む動作をまず行って、ここで指定されている環境変数を読み込む。たとえば明示的にphobosへのパスを指定していなくてもちゃんとリンクしてくれるのはこのファイルから環境変数を読み込んでいるためだ。 その初期設定ファイルは、Linuxではdmd.confという名前で保持している。 ちなみにこれはコンパイラごとに固有で、LDCもldc2.confというファイルで同じようなフォーマットで保持している。
で、installerとかで入るDMDにはdmd.confのほうで勝手に付与されるために意識する必要がなかったという話だったようで、macOSなんかだとbrewで入れるケースが多いけど自動でPICが有効になるためにみんな気がつかなかったという話っぽい。
ここまではよいのだが、ふと-fPIEと-fPICでDMDが生成するバイナリはなにか変わるんだろうか?と気になった。
前提として-fPIEが有効になる環境はLinuxのみである。
ひとつはpredefined versionでD_PIE
とD_PIC
がそれぞれ定義されるという違いがある。
まあこれはいいだろう。
より重要な部分として、コード生成での違いがある。
まずbackconfig.dのコードを読むと、PICの場合はPICのフラグのみだがPIEの場合はPICとPICの両方のフラグが有効になる。
switch (pic) { case 0: // PIC.fixed break; case 1: // PIC.pic cfg.flags3 |= CFG3pic; break; case 2: // PIC.pie cfg.flags3 |= CFG3pic | CFG3pie; break;
PIEのときの条件分岐を追っていくと違いがみえてきそうだ。
実際にコードを読んでみると、内容的に重複していたり使われていない箇所などもあるが、PIEだとより効率的なコードが生成できるって点だけおさえとけばいいんじゃないかと思う。
具体的な話でいえばPIEを使った場合よりコストの低いリロケーションタイプを使うことができる。
一例としてはこんな感じで、リロケーションタイプの割り当てが異なっている。
case SC.locstat: if (I64) { if (s.Sfl == FLtlsdata) { if (config.flags3 & CFG3pie) relinfo = R_X86_64_TPOFF32; else relinfo = config.flags3 & CFG3pic ? R_X86_64_TLSGD : R_X86_64_TPOFF32; }
PIC(-fPIC)だとGlobal-DynamicなTLSモデルを使う必要があり、__tls_get_addr
経由でGOTに確保したオフセット情報を元に相対計算をして変数を探す。この際常にTLSデータ全体を舐めるので遅い。
PIE(-fPIE)を使った場合、実行ファイル内の静的TLSブロックはリンク時にオフセットの値が確定するので、相対計算はずっと速く完了する。
というわけで今までなんとなくdmd.confに書かれてた-fPICをそのまま使っていたが、速度を考える必要が出た場合は-fPIEを検討することもよいかもしれない。
また、余談になるが他のコンパイラ、たとえばLLVM系のLDCではrelocation-modelに関してもコンパイルオプションで指定することができる。もちろんPIC/PIEなどのオプションとの兼ね合いになるだろうが、DMDよりも柔軟に調整できるんだろうか。
2023/09/26 追記
よく考えると-fPIEでTLSモデルをlocal-exec相当に暗黙仮定してcodegenしてるのアグレッシブ過ぎないか…? あとdmd.confで-fPICを指定している弊害か、だれもfPIEを使っていないためにバグりちらかしているという問題もあり。。