2016-08-31

C++標準化委員会の文書: P0360R0-P0360R9

[PDF] P0360R0:SG14: Low Latency Meeting Minutes 2016/02/17-2015/05/25

SG14 低レイテンシー会議の議事録

[死んで欲しいPDF] P0361R0: Invoking Algorithms asynchronously

Parallelism TSで提案されたアルゴリズムの並列実行を拡張する形で、アルゴリズムを非同期実行を追加する提案。

従来のシーケンシャル実行のアルゴリズムの使い方は以下で、

Container c ;

sort( begin(c), end(c) ) ;

Parallelism TSによって、以下のようにかける。

Container c ;


// シーケンシャル実行
sort( seq, begin(c), end(c) ) ;
// 並列実行
sort( par, begin(c), end(c) ) ;
// 並列ベクトル実行
sort( par_vec, begin(c), end(c) ) ;

この提案は、Parallelism TSを拡張する形で、アルゴリズムの非同期実行版を追加する。

Container c ;


// シーケンシャル非同期実行
auto f = sort( seq(task), begin(c), end(c) ) ;
f.get() ;

// 並列非同期実行
auto f = sort( par(task), begin(c), end(c) ) ;
f.get() ;

// 並列ベクトル非同期実行
auto f = sort( par_vec(task), begin(c), end(c) ) ;
f.get() ;

非同期実行版の選択は、実行ポリシーのoperator ()にtaskシングルトンを与えることで行える。非同期実行版のアルゴリズムは、通常の戻り値の型をTとした場合、future<T>を戻り値の型として返す。

戻り値に対してメンバー関数getを呼び出すと、非同期処理の完了に同期する。

なかなかいい提案だ。自然に拡張できている。

例えば以下のようなコードがあったとして

template<typenameBiIter, typenamePred>
pair<BiIter, BiIter>
gather( BiIterf, BiIterl, BiIterp, Predpred )
{
    BiIter it1 = stable_partition( f, p, not1(pred) );
    BiIter it2 = stable_partition( p, l, pred );
    return make_pair(it1,it2);
}

これを非同期実行版に書き換えると、以下のようになる。


template<typenameBiIter,typenamePred>
future<pair<BiIter,BiIter>>
gather_async(BiIterf,BiIterl,BiIterp,Predpred)
{
    future<BiIter>f1=stable_partition(par(task),f,p,not1(pred));
    future<BiIter>f2=stable_partition(par(task),p,l,pred);
    return when_all(f1,f2).then(
        [](tuple<future<BiIter>,future<BiIter>>p)
        { return make_pair(get<0>(p).get(),get<1>(p).get());}
    );
}

これはわかりにくいが、現在提案中のコルーチンを使えば、


return make_pair( co_await f1, co_await f2);

こうできる。

[PDF] P0362R0: Towards support for Heterogeneous Devices in C++ (Concurrency aspects)

SIMDやGPGPUのようなHeterogeneousなデバイスを利用するには、現在のところOpenMPやOpenCLのように、C++ではないプロプライエタリな言語を用いている。C++はこのようなデバイスをサポートする力があり、プロプライエタリな言語を使わずに標準規格で定められた言語のみを使って移植性の高いプログラミングできるのは価値が高い。

ホストCPUと、Heterogeneousなデバイスを、単一のC++のソースファイルで利用したい。OpenCLのように分離されている場合、型システムなどのエラーチェックが行えない。

この提案では、KhronosのSYCLを元に、デバイスをサポートする方法について考察している。文書中でサポートするにあたって興味深い問題がふたつある。

lambda式のABI問題。

デバイスで実行するコードを渡すのには、lambda式が最も都合がよい。ただし、lambda式のABIは規格で定められておらず、複数のC++コンパイラーを使う場合に問題になる。

lambda式は規格で名前が規定されていないので、名前マングリングが実装ごとに異なる。lambda式がキャプチャーした変数に対する、クロージャーオブジェクト内でのデータメンバーのレイアウトも規格で規定されていないので、実装ごとに異なる。

このために、コア言語で規定するか、静的リフレクションが必要だとしている。

非フラットなアドレス空間の問題

CPUはメモリに対してフラットなアドレス空間を持っている。ところが、GPUのメモリはフラットではなく、特性の異なる複数のメモリがあり、アドレス空間も複数ある。どうやってC++でサポートすればよいのか。

コア言語でアドレス空間を型システムに含める方法。アドレス空間を表現するクラスライブラリを経由してアクセスする方法。非効率的だがフラットなアドレス空間に見せかける方法などが考察されている。

[PDF] P0363R0: Towards support for Heterogeneous Devices in C++ (Language aspects)

HeterogeneousなデバイスをC++でサポートする際のlambda式のABI問題について少し深く掘り下げているが、基本的に内容はP0362R0からそれほど変わっていない。

[PDF] P0364R0: Report on Exception Handling Lite (Disappointment) from SG14

ゲーム、組み込み、金融分野が低レイテンシーについて議論するSG14から、例外処理の報告書。

例外にはオーバーヘッドがある。プログラムが実行中に例外を投げないとしても、依然としてオーバーヘッドがある。

例外のオーバーヘッドは確かに存在するのだが、現実的なソフトウェアにおけるコストが計測されたことは一度もない。このために、ソースコードが入手できるQuakeなどのエラー処理を例外を使うものに書き換えて計測する実験が行われてほしい。

ともかく、例外による予測できない処理時間の増加の可能性を避けるために、ゲームをコンパイルするときは、慣習的に例外は無効化されている。このために、例外を使わない設計のEASTLのようなものが開発されている。例外が使えないので、多くのC++ライブラリがゲーム業界では使えない。これによりC++利用者は分断され、C++の発展によろしくない。

文書は、SwiftやRustにおける例外は既存の言語の問題を認識したうえで改良がなされているとして、比較している。例外を外に投げる関数を呼び出すには、明示的な文法が必要となる。C++の例外をそのようにするのは利益が大きいが、何十年もの移行期間が必要だ。

また、組み込み業界からは、例外なき例外(exceptionless exception handling)という要望が出ている。これは、例外によるstack unwindingは行う者の、例外オブジェクトは扱わないというものだ。

例外オブジェクトが存在すると、そのオブジェクトのためのストレージを確保しなければならない。例外ハンドラーでは、オブジェクトの実行時に型に対して派生関係を比較するコードが必要だ。これは組み込みでは支払うことができないコストである。

[PDF] P0365R0: Report on SG14, a year later and future directions

SG14の設立経緯と注目している提案と将来性についての報告書。

SG14は、ゲーム、金融、組み込みの業界人がC++の低レイテンシーについて議論するために設立された。

その発端は、2014年のCPPCONで、筆者であるMichael Wongがパネルを務める議論において、カナダのゲーム会社の人間からC++をゲーム対応を改良することについて質問を受けたのがきっかけだ。

SG14は、市場初めて、ゲーム業界がその名前を名乗って業界を代表してC++に意見を表明する場所になった。というのも、C++標準化委員会は定期的に会議に参加して、会社、団体を代表して意見を表明し、提案し、議論し、コンセンサスを持ち帰って伝える役目を果たす人間が必要なのだが、ゲーム業界は厳しい納期に追われているために、そのような活動ができなかった。

SG14の目的は、制約のあるリソース、リアルタイムグラフィック、低レイテンシー、といった、ゲーム開発、金融、組み込みによくある要求の追求だ。

文書では、現在までにSG14が提案した文書を示している。

[PDF] P0366R0: Extending the Transactional Memory Technical Specification with an in_transaction Statement

トランザクションメモリーにトランザクション中かどうかを判定する機能を追加する提案。

トランザクショナルメモリーではトランザクションメモリーによる実行ができる関数とできない関数を型システムで区別している。transaction_safeな関数からtransaction-unsafeな関数を呼び出すことはできないし、transaction-unsafeな処理を行うこともできない。

問題は、ある関数が、トランザクション安全な実装をされているが、トランザクションの外で実行されたならば、もっと効率のいい実装ができる場合がある。このような実装を許可するために、トランザクションを実行中かどうかで実行時に条件分岐する機能がほしい。

この機能をどのように提供するかについて、提案は3つの案を示している。

ひとつ目の案はコア言語によるものだ。

何らかのキーワード(例えばin_transaction)を使う。

void* memcpy(void* dest, void* src, size_t n) transaction_safe {
    in_transaction {
        return memcpy_safe(dest, src, n);
    } else {
        return memcpy_asm(dest, src, n);
    }
}

in_transaction文のelseに続く文では、transaction-unsafeなコードを記述できるというものだ。

ふたつ目の案は、ライブラリによるものだ。

std::in_transaction()のようなboolを返す関数を使う。そして、falseが返った時に評価されるブランチでは、transaction-unsafeなコードを記述できる。


void* memcpy(void* dest, void* src, size_t n) transaction_safe {
    if (std::in_transaction()) {
        return memcpy_safe(dest, src, n);
    } else {
        return memcpy_asm(dest, src, n);
    }
}

3つめの案は、トランザクション実行の判定字体はふたつ目の案と同じくライブラリによるものだが、transaction-unsafeなコードを実行するには、コア言語に頼る。何らかのキーワード(例えばnot_in_transaction)で文を囲むことによって、transaction-unsafeなコードを記述できる。 


void* memcpy(void* dest, void* src, size_t n) transaction_safe {
    if (std::in_transaction()) {
        return memcpy_safe(dest, src, n);
    } else {
        not_in_transaction { return memcpy_asm(dest, src, n); }
    }
}

ふたつ目の案が最も良いように思われる。

[PDF] P0367R0: a C++ standard library class to qualify data accesses

汎用的なアクセッサーライブラリの提案。アクセッサーはラッパークラスとして提供される。

int x = 0 ;
auto ax = std::make_accessor< ... > ( x ) ;

// 元の型と同じように使える。
ax = 0 ;
int r = ax ;

このアクセッサーを通して、様々なアクセス方法の指定が行える。

例えば、1TBものdouble型の配列を初期化するとしよう。このような巨大な配列に対して先頭から最後まで値を書き込んでいく場合、わざわざそのストレージの極一部をキャッシュするのは無駄である。そこで、アクセスは一時的なものではないと指定することで、キャッシュを行わせないヒントを与えることができる。

// 1 TBもの配列
double a [2 < <37];
auto ac = make_accessor < non_temporal , write >( a ) ;
// 先頭から最後まで、42で始まるインクリメントされる整数値で初期化
std :: iota ( std :: par_vec , ac . rbegin () , ac . rend () , 42) ;

ある関数some_io()は、とても遅いI/O処理を行うので、値を返すのに時間がかかるとする。ただし、その値は、ほとんどの場合42であるとわかっているとする。

auto result = f( same_io() ) ;

その場合、f( 42 )をまず計算しておいて、some_ioが42以外の値を返した場合、関数fを計算し直すという投機的な実行が行える。そのためのヒントを出すことができる。

auto result = f ( make_accessor < likely > { some_io () , 42 }) ;

このように、様々なデータへのアクセスに対して、アクセス方法の指定を行える。

提案されているアクセス方法は極めて多岐にわたる。具体的なアクセス方法や、特定のパターンや特定のハードウェア機能に特化したアクセスの際のヒントを指定することができる。

異なるメモリ間のアクセス、read/writeアクセス、一時的ではないアクセス(キャッシュ回避)、エイリアシング、シーケンシャルアクセス、プリフェッチ、バーストモード、パイプラインアクセス、DMA、バスタイプ、アクセス幅、アドレスモード、アドレス変換、modulo addressing、アドレスビット設定(アドレスに使わないビットを指定できる)、トランザクショナルメモリー、予測。

だいぶ野望のある提案だが、ライブラリベースではなくてattributeではダメなのだろうか。

[PDF] P0369R0: 2017-07 Toronto ISO WG21 C++ Standard Meeting information

2017年7月に開催されるトロント会議の現地情報。

ドワンゴ広告

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

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

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

1 comment:

Anonymous said...

なかなか高度な内容でした。
個人的にはSG14に興味があります。自分はゲーム屋の端くれだったこともあるのでぜひ活発に議論してほしいです。