インテル コンパイラーで試す次世代C++規格「C++0x」

 インテル コンパイラーではOpenMP 3.0やC++ラムダ関数、並列コンパイルといった、新規格や最近注目されている機能がいち早く取り入れられている。本記事では、インテル コンパイラーが採用した新規格について解説する。

 インテル コンパイラーの特徴の1つに、新しい技術や標準規格への素早いサポートが挙げられる。たとえば最新のインテル コンパイラー 11.1では、プログラムを簡単に並列化できる新たなキーワードが追加されているほか、11.0からの機能として現在策定中のC++の新規格「C++0x」や、新たな並列化基盤「OpenMP 3.0」のサポートが追加されている。本記事ではこれらの機能について、簡単ではあるがその概要と使用例を紹介しよう。

C++ 0xのサポート

 C++は1983年代に開発されて以来、しばらくは公式な標準化規格が存在せず、各コンパイラメーカーにより独自に拡張が加えられていった。そして1998年、ANSIとISOによってやっと「ISO/IEC 14882:1998」として標準化規格が制定された(C++98とも呼ばれる)。そして2003年にはこの訂正版「ISO/IEC 14882:2003」(C++03)がリリースされた。現在のほとんどのC++コンパイラはこの規格C++98およびC++03のほとんどの機能をサポートしている。

 一方、最近では関数型プログラミングやジェネリックプログラミングといった新しいプログラミングパラダイムが広く知られるようになり、これらに対応したC#やObjective-Cといったプログラミング言語が登場するようになった。そして、C++もこれらの影響を受け、C++98/03を拡張する新たな標準化言語仕様が策定されている。これはISO/IECのC++標準化委員会(JTC1/SC22/WG21)によって行われており、2000年代中には策定が完了する見込みだったことから「C++0x」と呼ばれている(ただし、現状では2009年中の標準化規格の発行は難しい状況で、正式な発行は2010年以降になる見込みである。これについてC++の開発者であるBjarne Stroustrup氏は、「0xの『x』は16進数で考えてほしい」と述べている)。

 C++0xのワーキングドラフトや議論については、C++ Standards Committee Papersページから参照できる。現在の最新のワーキングドラフトは、2009年6月22日に公開された「N2914」という文書である。また、問題点リストなどの文章もここで入手できる。それぞれかなりのボリュームになっており、読破するのは容易ではないが、興味のある方はぜひご一読をおすすめしたい。

 さて、インテル コンパイラー 11.0以降では、このC++0xの一部が先行して採用されている。これらの機能はデフォルトでは利用できないものの、「/Qstd=c++0x」(Windows版)もしくは「-std=c++0x」(LinuxおよびMac OS X版)というコンパイルオプションを指定することで利用が可能だ。また、Windows版でVisual Studio向けの統合機能を利用している場合では、プロジェクトの設定画面からGUIでもC++0xの利用のOn/Offを設定できる(図1~3)。

図1 デフォルトの状態では、後述する「auto」やラムダ関数を利用するコードはコンパイルエラーとなる
図1 デフォルトの状態では、後述する「auto」やラムダ関数を利用するコードはコンパイルエラーとなる
図2 Visual Studioプラグインを利用することで、Visual Studioのプロジェクト設定画面内でC++0xの使用の有無を指定できる
図2 Visual Studioプラグインを利用することで、Visual Studioのプロジェクト設定画面内でC++0xの使用の有無を指定できる
図3 「Enable C++0x Support」を「Yes」にすることで、C++0xの機能を用いたコードもコンパイル可能となる
図3 「Enable C++0x Support」を「Yes」にすることで、C++0xの機能を用いたコードもコンパイル可能となる

インテル コンパイラー 11.1がサポートするC++0xの新機能

 インテル コンパイラーが先行サポートしている機能についてはインテル コンパイラーのヘルプに簡単に記載されているが、これらを機能/目的別に分類すると次の表1のようになる。

表1 インテル コンパイラー 11.1がサポートするC++0xの新機能
「C99」仕様のサポート
関数型マクロを引数なしで呼び出せるようになった
可変個数の引数を持つマクロ
「long long」型
enum型の宣言で最終要素に「,」を付けることが許可された
文字列定数とワイド文字列定数の結合
関数名を文字列として返す「__func__」マクロ
テンプレート機能の改善/拡張
テンプレートパラメータを「friend」として宣言できるようになった
テンプレートクラスを引数に持つテンプレート宣言内で「>>」が「operator>>」と解釈される問題の解消
「typename」の使用ルール緩和
「template」キーワードの使用ルール緩和
テンプレートのインスタンス化を遅延させる「extern template」構文
ジェネリックプログラミング機能の強化
「auto」型の利用
オブジェクトの型を返す「decltype」演算子
ラムダ関数(無名関数)の定義
より明瞭かつ高パフォーマンスなコードを記述する仕組みの追加
右辺値クラスへのバインド
コンパイル時に静的チェックを行う「static_assert」機能
直接参照における呼び出し可能でないコピーコンストラクタの許可

 C++0xでは、1999年にISO/IEC 9899:1999として発行されたCの標準仕様「C99」にすでに含まれていた機能が取り入れられているほか、テンプレート機能がより使いやすくなり、また現行のC++標準規格で規定されていた各種制限が緩和されている。下記では、これらのうち注目すべき機能について簡単に紹介しておこう。

コンパイル時に推論によって自動的に型が決定される「auto」型

 C++0xでは「auto」型という、コンパイル時に型推論によって決定される型を利用できるようになった(リスト1)。

リスト1 「auto」型の利用例

    int x = 10;
    int y = 20;
    auto z = x + y;  // コンパイル時に型が決定され、zはint型変数となる

 たとえばリスト1の場合、auto型として宣言されている変数zに対してコンパイル時に型推論が行われ、その結果zはint型の変数として割り当てられるようになる。

インスタンスの型を返す「decltype」演算子

 C++0xで新たに導入された演算子「decltype」は、引数で指定したインスタンスの型を返す働きをするものだ。C/C++に従来からあったsizeof演算子と同様、コンパイル時に評価が行われ、引数で指定した変数の型を表す文字列に置き換えられる

 たとえばリスト2は、decltypeを利用して変数の型を指定する例だ。

リスト2 decltype演算子の利用例

    double p;
    decltype(p) q;  // コンパイル時に型が決定され、qはpと同じdouble型変数となる

 この例では、qはpと同じdouble型の変数として割り当てられる。

「[]」を使用したラムダ関数(無名関数)の定義

 ラムダ関数(無名関数)を定義する仕組みも新たに用意された。C++0xでは、次のように「[]」を使用して無名関数を定義できる。

[](<引数>) -> <戻り値の型> { 関数の中身 };

 無名関数はauto型の変数に格納でき、またその変数を介して呼び出すことができる(リスト3)。

リスト3 ラムダ関数の利用

    // 引数としてint型の値2つを取り、int型の値を返す無名関数を定義
    auto hoge1 = [](int x, int y) -> int { return x + y; };

    // 引数としてint型の値2つを取り、int型の値を返す無名関数を定義(戻り値の型を省略)
    auto hoge2 = [](int x, int y) { int z = x * y; return z + x; };

    int x = hoge1(2, 1);
    int y = hoge2(2, 1);
    printf("x%d, %d.\n", x, y );  //実行結果は「3, 4」となる

 たとえばSTLなどのテンプレートライブラリでは、関数オブジェクトを引数として取る関数があるが、それに対して無名関数を関数オブジェクトの代わりに利用することが可能だ。これによって、より簡潔に処理を記述できるようになる。

コンパイル時にエラーチェックを実行する「static_assert」

 「static_assert」という、コンパイル時に動的なエラーチェックを行う機構も追加された。static_assertの構文は次の通りだ。

static_assert(<評価する式>, <エラーメッセージ>)

 static_assertは、「評価する式」をコンパイル時に評価し、その値が偽の場合に「エラーメッセージ」で指定した文字列を表示してコンパイルエラーを発生させるものだ。評価する式では「==」や「!=」、「<」、「>」といった比較演算子が利用できる。また、「エラーメッセージ」には通常文字列型でメッセージを指定する。

 たとえば次のリスト4は、コンパイル時に「CHAR256」型のサイズを確認するものだ。

リスト4 static_assertの使用例

typedef struct {
    char sz[256];
} CHAR256;

static_assert(sizeof(CHAR256) == 256, "invalid size - CHAR256.");

 この例では、CHAR256のサイズが256でない場合、コンパイル時に「”invalid size – CHAR256.”」というメッセージが表示され、コンパイルエラーとなる。

「タスク」という概念が追加されたOpenMP 3.0のサポート

 インテル コンパイラ 11.1では、並列プログラミングAPIであるOpenMPの最新版であるOpenMP 3.0に対応している。OpenMPはさまざまなプラットフォームで利用できる並列プログラミングの技術基盤であり、対応コンパイラおよびライブラリを組み合わせることで、比較的手軽にプログラムを並列化できる。

 以下ではOpen MP 3.0で追加された特徴などについて解説するが、OpenMPの概要やインテル コンパイラーでの利用法・利用例については「ソフトウェア高速化の鍵は「並列化」:いま注目される並列化技術を知る」や「並列アプリケーションを作ってみよう」といった以前の記事でも紹介しているので、そちらも参照して欲しい。

 最新のOpenMP 3.0は2008年5月にリリースされており、2005年リリースのOpenMP 2.5から下記のような変更が加えられている。

  • 「タスク」の概念が追加され、これにより新たな並列化パターン「task」が利用できるようになった
  • 「omp_set_schedule」および「omp_get_schedule」、「omp_get_level」、「omp_get_ancestor_thread_num」、「omp_get_team_size」といった新たなランタイムライブラリ関数の追加
  • 内部変数「thread-limit-var」および「max-active-levels-var」、「stack-size-var」、「wait-policy-var」の追加。これらおよび関連する環境変数を利用することで、OpenMPプログラムが使用する最大スレッド数やスレッドのスタックサイズ、待ち状態のスレッドの振る舞いなどを制御できる
  • スレッドの実行/割り当てルールが若干変更された

 この中で、OpenMPの新機能として特に注目されているのが「タスク」の概念である。OpenMP 3.0以前の場合、並列化の実装には「Folk-Joinモデル」というモデルが想定されていた。これは、プログラム中の特定の個所で複数のスレッドを生成して処理を実行させ(Folk)、それぞれのスレッドに割り当てられた処理が全て完了したら単一のスレッドで続きを実行する(Join)、というモデルである(図2)。

図2 OpenMPのFolk-Joinモデル
図2 OpenMPのFolk-Joinモデル

 しかし、OpenMP 2.5以前のFolk-JoinモデルではFolk時にしかスレッドが生成されず、処理途中で新たにスレッドを追加できない、という制限があった(Folkで生成されたスレッド中で再度Folk-Joinを行う、といった、ネストされた構造は可能)。そのため、たとえばforループで一定回数同じ処理を繰り返す、といった処理については簡単に実装できるものの、whileループのように、実行してみないとループが何回実行されるか分からない繰り返し処理については、そのままではOpenMPによる並列化が行えないという不都合があった。この問題を解決するべく考案されたのが、「タスク」の概念である。

 OpenMP 3.0では、処理すべきコードブロックを「タスク」という単位で管理するようになった。タスクは「omp parallel for」演算子などで生成されるほか、「omp task」演算子を利用して動的にも生成できる。たとえば次のリスト5は、whileループを「omp task」演算子を利用して並列する例である。このコードは、リンクドリストに対する繰り返し処理を行うものだ。

リスト5 「omp task」演算子を利用した並列コードの例

struct linked_list {
    struct linked_buffer* next;
    int data1;
      :
      :
};
  :
struct linked_list* ptr_next;
  :
ptr_next = ptr_start;
#pragma omp parallel
{
    #pragma omp single nowait
    {
        while(ptr_next) {
            #pragma omp task firstprivate(ptr_next)
            {
                foobar2(ptr_next);
            }
        ptr_next = ptr_next->next ;
    }
}

 このように「omp task」演算子を利用することで、リンクドリストなど「omp for」演算子では並列化できなかったデータに対する処理を、容易に並列化することが可能になる。

 なお、インテル コンパイラー11.1では、これらのOpenMP 3.0サポートに加え、新たに並列化ソースコードの解析やデバッグに便利な機能が追加されている。まず1つは、「Parallel Debugger Extension」という機能で、これはプログラム中で並列化された部分を、ソースコードの再コンパイルを行うことなしに非並列にステップ実行するものだ。これにより、並列化されたプログラムでも容易に実行されるコードを追跡できるようになる。

 またParallel Lint機能という、OpenMPによる並列化コードを分析し、メモリリークやデッドロックなどの問題を診断する機能も追加されている。

 並列化されたプログラムで問題が発生した場合、手作業によるデバッグでは問題の発生している個所を見つけにくいことも多い。このような並列プログラム向けの機能は、問題発生時に非常に有用だ。

インテル コンパイラー 11.1で加えられた、並列化のための新しいC++キーワード

 インテル コンパイラー 11.1では、並列処理を容易に実行するためのC/C++キーワードが新たに追加されている。これらはプログラム中で並列に実行できる処理をコンパイラに指示したり、それらの結果を集計するために利用できるものだ。

 追加されたキーワードは表2の4つとなる。まず「__par」は並列実行できるforループを指定するもので、OpenMPの「omp parallel for」演算子とほぼ同様の働きをするものだ。

表2 新たに導入されたキーワードとOpenMPの対応
キーワード 同等のOpenMPディテクティブ
__par #pragma omp parallel for
__taskcomplete #pragma omp single
__task #pragma omp task
__critical #pragma omp critical

 また、「__taskcomplete」および「__task」はこのキーワードを付けた処理を別スレッドで実行するもので、OpenMPの「omp task」と似た働きをする。__taskcompleteと__taskはリスト6のように、必ずセットで利用される。

リスト6 「__taskcomplete」および「__task」キーワードによる処理の並列化

__taskcomplete
{
    __task f_sum1(500, a, b, c);
    __task f_sum2(500, a+500, b+500, c+500, d);

    // 上記の関数「f_sum1」と「f_sum2」は、複数のスレッドで同時に実行され、
    // 両方の関数の実行が完了したら__taskcomplete{}に続く処理が実行される
}

 最後の__criticalはクリティカルセクション(複数のスレッドが同じ処理を実行する際、複数のスレッド間で同時に行ってはいけない処理)を指定するキーワードである。これにより、従来はMutexなどを利用して実装する必要があった処理を簡潔に記述できるようになる(リスト7)。この例では、「__par」キーワードを使ってforループ内を並列に実行するよう指定しており、複数のスレッドで同時に「num_founds」変数の値を更新しないよう、クリティカルセクションを利用している。

リスト7 クリティカルセクションの利用例

void count_keyword(char* buffer, char* keyword, int* num_founds) {
    if( strcmp(word, keyword) == 0) {
        __critical *num_founds++;
    }
}
  :
  :
int i;
int num_founds;
char** buffer;
char* keyword;
  :
  :
num_founds = 0;
__par for( i = 0; i < 128; i++ ) {
    count_keyword(buffer[i], keyword, &num_founds);
}

インテル コンパイラーで最新の機能を試そう

 以上で述べたように、インテル コンパイラーではC++0xやOpenMP 3.0といった、最新の言語規格をいち早く利用できる。これらを利用することで、いままで面倒な記述が必要だったコードを簡潔にしたり、またコードの視認性を高めることが可能だ。これによって予想外の動作やバグを見つけやすく、また問題の対処もしやすくなると思われる。これらの機能はインテル コンパイラーの体験版でも利用できるので、ぜひ実際に試してみてほしい。