C言語

意外と便利な gcc の builtin 関数

はじめに

マルチスレッドで構成されたプログラムで同じ変数(メモリ)にアクセスする場合、
値を変更するタイミングと参照するタイミングが同時に起きると、意図した動作とならない場合がある。

この現象を防ぐ手段として、mutex などを使ってスレッド間の処理を調停する仕組みが用意されており、
pthread_mutex_lock などの関数を使ったことのある方も多いと思います。

今回は、mutex 以外の手段として、GCC Builtin 関数をご紹介いたします。

SANACHAN
SANACHAN
Builtin関数は、GCCのコンパイラ自体に組み込まれた関数です。

 

参考

pthread 系の関数を使用する際の注意点を以下で紹介しています。ご参考まで。

 

意図しない動作とは?

先ずは、意図しない動作を行うサンプルプログラムを書き、動かしてみます。

 実行結果
(略)
ng_count=720

解説

プログラムはいたってシンプル。

30000個の要素数を持つ array_log に、100 という数値を加算します。
配列の要素の位置を pos で覚えており、output_log() が呼び出されるたびにインクリメントします。

 

シングルスレッドで動作させれば全ての要素に 100 がセットされることになり、
最後の check_and_exit()ng_count がインクリメントされることなく意図通りの結果となります。

 

しかし、今回は 2 つのスレッドから output_log() を呼び出しており、
値が 200 になっている要素が複数存在する結果となりました。

 

これは、2 つのスレッドから同じ要素に加算したことを意味します。
全て 100 となることが期待値に対して、意図しない結果となります。

SANACHAN
SANACHAN
配列の後半から 200 になる結果となります。
スレッドを生成して動き始めるまでに時間を要するため、競合の発生が遅れるためです。

 

なぜ?

なぜこのような現象が起きるのでしょうか?

詳細は割愛しますが、1 つのスレッドが pos を読み出すタイミングと、
もう 1 つのスレッドが pos を読み出すタイミングが同時になり、同じ要素へ書き込んでしまうためです。

 

GCC Builtinsを使わない場合

このようなスレッドの競合を防ぐ手段として、pthread_mutex_lock をよく使用します。
mutex を定義し、初期化して、競合が発生すると都合の悪い処理を lock/unlock で挟みます。

先ほどのサンプルプログラムでは、以下の★のような変更が必要になります。

 実行結果
ng_count=0

pthread ライブラリが提供している排他処理の仕組みを使うと、
意図した動作にすることができます。

 

しかし。。。とても面倒。

 

一つの変数を守るために追加しなければならない処理手順が多く、
そして mutex を宣言することでメモリも余分に消費します。

 

GCC Builtins を使った場合

そこで今回ご紹介する GCC Builtins を使うとどうなるのか、さっそく見ていきましょう。

 実行結果
ng_count=0

SANACHAN
SANACHAN
なんと、これだけです!

 

GCC Builtins 関数の __sync 系は、Atomic Memory Access を提供しています。
要は、メモリバリア付きのアクセスを行うアセンブラを、コンパイル時に吐き出してくれます。

詳しくは公式ページで紹介されていますが、今回の「加算」だけではなく、
減算、論理演算に対する関数も提供されています。

 

また、加算前の値を取得するのか、加算後の値を取得するのか、
どちらに対応する場合でも、それらの関数が提供されています。

例えば、先ほどの __sync_fetch_and_add は加算前の値を取得して加算、
対する __sync_add_and_fetch は加算後の値を取得することができます。

SANACHAN
SANACHAN
pthreadの排他系、同期系の関数も、これらの Atomic 処理を使っています。

 

Tips

いやいや、先ほどのログの例の場合、要素を保持する index はループするのが普通でしょ!
と、思われたかもしれません。

ループさせたい場合は、if 文で pos の値をチェックし、最大数を超えていたら 0 にする必要があります。
もちろん、この区間も含めて排他する必要があり、GCC Builtins だけでは実現できません。

上記のような処理を行いたい場合は、以下のように記述します。

要素位置を記憶している pos は永遠にインクリメントしていき、
使用する際に AND を使って正しい位置を算出します。

SANACHAN
SANACHAN
ポイントは、最大要素数を 0x1000 のように、1ビットだけ立てた値にすること。

 

AND する値は 0x1000 - 1 = 0xFFF とすることで、要素数を超えた pos=0x1000 の場合は idx=0、
pos=0x1001 の場合は idx=1 という要素位置を得ることができるようになります。

 

こちらの記事もよく読まれています

  • この記事を書いた人
  • 最新記事
SANACHAN

SANACHAN

「生涯一エンジニア」を掲げ、大手グローバル企業でSE/PGとして8年勤め、キャリアアップ転職した現役のエンジニアです。世にあるメジャーな全プログラム言語(コボル除く)を自由に扱えます。一児の父。自分のため、家族のため、日々勉強してます。システムエンジニア、プログラミングに関する情報を蓄積している雑記帳です。

-C言語
-, , , ,