レベル: 中級 M. Tim Jones, Consultant Engineer, Emulex Corp.
2008年 11月 18日 Linux® カーネルは GCC (GNU Compiler Collection) スイートの特殊な機能を使用します。これらの機能は、ショートカットを提供したり単純化を実現したりするものから、コンパイラーに最適化のヒントを提供するものまでさまざまです。この記事では特殊な GCC 機能のいくつかを取り上げ、Linux カーネルでその機能をどのように活用するかを説明します。
GCC と Linux は相性抜群のペアです。この 2 つはそれぞれに独立したソフトウェアではありますが、新しいアーキテクチャー上で動作する Linux を実現する上で、Linux は全面的に GCC に依存しています。Linux はさらに、機能の充実と最適化のためにも GCC ならではの機能 (GCC 拡張機能) を利用しています。この記事では、これらの重要な拡張機能の多くを取り上げ、Linux カーネル内部でのその使用方法を紹介します。
GCC は現行の安定バージョン (バージョン 4.3.2) で、以下の 3 つの C 標準をサポートします。
- C 言語本来の ISO (International Organization for Standardization) 標準 (ISO C89 または C90)
- Amendment 1 が適用された ISO C90
- 現行の ISO C99 (この記事で前提とする、GCC が使用するデフォルト標準)
注: この記事では、読者が ISO C99 標準を使用していることを前提とします。ISO C99 より古い標準を指定すると、記事で取り上げる一部の拡張機能を使用できない場合があります。GCC が実際に使用する標準を指定するには、コマンドラインから -std オプションを指定します。GCC マニュアルを参考に、標準のどのバージョンでどの拡張機能がサポートされているかを確認してください (「参考文献」にリンクを記載)。
 |
適用可能なバージョン
この記事の説明は、2.6.27.1 Linux カーネルで、GCC バージョン 4.3.2 の GCC 拡張機能を使用する場合に重点を置いています。それぞれの C 拡張機能では Linux カーネル・ソース内のファイルを参照していますが、このファイルにサンプル・コードが含まれています。
|
|
使用可能な C 拡張機能は何通りかの方法で分類できますが、この記事では以下の 2 つのカテゴリーに大別して説明します。
- 機能に関する拡張機能。GCC から新しい機能を引き出します。
- 最適化に関する拡張機能。より効率的なコードを生成できるようにします。
機能に関する拡張機能
まずは、標準 C 言語を拡張する GCC の裏技を探ります。
型検出
GCC では変数を参照することによって型を識別できるようになっています。この類の操作では、一般にジェネリック (汎用) プログラミングと呼ばれている形を使用することができます。これと同様の機能は、C++、Ada、そして Java™ 言語など、最近の多くのプログラミング言語でも見つかるはずです。Linux では、typeof を使用して、型に依存する演算処理 (min、max など) を作成します。リスト 1 に、typeof によって汎用マクロを作成する方法を記載します (./linux/include/linux/kernel.h から抜粋)。
リスト 1. typeof を使用した汎用マクロの作成
#define min(x, y) ({ \
typeof(x) _min1 = (x); \
typeof(y) _min2 = (y); \
(void) (&_min1 == &_min2); \
_min1 < _min2 ? _min1 : _min2; })
|
範囲拡張
GCC に含まれる範囲のサポートは、C 言語のさまざまな領域で活用することができます。これに該当する領域の 1 つは、switch/case ブロック内の case 文です。複雑な条件構造では通常 if 文のカスケードに依存しなければ実現できないような結果が、リスト 2 では簡潔に表現されています (./linux/drivers/scsi/sd.c から抜粋)。switch/case を使用すれば、ジャンプ・テーブルの実装を使用することによってコンパイラーを最適化することも可能です。
リスト 2. case 文のなかでの範囲の使用
static int sd_major(int major_idx)
{
switch (major_idx) {
case 0:
return SCSI_DISK0_MAJOR;
case 1 ... 7:
return SCSI_DISK1_MAJOR + major_idx - 1;
case 8 ... 15:
return SCSI_DISK8_MAJOR + major_idx - 8;
default:
BUG();
return 0; /* shut up gcc */
}
}
|
以下に示すように、範囲は初期化にも適用することができます (./linux/arch/cris/arch-v32/kernel/smp.c から抜粋)。この例では、spinlock_t からサイズが LOCK_COUNT の配列を作成しています。配列の各要素は、SPIN_LOCK_UNLOCKED の値で初期化されます。
/* Vector of locks used for various atomic operations */
spinlock_t cris_atomic_locks[] = { [0 ... LOCK_COUNT - 1] = SPIN_LOCK_UNLOCKED};
|
また、範囲ではこれより複雑な初期化もサポートします。以下は、配列のサブ範囲の初期値を指定するコードの例です。
int widths[] = { [0 ... 9] = 1, [10 ... 99] = 2, [100] = 3 };
|
ゼロ長の配列
標準 C では、配列を構成する要素を少なくとも 1 つは定義しなければなりません。コードの設計が複雑になりがちなのは、この要件が原因となっています。一方 GCC ではゼロ長の配列という概念をサポートしており、構造体を定義する際、この概念を利用すると便利です。この概念は ISO C99 での柔軟な配列メンバーに似ていますが、使用する構文は異なります。
以下の例 (./linux/drivers/ieee1394/raw1394-private.h から抜粋) で構造体の最後で宣言しているのは、要素数がゼロの配列です。これにより、構造体のメンバーは、構造体インスタンスのすぐ後に続くメモリーを参照することが可能になります。この方法は、配列の要素の数を可変にしなければならない場合に役立つはずです。
struct iso_block_store {
atomic_t refcount;
size_t data_size;
quadlet_t data[0];
};
|
呼び出しアドレスの判断
多くのインスタンスでは、特定の関数の呼び出し側を判断することが役に立つ場合や、あるいはその必要がある場合があります。GCC には、まさにそれを目的にした組み込み関数、__builtin_return_address が用意されています。この関数は一般的にデバッグに使用されているものですが、カーネル内では他にも多くの用途があります。
以下のコードに示されているように、__builtin_return_address は level という引数を取ります。この引数が定義するのは、戻りアドレスの取得対象とする呼び出しスタックのレベルです。例えば level を 0 に指定すると、現行の関数の戻りアドレスを要求することになり、level を 1 に指定すると、呼び出し側関数の戻りアドレスを要求することになります。
void * __builtin_return_address( unsigned int level );
|
以下の例 (./linux/kernel/softirq.c から抜粋) にある local_bh_disable は、ローカル側プロセッサーでのソフトウェア割り込みを無効にして、softirq、tasklet、ボトムハーフが現行プロセッサーで実行されないようにします。__builtin_return_address を使用して戻りアドレスを取得するため、後で追跡する際には、このアドレスを使用することが可能になります。
void local_bh_disable(void)
{
__local_bh_disable((unsigned long)__builtin_return_address(0));
}
|
定数検出
GCC には、コンパイル時に値が定数であるかどうかを判断するために使用できる組み込み関数が用意されています。定数であることがわかれば定数畳み込みによって最適化できる式を作成できるため、値が定数であるかどうかは貴重な情報となります。この __builtin_constant_p 関数は、定数であるかどうかをテストするために使用します。
以下に、__builtin_constant_p のプロトタイプを記載します。__builtin_constant_p はすべての定数を確認できるわけではないことに注意してください。これは、一部の定数は GCC によって定数であることを証明するのが簡単ではないためです。
int __builtin_constant_p( exp )
|
Linux では定数検出をかなり頻繁に使用します。リスト 3 に記載する例 (./linux/include/linux/log2.h から抜粋) では、定数検出を使用して roundup_pow_of_two マクロを最適化します。式を定数として確認できた場合には (最適化に使用可能な) 定数式が使用されます。式が定数でない場合には、値を 2 の累乗に切り上げるために別のマクロ関数が呼び出されます。
リスト 3. 定数の検出によるマクロ関数の最適化
#define roundup_pow_of_two(n) \
( \
__builtin_constant_p(n) ? ( \
(n == 1) ? 1 : \
(1UL << (ilog2((n) - 1) + 1)) \
) : \
__roundup_pow_of_two(n) \
)
|
関数属性
GCC には多種多様な関数レベルの属性が用意されており、これらの属性によって、最適化プロセスを支援する、より多くのデータをコンパイラーに提供することができます。このセクションでは機能に関連する属性を抜粋して説明します。最適化に影響する属性については次のセクションで説明します。
リスト 4 に示すように、属性にはシンボリック定義によって別名が割り当てられます。このリストに照らし合わせながら、(/linux/include/linux/compiler-gcc3.h で定義された) 属性の使用方法を説明するソース参照を読んでください。
リスト 4. 関数属性の定義
# define __inline__ __inline__ __attribute__((always_inline))
# define __deprecated __attribute__((deprecated))
# define __attribute_used__ __attribute__((__used__))
# define __attribute_const__ __attribute__((__const__))
# define __must_check __attribute__((warn_unused_result))
|
リスト 4 に記載した定義は、GCC で使用可能な一部の関数属性を反映しています。これらの関数属性は、Linux カーネルでもとりわけ役立つものです。以下に、その最適な使用方法を説明します。
always_inline は GCC に対し、最適化が有効であるかどうかに関わらず、指定した関数をインライン化するように指定します。
deprecated は、関数がすでに廃止されていて、現在使用できないことを通知します。廃止された関数を使用しようとすると、警告を受け取ります。この属性を型および変数に適用すれば、開発者がこれらのカーネル・アセットに依存しないようにすることができます。
__used__ はコンパイラーに対し、GCC が該当する関数への呼び出しインスタンスを検出するかしないかに関わらず、この関数を使用するように指示します。この属性は、C 関数をアセンブラから呼び出す場合に役立ちます。
__const__ はコンパイラーに対し、ある特定の関数には状態がないことを通知します (つまり、関数は渡された引数を使用して結果を生成し、返すということです)。
warn_unused_result は、すべての呼び出し側が関数の結果を確認していることをコンパイラーに確認させます。これにより、呼び出し側が対応するエラーを処理できるように、適切に関数の結果を確認することになります。
以下は、Linux カーネルでの上記の関数の使用例です。deprecated の例はアーキテクチャーに固有ではないカーネル (./linux/kernel/resource.c) から抜粋し、const の例は IA64 カーネル・ソース (./linux/arch/ia64/kernel/unwind.c) から抜粋しています。
int __deprecated __check_region(struct resource
*parent, unsigned long start, unsigned long n)
static enum unw_register_index __attribute_const__
decode_abreg(unsigned char abreg, int memory)
|
最適化に関する拡張機能
ここからは、できる限り最適なマシン・コードを作成するために用意された GCC の裏技を探っていきます。
分岐予測のヒント
Linux カーネルで最も使用されている最適化手法のなかで、最も汎用的な手法の 1 つは、__builtin_expect です。条件付きコードに取り組む際には、最も可能性の高い分岐がどれで、そうではない分岐がどれかであるかが明らかな場合がよくあります。コンパイラーにこのような予測情報があれば、分岐に関する最適なコードを生成することが可能になります。
以下に示すように、__builtin_expect を使用するには、likely と unlikely という 2 つのマクロをベースにします (./linux/include/linux/compiler.h から抜粋)。
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
|
__builtin_expect によって、コンパイラーは提供された予測情報に従って命令選択の決定を行えるようになるため、コードが条件に即して (分岐によるジャンプもなく) 実行される可能性が極めて高くなります。それだけでなく、キャッシングおよび命令のパイプライン化を改善することにもなります。
例えば、条件に「likely」というマークが付いていれば、コンパイラーは (おそらく分岐命令によるジャンプはないであろう) 分岐命令の直後にコードの True の部分を配置することができ、条件の False の部分 (実行される可能性も低く、パフォーマンスも最適ではない部分) は分岐命令によってジャンプした先に配置されるようになります。このようにして、コードは最も可能性の高い場合に合わせて最適化されるというわけです。
リスト 5 に、likely と unlikely 両方のマクロを使用した関数を記載します (./linux/net/core/datagram.c から抜粋)。この関数が期待する内容は、sum 変数がゼロになり (checksum はパケットに有効)、ip_summed 変数が CHECKSUM_HW と等しくならないということです。
リスト 5. likely および unlikely マクロの使用例
unsigned int __skb_checksum_complete(struct sk_buff *skb)
{
unsigned int sum;
sum = (u16)csum_fold(skb_checksum(skb, 0, skb->len, skb->csum));
if (likely(!sum)) {
if (unlikely(skb->ip_summed == CHECKSUM_HW))
netdev_rx_csum_fault(skb->dev);
skb->ip_summed = CHECKSUM_UNNECESSARY;
}
return sum;
}
|
プリフェッチ
パフォーマンスを向上させるもう 1 つの重要な方法は、必要なデータをプロセッサーの近くのメモリーにキャッシングすることです。キャッシングにより、データにアクセスする時間は最小限に短縮されます。最近のほとんどのプロセッサーはアクセス時間の異なる 3 種類のメモリーを内部に持っています。
- レベル 1 キャッシュ。通例、単一サイクルでのアクセスが可能です。
- レベル 2 キャッシュ。2 サイクルでのアクセスが可能です。
- システム・メモリー。これより長いアクセス・サイクルを必要とします。
アクセス待ち時間を最小限にしてパフォーマンスを向上させる最善の方法は、データを最も近いメモリーに入れることです。このタスクを手動で行うことを、プリフェッチと呼びます。GCC がデータの手動プリフェッチをサポートするために使う手段は、__builtin_prefetch という組み込み関数です。この関数を使用して、データが必要になる直前にデータをキャッシュに入れます。__builtin_prefetch 関数は以下の 3 つの引数を取ります。
- データのアドレス
rw パラメーター。このパラメーターを使用して、Read 操作のためにデータを取り込んでいるのか、Write 操作のためにデータを準備しているのかを示します。
locality パラメーター。このパラメーターは、データをキャッシュに残しておくか、あるいは使用した後にパージするかを定義するために使用します。
void __builtin_prefetch( const void *addr, int rw, int locality );
|
Linux カーネルでは、プリフェッチを広範に使用します。なかでも最もよく使われている方法は、マクロとラッパー関数によるプリフェッチです。リスト 6 は、組み込み関数にラッパーを適用したヘルパー関数の例です (./linux/include/linux/prefetch.h から抜粋)。この関数はストリーム操作に対し、プリエンプティブな先読みメカニズムを実装します。この関数を使用するとキャッシュ・ミスとパイプラインのストールが最小限になるため、通常はパフォーマンスの向上につながります。
リスト 6. ラッパー関数による範囲のプリフェッチ
#ifndef ARCH_HAS_PREFETCH
#define prefetch(x) __builtin_prefetch(x)
#endif
static inline void prefetch_range(void *addr, size_t len)
{
#ifdef ARCH_HAS_PREFETCH
char *cp;
char *end = addr + len;
for (cp = addr; cp < end; cp += PREFETCH_STRIDE)
prefetch(cp);
#endif
}
|
変数属性
この記事で前述した関数属性に加え、GCC には変数および型定義のための属性もあります。そのうち最も重要な属性の 1 つは、メモリー内のオブジェクトの配置に使用する aligned 属性です。オブジェクトの配置はパフォーマンスにとって重要であるだけでなく、特定のデバイスまたはハードウェア構成には必須となる場合もあります。aligned 属性が取る引数は 1 つだけで、この引数によって目的とする配置タイプが指定されます。
以下は、ソフトウェア・サスペンドに使用する例です (./linux/arch/i386/mm/init.c から抜粋)。PAGE_SIZE オブジェクトは、ページ位置合わせの必要に応じて定義されます。
char __nosavedata swsusp_pg_dir[PAGE_SIZE]
__attribute__ ((aligned (PAGE_SIZE)));
|
リスト 7 の例では、最適化に関する要点が明らかに示されています。
packed 属性は構造体の要素を圧縮し、使用するスペースを最小限にします。つまり、char 変数を定義する場合、その使用するスペースは 1 バイト (8 ビット) 以下になるということです。ビット・フィールドは 1 ビットに圧縮され、それ以上のスペースは使用されません。
- このソースは、カンマ区切りリストで複数の属性を定義する単一の
__attribute__ を指定することによって最適化された記述方法になっています。
リスト 7. 構造体の圧縮と複数の属性の設定
static struct swsusp_header {
char reserved[PAGE_SIZE - 20 - sizeof(swp_entry_t)];
swp_entry_t image;
char orig_sig[10];
char sig[10];
} __attribute__((packed, aligned(PAGE_SIZE))) swsusp_header;
|
さらに詳しく調べてください
この記事では、GCC によって可能になる Linux カーネルでの手法の一端を覗いただけに過ぎません。C および C++ で使用できるすべての拡張機能についての詳細は、GNU GCC マニュアルを参照してください (「参考文献」にリンクの記載があります)。Linux カーネルで大いに利用されているこれらの拡張機能は、いずれも独自のアプリケーションで使用することもできます。GCC が今後も進化するにつれ、新しい拡張機能がさらにパフォーマンスを改善し、Linux カーネルの機能を増やしていくことは確実です。
参考文献 学ぶために
製品や技術を入手するために
- Linux SEK を注文してください。この 2 枚組 DVD セットには、Linux 対応の DB2、Lotus®、Rational®、Tivoli®、そして WebSphere® の最新 IBM トライアル・ソフトウェアが収録されています。
- developerWorks から直接ダウンロードできる IBM ソフトウェアの試用版を使用して、Linux で次の開発プロジェクトを構築してください。
議論するために
著者について  | 
|  | M. Tim Jones は組み込みソフトウェアのエンジニアであり、『Artificial Intelligence: A Systems Approach』、『GNU/Linux Application Programming』現在、第 2 版です) や『AI Application Programming』(こちらも現在、第 2 版です)、それに『BSD Sockets Programming from a Multilanguage Perspective』などの著者でもあります。技術的な経歴は静止軌道衛星用のカーネル開発から、組み込みシステム・アーキテクチャーやネットワーク・プロトコル開発まで、広範にわたっています。また、コロラド州ロングモン所在のEmulex Corp. の顧問エンジニアでもあります。 |
記事の評価
|