Yoh2の日記: ロード/ストア命令の追い越し例
[2012-01-06追記: ここから]
ここで書いたコードの、ロック用の変数をふたつ用意するという例、どこかで見た記憶があったと思ったらBINARY HACKS #94 にあった。
というわけで、参考文献としてO'REILLY BINARY HACKSを挙げておきます。
[2012-01-06追記: ここまで]
C11にメモリフェンス関連の記述が加わったことを機に、ロード/ストア命令の追い越しが発生することで期待通りに動かないプログラム例を考えてみた。が、これでいいんだろうか?
メモリオーダリングは関係ないレース条件があったりしないかな?
一応自環境では、メモリフェンスなし→おかしな挙動、メモリフェンスあり→正常な挙動(に見える) となったけど。
また、gccに-Sオプションを追加して出力されたアセンブリソースを見て、コンパイラレベルでは順序の入れ替えが起きてないことを確認したつもり。
■ 自環境:
OS: Linux (kernel 2.6.37-gentoo-r4 (2.6.37.4相当))
CPU: Opteron 2427
コンパイラ: gcc-4.5.3
オプション: -Wall -O3 -march=native -mtune=native -lpthread
[2012-01-06 02:38追記] 最適化オプションなしの方がおかしな挙動が派手に出るっぽい。
■ 使い方:
引数なしでプログラムを実行 -- メモリフェンス命令を実行しない。
引数あり(何でもいい)でプログラムを実行 -- 要所要所でメモリフェンス命令を実行。
do_something()内のprintf()は実行されたら何か変なことになっている証拠。
■ ソース
※ __sync_synchronize() 関数はメモリフェンス命令を実行するgcc組み込みの関数。
# 長いなぁ
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <signal.h>
#include <stdint.h>
#include <inttypes.h>
int use_sync = 0;
volatile int locks[2] = {0, 0};
volatile unsigned long long num_spins[2] = {0, 0};
volatile unsigned long long num_processes[2] = {0, 0};
volatile int obj = -1;
volatile int done = 0;
/*
* ふたつのスレッドがそれぞれ引数0と1でこの関数を繰り返し呼び続ける。
*/
static void do_something(intptr_t id)
{
int tmp;
unsigned long long local_spins = 0;
/*
* 1. locks[自ID]を1にしてからlocks[他ID]を見る。
* 2. locks[他ID]が0なら次の処理へ進む。(ロード/ストア命令の追い越しがなければ同時にひとつのスレッドしか条件を満たさないはず)
* 3. locks[他ID]が非ゼロならlocks[自ID]一瞬0にして再び1に。そして2に戻る。
*/
locks[id] = 1;
if(use_sync) __sync_synchronize();
while(locks[!id])
{
locks[id] = 0;
if(use_sync) __sync_synchronize();
local_spins++;
locks[id] = 1;
if(use_sync) __sync_synchronize();
}
/* (ロード/ストア命令の追い越しがなければ) 同時にひとつのスレッドしか実行しないはず -- ここから */
num_spins[id] += local_spins;
tmp = obj;
if(tmp != -1)
{
/* (ロード/ストア命令の追い越しがなければ) ここには来ないはず */
printf("thread[%"PRIdPTR"]: obj = %d, locks[0] = %u, locks[1] = %u, local_spins = %llu\n",
id, tmp, locks[0], locks[1], local_spins);
}
obj = id;
if(use_sync) __sync_synchronize();
obj = -1;
if(use_sync) __sync_synchronize();
num_processes[id]++;
locks[id] = 0;
if(use_sync) __sync_synchronize();
/* (ロード/ストア命令の追い越しがなければ) 同時にひとつのスレッドしか実行しないはず -- ここまで */
}
static void *thread_proc(void *arg)
{
intptr_t id = (intptr_t)arg;
printf("thread[%"PRIdPTR"]: start.\n", id);
while(!done)
{
do_something(id);
}
return NULL;
}
/*
* シグナル処理スレッド。
*
* SIGUSR1: 経過確認用に各種カウンタ類を表示。あんまり正確じゃないのは御愛嬌。
* SIGTERM, SIGINT: プログラム終了。
*/
static void *sigthread_proc(void *arg)
{
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGUSR1);
sigaddset(&set, SIGTERM);
sigaddset(&set, SIGINT);
for(;;)
{
int sig;
if(sigwait(&set, &sig) == 0)
{
int i;
switch(sig)
{
case SIGUSR1:
for(i = 0; i < 2; i++)
{
printf("locks[%d] = %d, num_spins[%d] = %lld, num_processes[%d] = %lld\n",
i, locks[i], i, num_spins[i], i, num_processes[i]);
}
puts("");
break;
case SIGTERM:
case SIGINT:
done = 1;
break;
default:
break;
}
}
}
return NULL;
}
int main(int argc, char *argv[])
{
pthread_t sigth;
pthread_t ths[2];
intptr_t i;
sigset_t set;
/*
* 何か引数があったらuse_sync = 1 (メモリフェンス命令を使う),
* 引数がなければuse_sync = 0 (メモリフェンス命令を使わない)
*/
use_sync = (argc > 1);
sigemptyset(&set);
sigaddset(&set, SIGUSR1);
sigaddset(&set, SIGTERM);
sigaddset(&set, SIGINT);
pthread_sigmask(SIG_BLOCK, &set, NULL);
pthread_create(&sigth, NULL, &sigthread_proc, NULL);
pthread_detach(sigth);
for(i = 0; i < 2; i++)
{
pthread_create(&ths[i], NULL, &thread_proc, (void *)i);
}
for(i = 0; i < 2; i++)
{
pthread_join(ths[i], NULL);
}
puts("done.");
return 0;
}
しかし、C11での追加機能が使えるようになるのは何時になるんだろう。
ロード/ストア命令の追い越し例 More | Reply ログイン