Thread local storage

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"))) つけとくと最適化されやすそう
なにかあれば下記メールアドレスへ。
shinichiro.hamaji _at_ gmail.com
shinichiro.h