2015-06-30

C++標準化委員会の文書 2015-04-pre-Lenexaのレビュー: N4450-N4459

N4450: Variant: a typesafe union (v2)

型安全unionとして、variantライブラリの提案。

Boost.variantと似ているが、variantのネストはサポートしていない。

[PDF注意] N4451: Static reflection

静的リフレクションとして、メタプログラミングでプログラムの構造を辿れる。

論文の冒頭で、会議で上げられた懸念事項が書いてあるが、筆者も同感で、あまりにも巨大すぎる上に暴力的に強力な機能であると思う。

[PDF注意] N4452: Use cases of reflection

N4451で提案されている静的リフレクションは、あまりにも強力すぎる。初心者にそのような強力なツールを与えることは危険ではないか。という会議での懸念に対して、提案されている静的リフレクションの利用例を挙げている論文。

プログラミングにおいては、機械的な雛形コードを手で書かなければならないことがよくある。プリプロセッサーマクロやテンプレートによるメタプログラミングは、そのようなコードの記述量を抑えることに貢献しているが、限界がある。リフレクションがあれば問題を解決できる。

利用例1: ポータブルな型名

std::type_infoのnameメンバー関数の返す文字列は規格化されていない。それどころか、人間が読める文字列を返すとも、ユニークな文字列を返すとも規定されていない。文字列まで規格化された機能が必要である。さらに、nameメンバー関数はconstexpr関数ではないのでコンパイル時に使えない。また、type_infoはtypedefを考慮しないので、これまたやりにくい。

利用例2: ログ

関数の実行時にログを取るさい、関数の引数名をログ出力に含めたいことはよくある。

利用例3: シリアライゼーション

あるクラスのインスタンスをXMLやJSONやXDRのような構造化されたフォーマットで出力したい。これにはクラスごとに個別でメンバーを列挙するコードを手で書かなければならない。リフレクションがあればこのような問題は簡単に解決できる。

利用例4: Cross-cutting aspects

特定の条件を満たした関数の前後に何らかの共通処理を追加したい場合、リフレクションがあれば冗長なコードを書かずに簡単にできる。

利用例4: factory patternの実装

論文では、追加の利用例として、強力な静的リフレクションを使う利用例を挙げている。

特定のnamespace内にあるpersistent_で始まる変数名からSQLを生成。

コンパイル時にC++ソースコードを生成

デリゲートやデコレートの実装

データメンバーの配置を変える

struct foo
{
    bool b;
    char c;
    double d;
    float f;
    std::string s;
};

このようなクラスを与えると

struct rdbs_table_placeholder_foo
{
    column_placeholder<bool>::type b;
    column_placeholder<char>::type c;
    column_placeholder<double>::type d;
    column_placeholder<float>::type f;
    column_placeholder<std::string>::type s;
};

このようなコードを生成できる。

具体的にコード生成をするリフレクションを使ったコードは、論文を参照。

[PDF注意] N4453: Resumable Expressions

resumable expressionの提案。

Urbana会議以前は、コルーチンとresumable関数というやや似通った性質をもつ2つの機能が提案されていた。そして、2つの機能を統一することで合意していたのだが、スタックレスとスタックフルとはまったく別々の需要を満たすための機能であって、多少の共通項があるとはいえ、独立した機能として別々に議論すべきだということになった。これにより、スタックフルコルーチンは、純粋にライブラリ実装にして、スタックフルコルーチンをライブラリ実装できる軽いスタックレスコルーチンをコア言語に導入すべきだという方向に話が進んだ。統一の話がなくなったのであるから、awaitとかyieldなどのキーワードは不要となった。

resumable expressionsは、constexpr関数にヒントを得て設計された。

constexpr関数は、関数をconstexprであると明示的にキーワードで修飾する。そして、任意の式で呼び出すことができる。

constexpr int twice( int x ) { return x * 2 }

constexpr int a = twice( 2 ) ;
int b = twice( 4 ) ;

普通の式で使うには、関数はconstexpr関数であるかどうかを気にする必要はない。resumable expressionsもこのような設計を参考にしている。

まず、resumableキーワードを用いて、resumable関数を宣言する。

resumable void print_1_to_n( unsigned int n )
{
    for ( unsigned int i = 1 ; i <= n ; ++i )
    {
        std::cout << i << std::endl ;

        break resumable ;
    }
}

中断するところで、break resumableと記述する。

あとは、resumable式から使うだけだ。

int main()
{
    resumable auto r = print_1_to_n( 10 ) ;

    while( !r.ready() )
    {
        std::cout << "resuming ... " ;
        r.resume() ;
    }
}

resumable式は、以下の形で使う。

resumable auto r = expr ;

このときのrの型Rは実装が生成する。Rには、result_type, ready(), resume(), result()がある。

yeildやawaitなどは、resumable式を使ってライブラリで実装できる。

だいぶC++らしい小さなコア言語機能の提案だ。これは悪くなさそうだ。

N4454: SIMD Types Example: Matrix Multiplication

N4184で提案されているSIMD演算を使って行列の掛け算を実装する論文。利用例の例示として書かれた。

N4455: No Sane Compiler Would Optimize Atomics

「まともなコンパイラーはアトミックを最適化したりしない」という都市伝説は間違っていることを説いている論文。

コンパイラーはアトミックも最適化する。この論文では、最適化の例を紹介している。

コンパイラーは、プログラムをas-ifルールで最適化できる。これは、コンパイラーは、オリジナルのコードと挙動が変わらなければ、特定のアトミックを強くしたり弱くしたりできる。

最適化の例として、たとえば、アトミックの中間の値をすっ飛ばす。

void inc(std::atomic<int> *y) {
  *y += 1;
}

std::atomic<int> x;
void two() {
  inc(&x);
  inc(&x);
}

このようなコードを、最適化の結果、以下のようにしてもよい。

std::atomic<int> x;
void two() {
  x += 2;
}

他にも、x86においては、lock付きadd/subをlock付きinc/decに置き換えることも可能だ。

また、アトミックのまわりの処理も最適化の対象になる。

int x = 0;
std::atomic<int> y;
int dso() {
  x = 0;
  int z = y.load(std::memory_order_seq_cst);
  y.store(0, std::memory_order_seq_cst);
  x = 1;
  return z;
}

このコードは、以下のように最適化できる。

int x = 0;
std::atomic<int> y;
int dso() {
  // デッドstore操作の除去
  int z = y.load(std::memory_order_seq_cst);
  y.store(0, std::memory_order_seq_cst);
  x = 1;
  return z;
}

上と似ている以下のようなコードは、

int x = 0;
std::atomic<int> y;
int rlo() {
  x = 0;
  y.store(0, std::memory_order_release);
  int z = y.load(std::memory_order_acquire);
  x = 1;
  return z;
}

以下のように最適化できる(ただし、現在LLVMはこの最適化ができない)

int x = 0;
std::atomic<int> y;
int rlo() {
  // デッドstore操作の除去
  y.store(0, std::memory_order_release);
  // 冗長なloadの除去
  x = 1;
  return 0; // 保存された値がここまで到達
}

loadが除去されるのは、他のスレッドとの同期がないためだ。releaseの次にaquireがきているが、コンパイラーはstoreされた値が改変されないので、その次のloadは冗長だと判断する。

なんだかこの最適化は極めて怖い。

以下のようなコードは変換されない。

int x = 0;
std::atomic<int> y;
int no() {
  x = 0;
  y.store(0, std::memory_order_release);
  while (!y.load(std::memory_order_acquire));
  x = 1;
  return z;
}

論文は最後に、それぞれの立場に対して意見を述べている。

標準化委員会へ:これらの最適化が起こらないと仮定するな。むしろ推奨しろ。よりハードウェアに近い最適化ができる既存の方法を標準化しろ。同期と並列実行をより簡単にできて、失敗しにくいライブラリを提供しろ。

開発者へ:アセンブリを捨てろ。そんなに最適化できないし、そもそもコードを書いている時点で存在するアーキテクチャにしか対応できない。コンパイラーのパフォーマンスが期待通りでないのならばバグ報告を投げろ。標準化委員会に同期と並列実行を実現できる方法を提案しろ。ThreadSanitizerのようなツールをt受かってコード中の競合を発見しろ。

ハードウェアベンダーへ:ハードウェアの能力を示せ。

コンパイラー開発者へ:さっさと仕事にもどれ。まだ最適化できることと・・・ぶち壊れるコードは山ほどある。利用者がいいコードを書けるようにしろ。コンパイラーはアトミックの正しくない使い方を検出したならば、メッセージを吐くべきだ。

N4456: Towards improved support for games, graphics, real-time, low latency, embedded systems

N2771のEASTLの論文を考察している。現在のSTLに足りないもの、やや古い論文なので、現代ではもう意味がなくなったものなどを列挙している。

N4457: C++ Standard Core Language Active Issues
N4458: C++ Standard Core Language Defect Reports and Accepted Issues
N4459: C++ Standard Core Language Closed Issues

コア言語に対する問題集。

ドワンゴ広告

この記事はドワンゴ勤務中に書かれた。

ドワンゴは本物のC++プログラマーを募集しています。

採用情報|株式会社ドワンゴ

CC BY-ND 4.0: Creative Commons — Attribution-NoDerivatives 4.0 International — CC BY-ND 4.0

3 comments:

Anonymous said...

コルーチンは関数オブジェクトになったんですね。悪くない選択に見えます。
自分には実装できそうにはありませんが・・・。

Anonymous said...

N4450やN4454などは注意の要らないPDFですか?

Anonymous said...

> variantのネストはサポートしていない。
現論文には書かかれていないと思います。