http://dev.gentoo.org/~dberkholz/articles/toolchain/tls.pdf
を読みました。 TLS の実装はざっくり言うと常に TLS 管理領域を指してるレジスタを一個作ってて、それをスレッドごとに別の値にするだけ。 86 ではセグメントレジスタが使われてる…くらいのざっとした理解はまぁ一般的にあると思うんですが、細かい実装となるとなかなか難しい。間違ってるとこあるかもですがなんか書いてみます。
TLS はまず、本当に全スレッドが使うの? って話があるので、メモリ効率を考えると遅延ロードをした方がいい。ただ遅延ロードするとなるとどうしても関数呼び出しとかがからんで、最初に確保しておいた時のコードよりどうやっても遅くなる。 PLT みたいに関数の呼び出しがどっちにせよ起きるようなケースとは違って、 CPU とメモリのトレードオフがある。
基本的には、プログラム本体なら CPU のことを考えて全 TLS を最初に確保しておいて、共有オブジェクトに入ってる TLS は遅延ロードにする、って戦略にしているぽい。まぁなにやら妥当そうではある。
生成されるコードを見てみる
ただ、コンパイル時にはプログラム本体に使われるオブジェクトなのか、共有オブジェクトに使われるのかよくわからないケースが多いので、これで話がややこしくなる。具体的なコードを見ていく。
__thread int tls; int tls_func() { return tls; }
こんなコード。
普通にコンパイルしてみると
00000000 <tls_func>: 0: 65 a1 00 00 00 00 mov %gs:0x0,%eax 2: R_386_TLS_LE tls 6: c3 ret
で -fPIC をつけてみると
00000000 <tls_func>: 0: 53 push %ebx 1: e8 fc ff ff ff call 2 <tls_func+0x2> 2: R_386_PC32 __x86.get_pc_thunk.bx 6: 81 c3 02 00 00 00 add $0x2,%ebx 8: R_386_GOTPC _GLOBAL_OFFSET_TABLE_ c: 8d 04 1d 00 00 00 00 lea 0x0(,%ebx,1),%eax f: R_386_TLS_GD tls 13: e8 fc ff ff ff call 14 <tls_func+0x14> 14: R_386_PLT32 ___tls_get_addr 18: 8b 00 mov (%eax),%eax 1a: 5b pop %ebx 1b: c3 ret
となる。最初の3命令は関数の入口と、 -fPIC 使ったから GOT が必要という話なので、 TLS は関係ない。次の2命令に call ___tls_get_addr て関数呼び出しがあるのが、遅延ロード用に必要になっちゃった感じ。
これを main のある関数とリンクすると、
0804847c <tls_func>: 804847c: 53 push %ebx 804847d: e8 2e ff ff ff call 80483b0 <__x86.get_pc_thunk.bx> 8048482: 81 c3 7e 1b 00 00 add $0x1b7e,%ebx 8048488: 65 a1 00 00 00 00 mov %gs:0x0,%eax 804848e: 81 e8 04 00 00 00 sub $0x4,%eax 8048494: 8b 00 mov (%eax),%eax 8048496: 5b pop %ebx 8048497: c3 ret
こうなる。リンカが命令書き換えるってのはなかなかすごい。書き変わった命令2つ目の sub $0x4, %eax てやつは、 3 byte で表現できるはずだけど、まぁどうせ空いた隙間を埋める必要があるので命令数少ない方がいいからこうしてるんだろう。
あと、こういう方向性とは別に、一つの実行ファイル/共有オブジェクトでしか使われない TLS に対してできる最適化もある。
前者の最適化をするかしないか、後者をするかしないか、で、
- General Dynamic: しない しない
- Local Dynamic: しない する
- Initial Exec: する しない
- Local Exec: する する
って感じでざっくり4つに分類できる。コード見る感じでは、速度的には Local Exec > Initial Exec >> Local Dynamic >= General Dynamic という感じではないかと。
errno
たぶんもっとも身近な TLS は errno だと思う。 errno はマクロで (*__errno_location()) に展開される。この関数は、
int *__errno_location (void) { return &errno; }
とごく普通の定義。ただ宣言で __attribute__((const) ) がついているので、一つの関数で2回以上 errno を参照した場合に、2回目から TLS とか関係なく普通のメモリアクセスができるようになっている。
errno ってよく使うから、遅延ロードとかしない方が良さそうだけど、 shared object に入ってるからほっとくと効率よくないのになりそうだけど、どうやってるのかなと思って見たのが errno を見た理由だったんですが、 __attribute__((tls_model("initial-exec"))) というのをつけることによって Initial Exec モデルを強制しているようでした。
この attribute をつけると生成されるコードがわかりやすいです。
local-exec
00000000 <tls_use>: 0: 65 a1 00 00 00 00 mov %gs:0x0,%eax 2: R_386_TLS_LE tls 6: c3 ret
initial-exec
00000000 <tls_use>: 0: e8 fc ff ff ff call 1 <tls_use+0x1> 1: R_386_PC32 __x86.get_pc_thunk.cx 5: 81 c1 02 00 00 00 add $0x2,%ecx 7: R_386_GOTPC _GLOBAL_OFFSET_TABLE_ b: 8b 81 00 00 00 00 mov 0x0(%ecx),%eax d: R_386_TLS_GOTIE tls 11: 65 8b 00 mov %gs:(%eax),%eax 14: c3 ret
local-dynamic / global-dynamic
00000000 <tls_use>: 0: 53 push %ebx 1: e8 fc ff ff ff call 2 <tls_use+0x2> 2: R_386_PC32 __x86.get_pc_thunk.bx 6: 81 c3 02 00 00 00 add $0x2,%ebx 8: R_386_GOTPC _GLOBAL_OFFSET_TABLE_ c: 8d 04 1d 00 00 00 00 lea 0x0(,%ebx,1),%eax f: R_386_TLS_GD tls 13: e8 fc ff ff ff call 14 <tls_use+0x14> 14: R_386_PLT32 ___tls_get_addr 18: 8b 00 mov (%eax),%eax 1a: 5b pop %ebx 1b: c3 ret
local-dynamic は同じ関数に2回出てきたりしないとメリットが無いので、今回のコードでは違いがわからない。
まとめがあるとすると
- 実行ファイルにリンクされる場合は速くてスレッドの数だけメモリを使う実装になると想定して、それが気にいらない場合は __attribute__ 使う
- shared object の場合で速度が気になる場合も __attribute__ を使う
- 可能なら static か __attribute__((visibility("hidden"))) つけとくと最適化されやすそう