コグノスケ


link 未来から過去へ表示(*)  link 過去から未来へ表示

link もっと前
2023年12月10日 >>> 2023年12月1日
link もっと後

2023年12月10日

fno-builtinによるlibc関数呼び出し最適化の抑制

目次: GCC

GCCはlibcの関数呼び出しを別の関数呼び出しに置き換える最適化(名前がわからないので以降「libc関数呼び出し最適化」と呼びます)を行います。例を挙げると、

  • 1文字出力するprintf関数→putchar関数
  • 行末に改行がある文字列を出力するprintf関数→puts関数

時には勝手にlibcの関数呼び出しを置き換えてほしくないこともあると思います。libc関数呼び出し最適化を抑えることができるコンパイラオプションfno-builtinを指定したときの挙動についてメモしておきます。

GCCのバージョンは下記のとおりです。

GCCのバージョン
$ gcc --version

gcc (Debian 12.2.0-14) 12.2.0
Copyright (C) 2022 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

テストに使うプログラムは下記の通りです。

テストプログラム

int printf(const char *a, ...);

void _start(void)
{
        printf("\n");
}

特に何も指定しなければ、1文字出力するprintfはputchar呼び出しに置き換えられるはずです。確認のためnostdlibオプションを付けてコンパイルし、リンクエラーを見るとprintfの呼び出しがどの関数に置き換えられたかわかります。

nostdlibでのコンパイル、リンク結果
$ gcc -O2 -g -Wall a.c -nostdlib

/usr/bin/ld: /tmp/cc1D673J.o: in function `_start':
/home/katsuhiro/share/test_gcc_no_builtin/a.c:5: undefined reference to `putchar'
collect2: error: ld returned 1 exit status

リンクエラーは「putcharがない」と出ました。printfの呼び出しがputcharの呼び出しに置き換えられたことがわかりますね。次はfno-builtinオプションを指定します。

fno-builtin、nostdlibでのコンパイル、リンク結果
$ gcc -O2 -g -Wall a.c -nostdlib -fno-builtin

/usr/bin/ld: /tmp/cc6JbRD2.o: in function `_start':
/home/katsuhiro/share/test_gcc_no_builtin/a.c:5: undefined reference to `printf'
collect2: error: ld returned 1 exit status

リンクエラーは「printfがない」と出ました。fno-builtinオプションによってprintfからputcharへの置き換えが抑制されたことがわかります。

fno-builtinの仕様なのか?

動作結果を見る限りfno-builtinオプションでlibc関数呼び出し最適化が抑制されましたが、この挙動がオプションの仕様か?と聞かれると正直良くわかりません

仕様はGCCのマニュアル(Other Builtins (Using the GNU Compiler Collection (GCC)))に書かれていて、説明によれば残りのビルトイン関数(printfを含む)は最適化のために提供されている("The remaining functions are provided for optimization purposes.")とあります。

  • ビルトイン関数を無効にした
  • 最適化後に呼び出すためのビルトイン関数がない
  • libc関数呼び出し最適化そのものが無効になった

私はこのような動作をしているものと推測しています。正しいかどうかは知りません……。

最適化はビルトイン関数に頼る必要はありません。つまりfno-builtinオプションで抑制できないようなlibc関数呼び出し最適化があっても不思議ではありません。今のところそのような例は見つけていませんが、もしあったとすると抑制する方法はあるのでしょうか?うーん??

編集者:すずき(2023/12/11 02:56)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2023年12月3日

musl libcのpthread_barrier_wait()の実装 その3、インスタンス

目次: C言語とlibc

参考: 図を書くために使ったlink PlantUMLのコードです。

前回の続きです。musl libc 1.2.4のpthread_barrier_wait()の実装と動作の概要です。バリアに到達したスレッドがNスレッド(N >= 2)あるとすると、大きく分けて3つの役割に分かれます。役割名は私が適当に付けました。正式な名前はあるのかな?

  • Owner: 最初にバリアに突入してきたスレッド(1スレッド)
  • Last: 最後にバリアに突入してきたスレッド(1スレッド)
  • Others: OwnerでもLastでもないスレッド全て(0〜N-2スレッド、平たく言えば3スレッド以上のバリアのときだけ存在する)

今回はインスタンスがなぜローカル変数として確保されるかを紹介します。1回目と2回目のバリアが重なるところがポイントです。

スレッドの役割は変わる

各スレッドはOwnerかLastかOthersになりますが、役割は毎回のバリア同期処理ごとに変わります。例えばバリアとバリアの間の処理時間が同じだとすると、あるバリアのOwnerは次のバリアではLastになる可能性が高いです。Ownerはバリアから最後に脱出しますので、その間に他のスレッドの処理が進んで次のバリア同期処理のOwnerになるからです。

3スレッドあって下記のように役割が変化する場合を考えてみます。

  • スレッド1: Owner1, Last2: 1回目Owner、2回目Last
  • スレッド2: Others1, Owner2: 1回目Others、2回目Owner
  • スレッド3: Last1, Others2: 1回目Last、2回目Others

更に考えてみるとバリアとバリアの間の処理時間が非常に短い場合、1回目のバリアのOwner(スレッド1、Owner1, Last2)がバリアを脱出する前に、違うスレッドが2回目のバリアのOwner(スレッド2、Others1, Owner2)となってバリアに到達している可能性があります。シーケンス例は下記のようになります。


インスタンスが1つしかない場合のシーケンス例

もしバリアのインスタンスをグローバル変数などに確保した場合、1回目のバリアのOwner(スレッド1、Owner1, Last2)によるインスタンスの破棄と、2回目のバリアのOwner(スレッド2、Others1, Owner2)のインスタンスの初期化がぶつかって、お互いに内容を壊しあうため正常に動作しません。

この問題を解決にはインスタンスをOwnerスレッド固有の領域に置くと良いです。1回目のバリアのOwnerスレッドと、2回目のバリアのOwnerスレッドがそれぞれ別の領域に置けば、お互いに壊し合うことがなくなります。

ローカル変数はスレッド固有の領域

スレッドごとの固有の領域として使えるのは、

TLS(Thread Local Storage) ヒープから確保したメモリ(mallocなど) ローカル変数

があります。インスタンスを各Ownerスレッド固有の領域においたとき、2回分のバリア動機処理のシーケンス例は下記のようになります。


インスタンスがスレッドごとにある場合のシーケンス例

バリアのインスタンスは複数スレッド間で共有する領域です。ローカル変数を複数スレッド間で共有すると、ローカル変数が破棄されたときに問題が発生するので、ヒープを使うことが多いでしょう。

しかし前回説明したようにバリア同期処理の場合はローカル変数の寿命をうまく制御できるため、複数スレッド間で共有しても問題が起きません。インスタンスをローカル変数に確保すると、ヒープに比較して高速なメモリ割当が可能です。

編集者:すずき(2023/12/09 16:19)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2023年12月2日

musl libcのpthread_barrier_wait()の実装 その2、LastとOthers

目次: C言語とlibc

前回の続きです。musl libc 1.2.4のpthread_barrier_wait()の実装と動作の概要です。バリアに到達したスレッドがNスレッド(N >= 2)あるとすると、大きく分けて3つの役割に分かれます。役割名は私が適当に付けました。正式な名前はあるのかな?

  • Owner: 最初にバリアに突入してきたスレッド(1スレッド)
  • Last: 最後にバリアに突入してきたスレッド(1スレッド)
  • Others: OwnerでもLastでもないスレッド全て(0〜N-2スレッド、平たく言えば3スレッド以上のバリアのときだけ存在する)

いっぺんに説明すると意味不明なので、順番に説明します。今回はLastとOthersです。

LastとOthers(到達側)

自分がLastかどうか判定する条件はバリア変数の_b_instがNULL以外であり、インスタンスのカウントがバリア同期するスレッド数と一致(= 最後に到達した1スレッド)することです。Lastのやることはバリア変数(pthread_barrier_t)に設定されたインスタンスを消すことと、バリア同期を待っているOthersを起こすことです。

Lastがこの時点でb->_b_instをNULLにしていること、ローカル変数instを使っていてb->_b_instを使わないことには理由があって、次のバリア処理とのオーバーラップと関係しています。同時に説明するとややこしいので、次回ご説明しようと思います。

OwnerとLast以外は全部Othersスレッドとなります。OthersのやることはLastが来るまで待つことです。

LastとOthersの到達側に関連するコードは下記のとおりです。

pthread_barrier_wait()のLastとOthersの到達側のコード

// musl/src/thread/pthread_barrier_wait.c

int pthread_barrier_wait(pthread_barrier_t *b)
{
	int limit = b->_b_limit;
	struct instance *inst;

	/* Trivial case: count was set at 1 */
	if (!limit) return PTHREAD_BARRIER_SERIAL_THREAD;

	/* Process-shared barriers require a separate, inefficient wait */
	if (limit < 0) return pshared_barrier_wait(b);

	/* Otherwise we need a lock on the barrier object */
	while (a_swap(&b->_b_lock, 1))
		__wait(&b->_b_lock, &b->_b_waiters, 1, 1);
	inst = b->_b_inst;

	/* First thread to enter the barrier becomes the "instance owner" */
	if (!inst) {
		//★★Ownerのスレッドの処理は省略(その1をご覧ください)★★
	}

	/* Last thread to enter the barrier wakes all non-instance-owners */
	if (++inst->count == limit) {
		//★★Lastのスレッドはこちら★★
		b->_b_inst = 0;
		a_store(&b->_b_lock, 0);
		if (b->_b_waiters) __wake(&b->_b_lock, 1, 1);
		a_store(&inst->last, 1);
		if (inst->waiters)
			__wake(&inst->last, -1, 1);
	} else {
		//★★Othersのスレッドはこちら★★
		a_store(&b->_b_lock, 0);
		if (b->_b_waiters) __wake(&b->_b_lock, 1, 1);
		__wait(&inst->last, &inst->waiters, 0, 1);
	}

細かい部分は省いてシーケンスの例を1つだけ示せば下記のようになります。


pthread_barrier_wait()のLastとOthersの到達側シーケンスの一例

見たままなので解説することがないですね。全員でインスタンスのカウントを+1して、最後のスレッドLastだけが特殊な処理を行います。

LastとOthers(脱出側)

脱出側のやることは単純ですが役割分担が少しややこしいです。到達側と同様にLastとOthersに役割が分かれますが、到達側のLast = 脱出側のLastとは限らないからです。

脱出時のLastになる条件はインスタンスのカウント値が1であることです。到達時にLastであったかOthersであったかは無関係です。脱出時のLastのやることは、インスタンスのfinishedを+1して待機中のOwnerスレッドを再開させることです。

脱出時のOthersになる条件はLastではない、インスタンスのカウント値が1以外であることです。

LastとOthersの脱出側に関連するコードは下記のとおりです。

pthread_barrier_wait()のLastとOthersの脱出側のコード

// musl/src/thread/pthread_barrier_wait.c

int pthread_barrier_wait(pthread_barrier_t *b)
{
	int limit = b->_b_limit;
	struct instance *inst;

	//★★略★★

	/* Last thread to exit the barrier wakes the instance owner */
	if (a_fetch_add(&inst->count,-1)==1 && a_fetch_add(&inst->finished,1))
		__wake(&inst->finished, 1, 1);

	return 0;
}

細かい部分は省いてシーケンスの例を1つだけ示せば下記のようになります。


pthread_barrier_wait()のLastとOthersの脱出側シーケンスの一例

到達側のLastスレッドの処理では待機していたOthersスレッド達を全員再開させ、Lastも処理を再開します。するとLast + Othersスレッド全てがいっぺんに脱出側の処理を開始します。先ほど説明したとおり、どのスレッドが脱出側のLastになるかは運次第です。

実装の特徴はアトミックアクセスですかね。a_fetch_add(x, -1)はポインタxの指す先をアトミックに-1して、返り値でxの以前の値を返す関数です……といわれてもわかりにくいですよね。4スレッド(Owner, Others1, Others2, Last)の場合を書きましょうか。Ownerスレッドはカウント値を+1しないので、脱出処理開始時のカウント値は4 - 1 = 3です。

スレッド-1したあとのカウント値a_fetch_add()の返り値
Others 123
Others 212
Last 01

ちなみにアトミックアクセス以外の方法(if文とカウント値の変更など)では正常に動作しません。判定と値変更の間に他のスレッドが処理を行う可能性があるからです。

続きはまた今度。

編集者:すずき(2023/12/04 03:49)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2023年12月1日

musl libcのpthread_barrier_wait()の実装 その1、Owner

目次: C言語とlibc

誰得かわかりませんが、musl libc 1.2.4のpthread_barrier_wait()の実装と動作の概要です。このAPIはバリア同期を実現するためのAPIで、POSIXという規格の一部です。バリア同期はある地点に指定した数のスレッドが全員到達するまで、全スレッドを待機させる同期機構です。

バリアに到達したスレッドがNスレッド(N >= 2)あるとすると、大きく分けて3つの役割に分かれます。役割名は私が適当に付けました。正式な名前はあるのかな?

  • Owner: 最初にバリアに突入してきたスレッド(1スレッド)
  • Last: 最後にバリアに突入してきたスレッド(1スレッド)
  • Others: OwnerでもLastでもないスレッド全て(0〜N-2スレッド、平たく言えば3スレッド以上のバリアのときだけ存在する)

いっぺんに説明すると意味不明なので、順番に説明します。最初はOwnerからです。

Owner

自分がOwnerかどうか判定する条件はバリア変数の_b_instがNULLであることです。Ownerのやることはインスタンスを作成しバリア変数(pthread_barrier_t)に設定することと、バリアから一番「最後」に脱出してインスタンスを破棄することです。インスタンスはmuslの用語ですかね?それはさておいてインスタンスは1回のバリア同期に必要なパラメータがおかれた場所です。

インスタンスの必要性を簡易に説明するのは難しいですね……バリア変数は使いまわされるからです。具体例で言うと、1回目のバリアからスレッドが脱出している途中で、先に脱出した他のスレッドが2回目のバリアに到達する、というケースが発生します。もしバリアのカウンタ値などをバリア変数に置いてしまうと、1回目のバリアの処理と2回目のバリアの処理が混ざって正常に処理できなくなります。

Ownerに関連するコードは下記のとおりです。

pthread_barrier_wait()のOwner関連のコード

// musl/src/thread/pthread_barrier_wait.c

int pthread_barrier_wait(pthread_barrier_t *b)
{
	int limit = b->_b_limit;
	struct instance *inst;

	/* Trivial case: count was set at 1 */
	if (!limit) return PTHREAD_BARRIER_SERIAL_THREAD;

	/* Process-shared barriers require a separate, inefficient wait */
	if (limit < 0) return pshared_barrier_wait(b);

	/* Otherwise we need a lock on the barrier object */
	while (a_swap(&b->_b_lock, 1))
		__wait(&b->_b_lock, &b->_b_waiters, 1, 1);
	inst = b->_b_inst;

	/* First thread to enter the barrier becomes the "instance owner" */
	if (!inst) {
		struct instance new_inst = { 0 };
		int spins = 200;
		b->_b_inst = inst = &new_inst;
		a_store(&b->_b_lock, 0);
		if (b->_b_waiters) __wake(&b->_b_lock, 1, 1);
		while (spins-- && !inst->finished)
			a_spin();
		a_inc(&inst->finished);
		while (inst->finished == 1)
			__syscall(SYS_futex,&inst->finished,FUTEX_WAIT|FUTEX_PRIVATE,1,0) != -ENOSYS
			|| __syscall(SYS_futex,&inst->finished,FUTEX_WAIT,1,0);
		return PTHREAD_BARRIER_SERIAL_THREAD;
	}

	//...

各所に工夫が散りばめられていて全部説明すると30分くらい説明が必要な気がしますが、細かい部分は省いてシーケンスの例を1つだけ示せば下記のようになります。


pthread_barrier_wait()のOwnerシーケンスの一例

特徴的というか個人的に感心した点は、インスタンスをローカル変数で確保していることです。変数のアドレスは一般的にはスレッドのスタック領域の一部になることが多いでしょう。ローカル変数の特徴として、

  • 生存期間は関数実行している間だけ
  • 他のスレッドと干渉しない

1つ目の特徴はmusl libcのバリア実装に限って言えば問題ありません。Ownerが必ず最後にバリア同期APIを脱出するように実装が工夫されていて、壊れたローカル変数に他のスレッドがアクセスする状況が発生しないからです。

2つ目の特徴はうまく活かしています。ローカル変数はバリアに一番最初にたどり着いたスレッド(= Owner)が、他スレッドと干渉せずインスタンスを確保できる簡単かつ高速な方法です。mallocのようなヒープでも目的は達成できますが、速度的に不利でしょう。面白い実装ですね。

続きはまた今度。

編集者:すずき(2023/12/04 02:43)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



link もっと前
2023年12月10日 >>> 2023年12月1日
link もっと後

管理用メニュー

link 記事を新規作成

<2023>
<<<12>>>
-----12
3456789
10111213141516
17181920212223
24252627282930
31------

最近のコメント5件

  • link 24年4月22日
    hdkさん (04/24 08:36)
    「うちのHHFZ4310は15年突破しまし...」
  • link 24年4月22日
    すずきさん (04/24 00:37)
    「ちゃんと数えてないですけど蛍光管が10年...」
  • link 24年4月22日
    hdkさん (04/23 20:52)
    「おお... うちのHHFZ4310より後...」
  • link 20年6月19日
    すずきさん (04/06 22:54)
    「ディレクトリを予め作成しておけば良いです...」
  • link 20年6月19日
    斎藤さん (04/06 16:25)
    「「Preferencesというメニューか...」

最近の記事3件

  • link 24年2月7日
    すずき (04/24 02:52)
    「[複数の音声ファイルのラウドネスを統一したい] PCやデジタル音楽プレーヤーで音楽を聞いていると、曲によって音量の大小が激しく...」
  • link 24年4月22日
    すずき (04/23 20:13)
    「[仕事部屋の照明が壊れた] いきなり仕事部屋のシーリングライトが消えました。蛍光管の寿命にしては去年(2022年10月19日の...」
  • link 24年4月17日
    すずき (04/18 22:44)
    「[VSCodeとMarkdownとPlantUMLのローカルサーバー] 目次: LinuxVSCodeのPlantUML Ex...」
link もっとみる

こんてんつ

open/close wiki
open/close Linux JM
open/close Java API

過去の日記

open/close 2002年
open/close 2003年
open/close 2004年
open/close 2005年
open/close 2006年
open/close 2007年
open/close 2008年
open/close 2009年
open/close 2010年
open/close 2011年
open/close 2012年
open/close 2013年
open/close 2014年
open/close 2015年
open/close 2016年
open/close 2017年
open/close 2018年
open/close 2019年
open/close 2020年
open/close 2021年
open/close 2022年
open/close 2023年
open/close 2024年
open/close 過去日記について

その他の情報

open/close アクセス統計
open/close サーバ一覧
open/close サイトの情報

合計:  counter total
本日:  counter today

link About www.katsuster.net
RDFファイル RSS 1.0

最終更新: 04/24 08:36