Chiharu の日記

絵描き C/C++ プログラマーの日記です。

オーバーヘッドの少ないスレッドローカルストレージを考える

スレッドローカルストレージの利用に際して、POSIX も Win32API も、値の取得と設定にシステムコールなりのオーバーヘッドがかかりますが、グローバル変数のような気軽さで頻繁にアクセスしたい場合、このオーバーヘッドが許容できないことがあります。というか、許容できないケースに遭遇したので代替え案を考えてみました。
話を単純化するために環境を Win32 アプリに絞ります。この環境では、スタック領域の初期サイズは 1MB で、1MB アライメントされたアドレスから若いアドレスへ自動変数を積んでいきます。ということは、スタックの積み方をスレッド間で共通化すれば、自動変数をスレッドローカルストレージ的に使えそうです。
やってみました。

#ifndef TLS_HPP_INCLUDED
#define TLS_HPP_INCLUDED

#include <cstdlib>

// スレッドローカル変数アドレスを初期化する。
__noinline void tls_init(std::ptrdiff_t* adr);

// スレッドローカル変数をスタックに確実に積む。
__noinline void tls_prep(std::ptrdiff_t* adr);

// スレッドローカル変数アドレスを取得する。
std::ptrdiff_t* tls_get(void);

#endif
#include "tls.hpp"

namespace {
std::ptrdiff_t* l_adr;
};

// スレッドローカル変数アドレスを初期化する。
__noinline void tls_init(std::ptrdiff_t* adr)
{
 l_adr = adr;

 if (adr != tls_get()) {
  std::abort();
 }
}

// スレッドローカル変数をスタックに確実に積む。
__noinline void tls_prep(std::ptrdiff_t* adr)
{
 if (adr != tls_get()) {
  std::abort();
 }
}

// スレッドローカル変数アドレスを取得する。
std::ptrdiff_t* tls_get(void)
{
 char cur;

 // スタックアドレスが 1 MB でアライメントされる前提で算出する
 const auto bas =
  reinterpret_cast<std::ptrdiff_t>(&cur) & (0xFFFFFFFFu << 20);
 const auto off =
  reinterpret_cast<std::ptrdiff_t>(l_adr) & ~(0xFFFFFFFFu << 20);

 return reinterpret_cast<std::ptrdiff_t*>(bas | off);
}

下記で検証しました。

#include <thread>
#include <cstdio>
#include "tls.hpp"

__noinline void init_proc()
{
 std::printf("get : %p\n", tls_get());
}

__noinline void user_proc()
{
 std::printf("get : %p\n", tls_get());
}

int main()
{
 // 初期化する
 std::thread th0([] { std::ptrdiff_t local;
  tls_init(&local); init_proc(); });
 th0.join();

 // スレッド別で検証する
 std::thread th1([] { std::ptrdiff_t local;
  tls_prep(&local); user_proc(); });
 std::thread th2([] { std::ptrdiff_t local;
  tls_prep(&local); user_proc(); });
 std::thread th3([] { std::ptrdiff_t local;
  tls_prep(&local); user_proc(); });
 std::thread th4([] { std::ptrdiff_t local;
  tls_prep(&local); user_proc(); });
 std::thread th5([] { std::ptrdiff_t local;
  tls_prep(&local); user_proc(); });
 std::thread th6([] { std::ptrdiff_t local;
  tls_prep(&local); user_proc(); });
 std::thread th7([] { std::ptrdiff_t local;
  tls_prep(&local); user_proc(); });
 std::thread th8([] { std::ptrdiff_t local;
  tls_prep(&local); user_proc(); });
 std::thread th9([] { std::ptrdiff_t local;
  tls_prep(&local); user_proc(); });

 th1.join();
 th2.join();
 th3.join();
 th4.join();
 th5.join();
 th6.join();
 th7.join();
 th8.join();
 th9.join();

 return 0;
}

結果は下記のとおりです。std::abort() が呼ばれることなく実行できました。

get : 0xffffcd18
get : 0xffefcd18
get : 0xffdfcd18
get : 0xffcfcd18
get : 0xffbfcd18
get : 0xffafcd18
get : 0xff9fcd18
get : 0xff8fcd18
get : 0xff7fcd18
get : 0xff6fcd18

使えそうですね。グローバル変数アクセスとアドレス計算だけでスレッドローカルな変数を利用できるようになりました。前提条件とハードコーディングが多いので、あくまでアイデアまで。スレッド間でスタックの積み方が自明なシステムなら、この方法の発展で、オーバーヘッドの少ないスレッドローカルストレージを実現できそうです :-D