アトミック #2

プロシージャコールがループに変わってしまう末尾最適化を"たいしたことない"と言い切ってしまう強さには敬服します。

えー、違う違う。末尾呼び出し最適化は関数呼び出しをループじゃなくて、ジャンプに変換する。末尾呼び出しする関数が自身じゃなくても最適化できることに注意。で、それこそ人間でも機械的にできるんだし、本来 call のところが、ローカル変数いじって jmp するだけなんだからたいしたことないでしょ。

それって結局

The_Value++ ;

っていうコードは

mov The_Value, %eax
inc %eax
mov %eax, The_Value

という風にコンパイルされて、コンパイル後のコードそれぞれの行間でディスパッチ(割り込み)が起きうるよ、という話を日本語に翻訳しただけの話だよね。んで、これが本質である以上、いわゆるマシン語の知識は必須だよね、プロなら。

ちーがーうー。そんな適当な説明していると「ロックの取得は高コストだから」とかいって、新人がひどいコード書いちゃいますよ?例えばこんなの(コードは C++)。

void atomic_inc(int &n)
{
    __asm__("incl %0" :"=m"(n));
}

x86_64 で gcc -O2 でコンパイルするとこうなる。

incl (%rdi)
retq

でも、このコードは全然だめ。「命令が複数あるからアトミックじゃない」わけじゃなくて、一命令でもアトミックじゃないわけですよ。いや、そんなことはわかっているとは思うんだけど。重要なのは機械語レベルでどういう実装になっているかじゃなくて、もう少し一般的なモデルに関する知識だったり、具体的なノウハウなんじゃないかなぁ。まじめに議論しようとすると、それこそ TAS、CAS、LL/SC だの言い出さないといけない。
でだ。世の中のプログラマがみんな C でマルチスレッドプログラム組んでいるわけじゃない訳ですよ。Java ではマルチスレッドも普通に行うけど、そこで必要なのは

  • Java での排他制御の仕組み(synchronized ブロック、synchronized メソッド)
  • volatile 修飾子をつけた場合/つけない場合の変数アクセスの振る舞い

で、それはまぁ、機械語レベルの知識があった方が理解は速いとは思うけど、必須かと言われれば、それはどうかなぁ。高級言語高級言語排他制御機構を持っていたりするわけで、それはその言語が規定している機構そのものを理解しないといけない。Erlang なんか変数を共有しないから排他制御なんてしないし。
機械語レベルの知識が必要な現場があるのもわかるし、知識があると自己解決できる場面があるのもわかる。じゃあ、トラブルがあったときに全部自己解決しないといけないかといえばそうでもなくて*1、それはそういうレイヤを得意としている人に丸投げする、という選択肢もあり得る。例えば、LL でプログラムを書いていて特定のアークテクチャで問題がよくわからない挙動を示したら、普通にバグレポートを投げりゃいいじゃん。と、まあそういうスタンスな訳ですよ。
ついでに、上のコードに問題があることを示すためのテストコードでも。
erb で書いているので適当に zsh

% erb t.erb > t.c
% gcc -O2 t.c -lpthread
% repeat 10 ./a.out

とでも。ちなみに Athlon 64 X2 4400+ だとこうなった。

8250157
8835858
7972059
9800634
7854913
8665203
9177237
9385040
9473736
8337679

シングルコアのシングルCPUなら問題は起こらない。

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <pthread.h>
#include <unistd.h>

#define NUM_THREADS 100

static void *run(void*);

static volatile int counter;

static pthread_mutex_t mutex;
static pthread_cond_t cond;

int main()
{
    int i;
    pthread_t threads[NUM_THREADS];

    if (pthread_mutex_init(&mutex, NULL) != 0) {
        perror("perror_mutex_create");
        exit(errno);
    }
    if (pthread_cond_init(&cond, NULL)) {
        perror("pthread_cond_init");
        exit(errno);
    }

    for (i = 0; i < NUM_THREADS; i++) {
        if (pthread_create(&threads[i], NULL, &run, NULL) != 0) {
            perror("pthread_create");
            exit(errno);
        }
    }

    sleep(1);
    counter = 0;

    pthread_mutex_lock(&mutex);
    pthread_cond_broadcast(&cond);
    pthread_mutex_unlock(&mutex);

    for (i = 0; i < NUM_THREADS; i++) {
        if (pthread_join(threads[i], NULL) != 0) {
            perror("pthread_join");
        }
    }

    pthread_cond_destroy(&cond);
    pthread_mutex_destroy(&mutex);

    printf("%d\n", counter);

    return 0;
}

static void *run(void *param __attribute__((__unused__)))
{
    pthread_mutex_lock(&mutex);
    pthread_cond_wait(&cond, &mutex);
    pthread_mutex_unlock(&mutex);

    <% 100000.times do %>
    __asm__("incl %0" : "=m"(counter));
    <% end %>

    return NULL;
}

*1:さすがにスレッド間の排他・同期制御ぐらいは理解してもらわないとあれだけど