none
C++11やVisual C++ 2010(主にラムダ式やvolatileなど)について RRS feed

  • 質問

  • どうも、お世話になります。

    現状大きく分けて2つの疑問があるのですが

    まず(こっちの疑問に関してはC++11サイドの話である可能性が高いです)

    以下のコードを試してみました。


    #include <stdio.h>
    
    #define constexpr const
     
    template < class F > 
    void ForReturnable( const F& f, const int len ){
    	for ( int i=0; i<len; ++i ) if ( f( i ) ) return; 
    }
    
    int main(void){
    
    	double d = 2.2;
    	constexpr auto lam = [d]( int i ){ 
    		return ( i <= d ) ? printf("%d ",i), false: true; 
    	};
    	constexpr int LEN_ = 10;
    
    	ForReturnable( lam, LEN_ );
    
    	printf("\r\n\r\n"); 
    	d = 8;
    	ForReturnable( lam, LEN_ );
    
    	getchar();
    	return 0;
    
    }

    実行結果は

    0 1 2

    0 1 2

    となり ちょっと意図したかった結果と違います。

    そこで


    constexpr auto lam = [d]( int i ){

    constexpr auto lam = [&d]( int i ){


    に書き変えてみると

    0 1 2

    0 1 2 3 4 5 6 7 8

    となりました。

    また、このように constexpr auto lam = ~

    としたlamを使いまわさず


    ForReturnableの呼び出し時に、2回とも全く同じコードでその場でラムダ式による関数オブジェクトを作ると、同じように

    0 1 2

    0 1 2 3 4 5 6 7 8

    となりました。

    ということは、ラムダ式をこのように、auto 名前 = ラムダ式;

    として使いまわした場合は、その変数の宣言地点でのキャプチャリストが、コピーなら値が

    埋め込まれるように、(参照ならその参照が埋め込まれるように)そのまま適用されるというのが正しい解釈、ということでしょうか?

    (つまり、「参照でのキャプチャ」というのはコピーの手間を削減するという意味合いだけではなかった、という意味で正しいでしょうか?)


    また、こちらはVC++2010サイドの話だと思うのですが

    (サインインのパスワード失念してたので、一度教えてgooで質問させていただきましたが、しばらく待って回答がいただけなかったのと、パスワードを思い出したのでこちらのVC++フォーラムで再度質問させてください。(マルチポストを避けるため、元の質問ページは削除済みです。))

    volatileはマルチスレッドやマルチプロセス
    絡みの、非同期関連で使う可能性が高く
    あるいはそれも排他制御を綿密にやってれば使わなくてもOKなんでは?

    と思ってたんですが

    FUURAさんとこ http://homepage1.nifty.com/~takaot/index.html

    C# vs C++(1)

    を、ラムダ式とテンプレート使って書き変えた以下のコードで

    #include <stdio.h>
    #include <time.h>
    
    typedef const char* const StrLiteral;
    typedef size_t szt;
    #define constexpr const
    
    template < class T, class F >
    T Sample( const F& f, const szt len){
    	
    	T t = 0;
    	for (szt i=0; i<len; ++i){
    		for (szt j=0; j<len; ++j){
    			t = f( i, j, t );
    		}
    	}
    	return t;
    
    }
    
    template < class T, class F >
    void CheckTime( const F& f, StrLiteral s ){
    	
    	constexpr szt LEN_ = 20000;
    	
    	const clock_t be = clock();
    	const T&& t = Sample<T>(f,LEN_); //ココが問題
    	const clock_t af = clock();
    	
    	printf("time(ms): %d\n", (af-be)*1000/CLOCKS_PER_SEC );
    	printf(s, t);
    
    }
    
    int main(void){
    	
    	constexpr auto lam2 = [](szt i, szt j, double d){
    		return (i*3.5 + 123.1)/65.7 + d/1000.9 + j;
    	};
    	
    	CheckTime<szt>( [](szt i, szt j, szt a){ return (i*3 + 123)/65 + a/1000 + j; }, "%u\n\n" );
    	CheckTime<double>( lam2, "%f" );
    	getchar();
    	return 0;
    
    }

    少なくともこちらの環境だと(おそらく最高レベルの最適化にしていると)

    CheckTime関数での
    const T&& t = Sample<T>(f,LEN_);

    の部分で

    const T t = Sample<T>(f,LEN_);

    にしてしまうと

    time(ms): 0

    と表示されてから
    計算されるように最適化されてしまうようなのです。

    const T&& や const T& の場合は問題はないのですが

    参照ではない場合

    const volatile T t = Sample<T>(f,LEN_);

    のように volatile を書けばいいのですが
    volatile書かないと上記のようにtime(ms): 0になってしまいます。

    シングルスレッドでも際限なくvolatile書かないといけないというのは「流石にちょっとあり得ない」と思うのですが

    volatileを書く基準って言うのは
    どういう風に考えれば良いんでしょうか?

    あるいはこれに関しては単純に、バグの類に近いと考えていいでしょうか?

    • 編集済み mr.setup 2012年3月12日 15:51
    2012年3月10日 11:35

回答

  • 個人的意見です。実際の規格の内容についてはほかの識者の方にお任せします。

    ということは、ラムダ式をこのように、auto 名前 = ラムダ式;

    として使いまわした場合は、その変数の宣言地点でのキャプチャリストが、コピーなら値が

    埋め込まれるように、(参照ならその参照が埋め込まれるように)そのまま適用されるというのが正しい解釈、ということでしょうか?

    (つまり、「参照でのキャプチャ」というのはコピーの手間を削減するという意味合いだけではなかった、という意味で正しいでしょうか?)

    実際の規格を確認してはいませんが、ラムダ式の宣言時に匿名の関数オブジェクトのクラス、インスタンスが生成されて、キャプチャした変数はそのメンバ変数として用いられるとか言う話だったと思います。

    #どこで見たかは失念。

    つまりラムダ式を宣言した時点で、キャプチャ変数を引数とした関数オブジェクトが宣言されるので、参照でキャプチャしていない限り、宣言時点の値が使われるのだったと思います。

    volatileを書く基準って言うのはどういう風に考えれば良いんでしょうか?

    あとこちらは、そもそも「volatile」というのはマルチスレッドとか関係なく、最適化抑止の為のキーワードだったと思います。

    つまり、代入や参照をされた時に即値の展開を行わずに実際にメモリへの代入、メモリからの読み込みを強制される為のキーワードだったかと。

    実際にコードを動かしたりはしていませんが、「time(ms): 0」になってしまうということから最適化が行われてラムダ式、Sample関数の展開及び計算結果まで

    コンパイル時に行われてしまって即値の代入処理になってしまってるのではないでしょうか。

    volatileや参照で宣言することによって実際の代入処理が行われなけばならなくなり、関数の展開処理などが抑止されたものと思われます。

    #ラムダ式、Sample関数ともに引数のみに依存する純粋関数ですし、引数もconst、constexprで定数となっているのですべて展開されてしまっても問題はないと判断されても無理はないと思われます。ラムダ式で変数を参照でキャプチャするなどすればまた結果が変わると思います。

    個人的見解としてはそもそもこれはバグなんでしょうか?実際に計算結果として「t」の値自体が異なってしまってるわけではないですよね?

    上記2つともアセンブラが読めるようならば、コンパイル時にアセンブラコードの出力をさせてみて、どのようになっているのか確認するのもありではないでしょうか?

    • 回答としてマーク mr.setup 2012年3月10日 20:17
    2012年3月10日 17:14
  • まず最初に返信しにくい質問になってると思いますので、もう少し要点に絞った短めの文章にすることをお勧めします。

    あと表題は質問内容が判り易いようにすることもですね。

    質問についてですが、

    最初の質問の lambda の動作ですが、変数へ戻り値返すのだからポインタを渡すのが当然では?

    lambda 動作テストの最後のあたりはどうなったか理解できないので「参照でのキャプチャ」というのはちょっと判りません。

    2番目の volatile の使い方ですが、これは謎人さんも答えてくれているとおり「最適化抑止」用のキーワードですね。

    C# や java と同じ様に考えてるようですが、根本的に別物と思った方がよいでしょう。

    「最適化されると困るコード」を「最適化抑止」するために使います。

    主にマルチスレッド処理でそういう状況がありますがマルチスレッドに限られてはいません。

    識者という訳でもないので内容の真偽は保証できかねますが・・・・。

    • 回答としてマーク mr.setup 2012年3月10日 20:17
    • 回答としてマークされていない mr.setup 2012年3月10日 20:18
    • 回答としてマーク mr.setup 2012年3月10日 20:19
    2012年3月10日 18:02
  • キャプチャリストですか・・・

    自分はMSDNの英語リファレンスの独自訳で機能をおっかけてるので、こういった言葉はしりませんでしたね・・・。

    追記したのですが、どちらにせよ実行される位置スコープで参照不可変数へのアクセスが保証される気はしませんけど・・・。

    他言語のラムダ式ではスコープで参照可能な変数は自動的にキャプチャ(っていうのですかね?)されるのでC言語的に実装するなら値渡しされるはずと思いこんでまして

    英語の注釈の訳からも、特別に値を返す場合に指定するのだろうと思ってたんですが違うようですね・・・。


    どういう部分をそう思われたのですか?

    clock() 呼出の間に保存している変数にアクセスしてないですから、呼出し結果の値を保証する必然性がない典型に見えました。

    間の処理が関数呼出しのみで位置も近いので簡単に最適化対象になるように思えます。


    といった所です。当然ながらヘタレ英語力での独自解釈を元とした意見ですので内容の保証は・・・


    追記:リンクサイトでキャプチャを確認しましたが現状の理解と大差なく、元コードはそのように動くように思いますよ?

    • 編集済み kyano30 2012年3月11日 3:30 追記
    • 回答としてマーク mr.setup 2012年3月11日 5:03
    2012年3月11日 3:10
  • 本筋とは関係ないと思いますが、volatileを使用しないといけない場合について知っている範囲で述べます。

    メモリマップドIOというハードウェア構成の組み込みシステムをC言語で開発する場合にvolatileキーワードは

    必須です。メモリマップドIOでは、周辺コントローラのレジスタがメモリアドレス空間に配置されるため

    それらのレジスタにCPUからアクセするC言語コードはメモリ(ROM/RAM)にアクセスするコードと同じになります。

    そのため、C言語コンパイラにアクセスする領域がROM/RAM領域かメモリマップドIOの領域かを知らせておく

    ためにvolatileを使います。

    例えば、物理アドレス0xFFFF0000にメモリマップドIOだったとしますと、

    volatile char* pReg = (char*)0xFFFF0000;

    *pReg = 0x80;

    *pReg = 0x00;

    このようにC言語で書いた場合、通常のメモリならCPUは2番目の*pReg = 0x00;だけ実行すれば意味的には

    変わりませんが、メモリマップドIOの領域であれば、CPUは2回のWRITE命令を実行する必要があります。

    コンパイラは最適化指示がされてもvolatile属性をもつ領域へのアクセスは最適化をしないので、C言語で

    書いた通りに2回のWRITE命令に置き換えます。

    また、以下のような関数では、最適化されると何もしない関数に置き換えられることがあります。

    void Clear()

    {

     char s = *pReg;

    }

    このば場合でもvolatile指定があれば、コンパイラは1回のREAD命令に置き換えてくれます。

    つまり、周辺コントローラ(デバイス)は常にCPUのアドレスバス、データバス、READ/WIRTE信号を監視していて

    自分へのアクセス要求に応答するので、何回どの順番でREAD/WRITE命令が実行されたかを確実に知らなければ

    なりません。さらにいえば、メモリマップドIO領域の物理アドレスは論理アドレスと一致し、キャッシュも無効になっている

    必要があります。

    もう組み込みシステムの開発から離れて何年もたつので、間違いがあればどなたでも構わないので指摘してください。

    • 回答としてマーク mr.setup 2012年3月22日 9:08
    2012年3月18日 3:11
  • 本題とは離れますが

    shinano 様の記述に全面同意を返信します。

    C言語の volatile が実装された経緯は、最適化しては困る記述を仕様化することでコンパイラ毎の環境依存を抑えるためだと思っています。

    組込系では基本的な内容で普通に利用されてると思っていたのですが、どうも Win アプリ系でみるとマルチスレッドの影響か解釈にズレを感じてます・・。

    割込み処理での volatile の利用方法が、マルチスレッド時の利用方法と同じなので現状のようになったのでしょうけど・・・・。

    本筋的に volatile はマルチスレッド処理のために実装されたものではないと信じてます。

    • 回答としてマーク mr.setup 2012年3月22日 9:09
    2012年3月18日 3:49
  • 環境に依存しないようなコードを書きたいのですよね?

    「うまく伝わらない」と判断した理由は、今回のケースこそ設計者の意図を伝えるために volatile を使うべきケースという個人的見解からです。

    ということで、やはりここは「問題が発生した事が判明したらそこで対処すればいい」ということに割り切った方が良い気がしてきましたw (最適化なしはさすがにC++を使ってる意味が薄くなるので)

    現実的には環境移行をする前提でコード記述したことはないので、ほぼそのようにしているのですが、個人的に使ってる指標は最適化したくない箇所です。

    実は無駄だったとしても、元々最適化されないのを明示することになるだけなので特に副作用もありませんし、コードの意図が判り易くなると思っています。

    判りにくいですかね?


    後、

    これがもし原文を引用したものであった場合は、calling a library I/O functionについても少なくともどう考えても「規格的には」side effectsでしょう。

    例文がどういった内容での言及なのかわかりませんが、外部ライブラリの振る舞いはコンパイラが知り得ない情報ですから、コンパイラが外部ライブラリの呼出しを最適化しないのは当然だと思いますよ。

    • 回答としてマーク mr.setup 2012年3月25日 13:58
    2012年3月25日 11:40

すべての返信

  • 個人的意見です。実際の規格の内容についてはほかの識者の方にお任せします。

    ということは、ラムダ式をこのように、auto 名前 = ラムダ式;

    として使いまわした場合は、その変数の宣言地点でのキャプチャリストが、コピーなら値が

    埋め込まれるように、(参照ならその参照が埋め込まれるように)そのまま適用されるというのが正しい解釈、ということでしょうか?

    (つまり、「参照でのキャプチャ」というのはコピーの手間を削減するという意味合いだけではなかった、という意味で正しいでしょうか?)

    実際の規格を確認してはいませんが、ラムダ式の宣言時に匿名の関数オブジェクトのクラス、インスタンスが生成されて、キャプチャした変数はそのメンバ変数として用いられるとか言う話だったと思います。

    #どこで見たかは失念。

    つまりラムダ式を宣言した時点で、キャプチャ変数を引数とした関数オブジェクトが宣言されるので、参照でキャプチャしていない限り、宣言時点の値が使われるのだったと思います。

    volatileを書く基準って言うのはどういう風に考えれば良いんでしょうか?

    あとこちらは、そもそも「volatile」というのはマルチスレッドとか関係なく、最適化抑止の為のキーワードだったと思います。

    つまり、代入や参照をされた時に即値の展開を行わずに実際にメモリへの代入、メモリからの読み込みを強制される為のキーワードだったかと。

    実際にコードを動かしたりはしていませんが、「time(ms): 0」になってしまうということから最適化が行われてラムダ式、Sample関数の展開及び計算結果まで

    コンパイル時に行われてしまって即値の代入処理になってしまってるのではないでしょうか。

    volatileや参照で宣言することによって実際の代入処理が行われなけばならなくなり、関数の展開処理などが抑止されたものと思われます。

    #ラムダ式、Sample関数ともに引数のみに依存する純粋関数ですし、引数もconst、constexprで定数となっているのですべて展開されてしまっても問題はないと判断されても無理はないと思われます。ラムダ式で変数を参照でキャプチャするなどすればまた結果が変わると思います。

    個人的見解としてはそもそもこれはバグなんでしょうか?実際に計算結果として「t」の値自体が異なってしまってるわけではないですよね?

    上記2つともアセンブラが読めるようならば、コンパイル時にアセンブラコードの出力をさせてみて、どのようになっているのか確認するのもありではないでしょうか?

    • 回答としてマーク mr.setup 2012年3月10日 20:17
    2012年3月10日 17:14
  • まず最初に返信しにくい質問になってると思いますので、もう少し要点に絞った短めの文章にすることをお勧めします。

    あと表題は質問内容が判り易いようにすることもですね。

    質問についてですが、

    最初の質問の lambda の動作ですが、変数へ戻り値返すのだからポインタを渡すのが当然では?

    lambda 動作テストの最後のあたりはどうなったか理解できないので「参照でのキャプチャ」というのはちょっと判りません。

    2番目の volatile の使い方ですが、これは謎人さんも答えてくれているとおり「最適化抑止」用のキーワードですね。

    C# や java と同じ様に考えてるようですが、根本的に別物と思った方がよいでしょう。

    「最適化されると困るコード」を「最適化抑止」するために使います。

    主にマルチスレッド処理でそういう状況がありますがマルチスレッドに限られてはいません。

    識者という訳でもないので内容の真偽は保証できかねますが・・・・。

    • 回答としてマーク mr.setup 2012年3月10日 20:17
    • 回答としてマークされていない mr.setup 2012年3月10日 20:18
    • 回答としてマーク mr.setup 2012年3月10日 20:19
    2012年3月10日 18:02
  • 謎人さん

    ご回答ありがとうございます♪


    >ラムダ式を宣言した時点で、キャプチャ変数を引数とした関数オブジェクトが宣言されるので、参照でキャプチャしていない限り、宣言時点の値が使われるのだったと思います。


    なるほど、やはりおそらくはそういうこと、だったんですね。


    >あとこちらは、そもそも「volatile」というのはマルチスレッドとか関係なく、最適化抑止の為のキーワードだったと思います。

    >つまり、代入や参照をされた時に即値の展開を行わずに実際にメモリへの代入、メモリからの読み込みを強制される為のキーワードだったかと。


    はい、そう言う基本的なとこに関してはその通りです。

    しかしclock();とclock();の間の

    const T t = Sample<T>(f,LEN_); をvolatileにしないといけないのは

    さすがに思いっきり環境依存な気がしてなりません。

    このように最適化されて順番変わるのがいつもいつも普通ってことになると、バージョン変わるたびに全てのコードをくまなく調べ直しということになりませんかね?


    >コンパイル時に行われてしまって即値の代入処理になってしまってるのではないでしょうか。


    いえ、そいつは違うと断言できます。

    何しろ

    CheckTime<szt>は1140ms

    CheckTime<double>は2250ms

    程度安定してかかるのですが、非参照かつ非volatileの場合だけ

    「体感それと同じ程度の時間が、time(ms): 0の表示後に必ずかかってから計算結果が表示されます。」


    >すべて展開されてしまっても問題はないと判断されても

    展開されたところで

    clock();ふたつとSample<T>の順番が変わるのは望ましくないんではないでしょうか?


    >実際に計算結果として「t」の値自体が異なってしまってるわけではないですよね?

    はい20943と21087.336782


    >上記2つともアセンブラが読めるようならば、コンパイル時にアセンブラコードの出力をさせてみて、どのようになっているのか確認するのもありではないでしょうか?


    それはもうこの場合あんま見る必要ないんではないでしょうか?w

    前者については規格の話だとすれば、あとはVC++が規格通りかどうかというだけですし

    後者については明らかなんですもん。

    といいつつ、一応出力させて関係個所を抜粋すると


    参照にするか、volatile変数の時の形


      0000a    8b 35 00 00 00
        00         mov     esi, DWORD PTR __imp__clock
      00010    57         push     edi
      00011    ff d6         call     esi
      00013    8b f8         mov     edi, eax
      00015    e8 00 00 00 00     call     ??$Sample@長い名前~
      0001a    dd 5c 24 38     fstp     QWORD PTR _$S2$[esp+64]

    ; 29   :     const clock_t af = clock();

      0001e    ff d6         call     esi



    非参照非volatileの時の形

      0000a    8b 35 00 00 00
        00         mov     esi, DWORD PTR __imp__clock
      00010    57         push     edi
      00011    ff d6         call     esi
      00013    8b f8         mov     edi, eax
      00015    ff d6         call     esi

    その後色々あって・・・

      00040    e8 00 00 00 00     call     ??$Sample@長い名前~


    まぁ、そのまんまですよね。


    時間計測など「順序が密接に絡み、もともとそういう点での最適化はしなくていい、というかしてほしくない」変数には、実際には積極的にvolatileつけといた方がいいとかいう判断になるでしょうか?(う~ん・・・そこまでアセンブリレベルで意識してないといけないのだろうか)

    (クリティカルセクションの排他位置とかはさすがに変わんないとは思いますが)

    • 編集済み mr.setup 2012年3月10日 21:15
    2012年3月10日 20:06
  • kyano30さんもご回答ありがとうございます♪


    >まず最初に返信しにくい質問になってると思いますので、もう少し要点に絞った短めの文章にすることをお勧めします。


    あー、そうでしたw

    大事な一文を書くのを忘れていました。(要点を絞らないことにも大変重要な意味があるのです)

    ・その他これらの解釈やコードに関して、突っ込みどころがありましたら教えてください。

    C++11にはそれほどまだ詳しくないですし、Visual C++2010の準拠状況はさらにまだまだちゃんと把握していませんので。


    >あと表題は質問内容が判り易いようにすることもですね。


    色々似た部分の派生質問が出てくる可能性があるので、ある程度抽象的にした方が良いと判断しました。

    ただ、ひとまずはラムダ式やvolatileなどの話なので、その用に書き足しておくことにいたします。

    >最初の質問の lambda の動作ですが、変数へ戻り値返すのだからポインタを渡すのが当然では?

    う~ん、意味がよく分かりません。

    前者の質問の内、非参照でのキャプチャでは

    void ForReturnable( const double d ){
    	for ( int i=0; i<10; ++i ){
    		if ( d < i ) return; 
    		printf("%d ",i);
    	}
    }
    
    int main(void){
    
    	ForReturnable( 2.2 );
    
    	printf("\r\n\r\n"); 
    	ForReturnable( 2.2 );
    
    	getchar();
    	return 0;
    
    }

    と等価になり参照でのキャプチャなら

    void ForReturnable( const double& d ){
    	for ( int i=0; i<10; ++i ){
    		if ( d < i ) return; 
    		printf("%d ",i);
    	}
    }
    
    int main(void){
    
    	double d = 2.2;
    	ForReturnable( d );
    
    	printf("\r\n\r\n"); 
    	d = 8;
    	ForReturnable( d );
    
    	getchar();
    	return 0;
    
    }

    と等価になる、という話だと思います。この場合ポインタは関係ないのでは?

    >2番目の volatile の使い方ですが~

    はい、もともと私はC++屋なのでそういう「基本的な概念的なこと」に関しては大丈夫です。

    今までVC++2008で、「最強の最適化」にしても困ったケースに遭遇しなかったので(致命的な副作用の変更など)特に使っていなかったのですが、ということです。

    • 編集済み mr.setup 2012年3月10日 20:41
    2012年3月10日 20:17
  • 色々似た部分の派生質問が出てくる可能性があるので、ある程度抽象的にした方が良いと判断しました。

    どうやら質問というより ディスカッション 希望なのですかね?

    投稿時に選択できたはずだけどディスカッションになってるのかなここ?


    う~ん、意味がよく分かりません。

    lambda 記述の[] 内の要素は定義処理内で戻り値を返す変数を記述します。

    よって main 関数のローカルスコープの d 変数のポインタを渡す必要があるはずです。

    という事がいいたかったのですが伝わらなかったですかね?

    ForReturnable の引数には const int len には 10 を渡してますが d の値ではないですよね。

    再度例示された等価コードになる記述になってないと思います。

    ※追記:そもそも実行時にスコープ外となる変数アクセスってのも・・・動くとは思うけど

    今までVC++2008で、「最強の最適化」にしても困ったケースに遭遇しなかったので...

    それならば今回が初めてのケースでしょうが、そういうものだと思うべきでしょう。

    十分に最適化の対象になるように見受けられます。

    というか典型的なコードに見えますけど・・・・・。


    前回同様、内容については保証できませんが・・・・・・・


    • 編集済み kyano30 2012年3月11日 2:46 書き忘れ:
    2012年3月11日 2:16
  • ありがとうございます♪

    >どうやら質問というより ディスカッション 希望なのですかね?

    微妙なとこです。

    少なくとも後者に関しては、ぜひとも解決しておきたい質問です。


    >lambda 記述の[] 内の要素は定義処理内で戻り値を返す変数を記述します。

    ちゃいますちゃいます。

    少なくともC++11では[]内で引数とは別にキャプチャリストをとれます。

    で、戻り値は引数リストの後に->型名で指定します。

     int a=1;

    [a](int i) -> int { return i+a; };

    この場合

    ローカル変数aをキャプチャしint型の引数を一つ取り、int型を返すoperator()をもつ関数オブジェクトと同じ挙動とゆーことになります。

    ただし、戻り値については型推論が効く場合表記上省略できます。

    参照でのキャプチャ([&d]などと書いた)とかについては本の虫の

    lambda 完全解説

    とかが参考になるかと思います。


    なので

    >d 変数のポインタを渡す必要があるはずです。

    ポインタではなく参照ではありますが、既にわたっています。


    >再度例示された等価コードになる記述になってないと思います。

    思うのはkyano30さんの自由ですが、私はそれは事実とは異なるかと思います。


    >十分に最適化の対象になるように見受けられます。

    >というか典型的なコードに見えますけど・・・・・。

    どういう部分をそう思われたのですか?(そう思う部分があるならそれが重要なので、それを知りたいのですが)

    • 編集済み mr.setup 2012年3月11日 5:20 脱字修正など
    2012年3月11日 2:44
  • キャプチャリストですか・・・

    自分はMSDNの英語リファレンスの独自訳で機能をおっかけてるので、こういった言葉はしりませんでしたね・・・。

    追記したのですが、どちらにせよ実行される位置スコープで参照不可変数へのアクセスが保証される気はしませんけど・・・。

    他言語のラムダ式ではスコープで参照可能な変数は自動的にキャプチャ(っていうのですかね?)されるのでC言語的に実装するなら値渡しされるはずと思いこんでまして

    英語の注釈の訳からも、特別に値を返す場合に指定するのだろうと思ってたんですが違うようですね・・・。


    どういう部分をそう思われたのですか?

    clock() 呼出の間に保存している変数にアクセスしてないですから、呼出し結果の値を保証する必然性がない典型に見えました。

    間の処理が関数呼出しのみで位置も近いので簡単に最適化対象になるように思えます。


    といった所です。当然ながらヘタレ英語力での独自解釈を元とした意見ですので内容の保証は・・・


    追記:リンクサイトでキャプチャを確認しましたが現状の理解と大差なく、元コードはそのように動くように思いますよ?

    • 編集済み kyano30 2012年3月11日 3:30 追記
    • 回答としてマーク mr.setup 2012年3月11日 5:03
    2012年3月11日 3:10
  • おお、ありがとうございます♪

    clock() 呼出の間に保存している変数にアクセスしてないですから、呼出し結果の値を保証する必然性がない典型に見えました。

    間の処理が関数呼出しのみで位置も近いので簡単に最適化対象になるように思えます。

    こういうのですよ、こういうの

    これはだいぶ真相に近づけそうな感じがします。

    つまり、逆に言うと

    VC++2010には全く問題がなくて

    しかもvolatileを使う必然性もなくて

    それはつまり「結果の値を保証する必然性」を持たせてやれば、volatileを書かなくても大丈夫そうだということ、と考えられますね。

    (volatileは環境に強く依存すると思うので出来ればこれをメインにはあまり使いたくはないです。よほどのことがない限り、おそらく、規格上副作用を考慮して結果を保存しなくちゃいけない、という状況にしてやるほうが良いでしょう。その上で尚且つ念のため、という意味合いで使うなら、個人的にも全然OKです。)


    現実のアプリでは、そもそもC++でグローバルな変数や関数はほとんどないか、必要最小限に抑えるべきですが

    実験なので試しにお手軽に

    template <class T> struct DATA { T t; };

    というものを用意してやり、動的確保で


    template <class T> struct DATA { T t; };
    
    template < class T, class F >
    void CheckTime( DATA<T>* data もふくむ ){	
    	
    	constexpr szt LEN_ = 20000;
    
    	const clock_t be = clock();
    	data->t = Sample<T>(f,LEN_);  
    	const clock_t a = clock();
    	
    	printfで時間表示
    	printf(s, data->t);
    
    }
    
    int main(void){
    	
    	auto* data1 = new DATA<szt>;
    	auto* data2 = new DATA<double>;
    
    	CheckTime<szt>( ラムダ式, data1, "%u\n\n" );
    	CheckTime<double>( ラムダ式, data2, "%f" );
    
    //以下解放など略


    こんな感じにしてみると

    やはりうまく行きました。

    (推奨はできないものですが、意味的に)DATA<double>などがグローバル変数でもうまく行きました。

    月並みですが、↓こんなんだけでもtime(ms) : 0を阻止できました。(これは若干環境依存な気がしますが)

    const clock_t af = clock();	
    printf("time(ms): %d\n", (af-be)*1000/CLOCKS_PER_SEC );
    
    //↓
    
    const clock_t af = clock()+(clock_t)t;	
    printf("time(ms): %d\n", (af-(clock_t)t-be)*1000/CLOCKS_PER_SEC );


    ただし、こういうことやらず、またDATA<T>のポインタや参照渡してやっても

    そのインスタンスがローカル変数だと、スタック上での話になるので(副作用の恐れが遥かに少ない)

    やはり最適化対象となりました

    いや、というより

    注意すべきはほとんどの場合に置いて、もっぱら「ローカル変数のみ」となるというわけですよね?

    その上、関数の順序が入れ替わってしまうと困る場合で、しかもそれぞれの関数(および使用データ)は直交してる。

    しかも、今回のように、volatileをつけるか迷う対象が、const T& や const T&& などのように参照としての変数だと、最適化で順番入れ替えされないケースがある。(?)

    (ついでに、もしかしたら実装が見えずインライン化されなければ、ローカル変数でも案外アレかも)

    ここまで条件がそろわないといけないとなると、どーりで今までそういう現象はなかなか確認しなかったわけですね。

    参照としての変数だと最適化で順番入れ替えされない事が確実なのかどうか・・・っていうのは気になりますね。どうなんだろう?

    大事なことを忘れてたので試してみましたが

    ラムダ式をC++03での関数オブジェクトに書き換えて

    VC++2008で、同じ設定でやってみたところ

    全く同じ結果となりました。

    (つまり「呼出し結果の値を保証する必然性がない」コードである場合は順番入れ替えが起こりました)

    つまり、これの全貌が分かりきらないと恐ろしいので、というのはVC++2010に移行するのをためらう理由にはならない、ということですね。

    >実行される位置スコープで参照不可変数へのアクセスが保証される気はしませんけど・・・。

    >スコープで参照可能な変数は自動的にキャプチャ(っていうのですかね?)されるのでC言語的に実装するなら値渡しされるはずと思いこんでまして

    キャプチャリストは、言い換えれば関数オブジェクトのメンバ、という事だと私は認識しています。

    なので参照不可能変数ではなく、また、値渡しと参照渡しの両方を

    用意しているところは、さすがは無駄なオーバーヘッドを避けたがるC++らしいなぁと思ってますw

    なのでC++11のラムダ式をC++03的に書くなら

    double d = 2.2;
    constexpr auto lam = [d]( int i ){ 
    	return ( i <= d ) ? printf("%d ",i), false: true; 
    };
    ForReturnable( lam, 10 );

    double d = 2.2;
    
    struct FUNCTOR2 {
    	const double d;
    	FUNCTOR2( const double d_ ) : d(d_){}
    	bool operator()( int i ) const {
    		return ( i <= d ) ? printf("%d ",i), false: true; 
    	}
    } f(d);
    
    ForReturnable( f, 10 );


    と等価と考えて良く

    double d = 2.2;
    constexpr auto lam = [&d]( int i ){ 
    	return ( i <= d ) ? printf("%d ",i), false: true; 
    };

    double d = 2.2;
    
    struct FUNCTOR2 {
    	const double& d;
    	FUNCTOR2( const double& d_ ) : d(d_){}
    	bool operator()( int i ) const {
    		return ( i <= d ) ? printf("%d ",i), false: true; 
    	}
    } f(d);

    と等価だと思います。

    (万が一「間違ってる!」って場合はご存知の方正解を教えてください)




    • 編集済み mr.setup 2012年3月11日 6:29
    2012年3月11日 5:03
  • 内容が長すぎて読む気が起こりませんので返信しないつもりでしたが・・・

    (万が一「間違ってる!」って場合はご存知の方正解を教えてください)


    とのことなので一つだけ意見しますと・・・

    volatileは環境に強く依存すると思うので出来ればあまり使いたくはないです。


    volatile は C言語で規定されているキーワードのはずなので環境に強く依存するってのはおかしいですね。

    意味的にも「最適化しない」指定なので依存させないためのキーワードでしょう?

    実際、環境毎に変化したような経験もありませんし・・・・。


    周知されてる機能かと言われるとかなり疑問ですけどね・・・・・

    2012年3月11日 6:46
  • C++03でも、揮発性左辺値を持つオブジェクトの操作については副作用であり

    副作用完了点では、その副作用完了点以前の操作すべての副作用が完了するまで、それ以降の操作の副作用が生じてはならない。

    となっていますね。

    ただ、コンパイラと言語の規格というのはまた別に考えないといけないのも事実で、改めて調べて

    >周知されてる機能かと言われるとかなり疑問ですけどね・・・・・

    これが致命傷となるかもしれないとは思いました。

    真偽のほどは全く分かりませんし、だいぶ昔のことですが

    >http://ml.tietew.jp/cppll/cppll/thread_articles/11727

    >IC メーカーのコンパイラ・サポートの人間でも知らない方がいました。

    「規格」と「コンパイラ」という話については

    少なくともコンパイラが時間をかけて規格に追い付くということは今までも良くあった話だと思います。

    ただ、VC++等をはじめとする代表的なコンパイラでは

    流石にvolatileの原義は通ってる可能性が非常に高いとも思うので

    >強く依存すると思う

    というのは少々オーバーでした。

    実際には、これらを踏まえた必要箇所には積極的につけて良いかもしれません。



    追記:

    時間置いてたので上で自分が書いた本当の意図を忘れていましたが

    それでもどうもなんかおかしいなと思い、冷静に考え直したら思い出したので、付け足しておきます。

    「volatileを使う場合」に強く環境依存と言いたかったのではなくて

    「volatileに頼らないといけないコードばかり書こうとした場合は、そもそもvolatileを使わなきゃならない箇所」というのは強く環境に依存する

    ので

    「そもそもvolatileを使わなくても大丈夫なコード」になるならそういうコードを優先した方が良い、という意図でした。

    • 編集済み mr.setup 2012年3月11日 23:42 主な意図を忘れてたことに気付いたの追記
    2012年3月11日 13:48
  • 本筋とは関係ないと思いますが、volatileを使用しないといけない場合について知っている範囲で述べます。

    メモリマップドIOというハードウェア構成の組み込みシステムをC言語で開発する場合にvolatileキーワードは

    必須です。メモリマップドIOでは、周辺コントローラのレジスタがメモリアドレス空間に配置されるため

    それらのレジスタにCPUからアクセするC言語コードはメモリ(ROM/RAM)にアクセスするコードと同じになります。

    そのため、C言語コンパイラにアクセスする領域がROM/RAM領域かメモリマップドIOの領域かを知らせておく

    ためにvolatileを使います。

    例えば、物理アドレス0xFFFF0000にメモリマップドIOだったとしますと、

    volatile char* pReg = (char*)0xFFFF0000;

    *pReg = 0x80;

    *pReg = 0x00;

    このようにC言語で書いた場合、通常のメモリならCPUは2番目の*pReg = 0x00;だけ実行すれば意味的には

    変わりませんが、メモリマップドIOの領域であれば、CPUは2回のWRITE命令を実行する必要があります。

    コンパイラは最適化指示がされてもvolatile属性をもつ領域へのアクセスは最適化をしないので、C言語で

    書いた通りに2回のWRITE命令に置き換えます。

    また、以下のような関数では、最適化されると何もしない関数に置き換えられることがあります。

    void Clear()

    {

     char s = *pReg;

    }

    このば場合でもvolatile指定があれば、コンパイラは1回のREAD命令に置き換えてくれます。

    つまり、周辺コントローラ(デバイス)は常にCPUのアドレスバス、データバス、READ/WIRTE信号を監視していて

    自分へのアクセス要求に応答するので、何回どの順番でREAD/WRITE命令が実行されたかを確実に知らなければ

    なりません。さらにいえば、メモリマップドIO領域の物理アドレスは論理アドレスと一致し、キャッシュも無効になっている

    必要があります。

    もう組み込みシステムの開発から離れて何年もたつので、間違いがあればどなたでも構わないので指摘してください。

    • 回答としてマーク mr.setup 2012年3月22日 9:08
    2012年3月18日 3:11
  • 本題とは離れますが

    shinano 様の記述に全面同意を返信します。

    C言語の volatile が実装された経緯は、最適化しては困る記述を仕様化することでコンパイラ毎の環境依存を抑えるためだと思っています。

    組込系では基本的な内容で普通に利用されてると思っていたのですが、どうも Win アプリ系でみるとマルチスレッドの影響か解釈にズレを感じてます・・。

    割込み処理での volatile の利用方法が、マルチスレッド時の利用方法と同じなので現状のようになったのでしょうけど・・・・。

    本筋的に volatile はマルチスレッド処理のために実装されたものではないと信じてます。

    • 回答としてマーク mr.setup 2012年3月22日 9:09
    2012年3月18日 3:49
  • shinanoさん、kyano30さんご回答ありがとうございます。

    調べてみましたが

    volatileは元は組み込みとかでのメモリマップドIOのために作られた(実際にはそうではなかったとしても、少なくともそちらの方が適切な用途であり、マルチスレッドは考慮されていなく、本来その目的ではメモリバリアとはならない)と考えてよさそうですね。

    その理由はデバイスからの監視とかへの対処であり、その場合はシングルスレッド・シングルプロセスかどうかにかかわらず必要不可欠、というわけなのでしょうが


    ただ、今回のケースで「それではシングルスレッド・シングルプロセスで、メモリマップドIOも絡まないであろう」質問文のような時、最適化による順番入れ替えを逃れられる、最もシンプルかつ確実な方法はどんな感じなんでしょう?



    調べまくってみたのですが、これだっていうものが見つからず

    ・少なくともVC++において、ヒープ上の物は最適化による順番入れ替えは免れると考えられる

    ・少なくともVC++において、const左辺値参照、または右辺値参照で関数の戻り値を束縛した場合は、順番入れ替えは免れると考えられる

    ・VC++かどうかに限らず、関数の戻り値をvolatileで束縛すれば、その変数の操作に関しては、その他の副作用完了点との順番入れ替えは免れると考えられる

    ・少なくともVC++において、インライン化が起きた場合は順番入れ替えを免れる可能性が高いと考えられる

    ・規格上、標準ライブラリのIOは副作用(のはず)である

    これらよりもっと具体的で「こうするといい」という指針の詳細についてはお手上げ状態です。


    (ちなみに「メモリマップド」まで同じ名前ですが、「メモリマップドファイル」に関しては別段注意は必要ないでしょうか?)

    • 編集済み mr.setup 2012年3月23日 1:53
    2012年3月22日 9:21
  • どうにもうまく伝わらないようなので、もう少しだけ volatile について返信しますが個人的見解です。


    コンパイラは通常、最適化を行います。これは当然の事ですがコードを作り変えるのと同じ事になります。

    コンパイラは与えられたC言語ソースの情報のみからコードを作り変えるので、情報外の影響があれば設計者の意図と違った動作になる可能性がでてきます。


    この情報外の影響というのが、

    スコープ外での値変更 =  割込み、メモリマップドIO、外部スレッド等による参照&変更

    必ず実行する前提の処理 = 無駄ループ等によるウェイト処理、時間計測処理、メモリマップドIO等

    といった事が代表例で、shinano さんがメモリマップドIOでの事例を詳しく解説してくれてますね。


    コード外の設計者の意図(=最適化を抑止する)をコンパイラに伝えるための記述が volatile です。


    C言語のコンパイラはシンプルなのが売りなので言語機能外の情報を類推することはありえないはずで、各種ライブラリの動作などを踏まえることは本来ありえません。

    標準ライブラリの CRT だろうと API だろうと・・・・。


    コンパイラの説明記述の古い記憶なので信頼性はないですが、仕様の言及では

    「最適化しないことが可能」

    「なるべくコードの順序で処理する」

    といった程度しか触れてなかったことからの個人的見解です。

    この文章に「コンパイラはソース情報のみに基づいて実行モジュールを生成する。それは設計者の願望を含むことはない」と書かれてて印象に残ってます・・・。

    蛇足になりますが、たしかに、「volatile を使う場合」という「これだっていう」指標はみかけませんね・・・。

    そのあたりが知られてなかったり、おまじない扱いされる理由なのでしょうか?

    最適化がどう作用するかは環境依存で指標は存在しないので、指標として出しづらいってのがあるのですかね・・・。

    その辺りが判らないようなら「最適化しない」を選びなさい、という風に仕様に迫られてる気がします。

    2012年3月25日 6:36
  • >C言語のコンパイラはシンプルなのが売りなので言語機能外の情報を類推することはありえないはずで、各種ライブラリの動作などを踏まえることは本来ありえません。


    C言語については詳しく知りませんが、今回はC++についての話です。

    C++の規格のPDFがなぜか今再度確認しようとしたら見れないので、適当に検索していろんなサイトに載ってた文を引用しますが↓

    Accessing an object designated by a volatile lvalue (3.10), modifying
    an object, calling a library I/O
    function, or calling a function that does any of those operations are
    all side effects, which are changes in the
    state of the execution environment.

    これがもし原文を引用したものであった場合は、calling a library I/O functionについても少なくともどう考えても「規格的には」side effectsでしょう。

    副作用であるなら

    副作用完了点では、その副作用完了点以前の操作すべての副作用が完了するまで、それ以降の操作の副作用が生じてはならない。

    ですから


    それ以外のことについては、書くまでもないと思ったので特に書いてなかっただけです。なので

    >どうにもうまく伝わらないようなので

    と、上記レスだけから断定するのは早計だと思いますが

    >最適化がどう作用するかは環境依存で指標は存在しないので、指標として出しづらいってのがあるのですかね・・・。

    この見解に関しては全く同感です。

    ありがとうございます。

    ということで、やはりここは「問題が発生した事が判明したらそこで対処すればいい」ということに割り切った方が良い気がしてきましたw (最適化なしはさすがにC++を使ってる意味が薄くなるので)

    規格は「理想論」で、C++ぐらい複雑な言語で、特定のコンパイラが「完全に準拠している」というのを証明することが果たして可能なのかどうかという事もありますし。

    • 編集済み mr.setup 2012年3月25日 8:17
    2012年3月25日 8:09
  • 環境に依存しないようなコードを書きたいのですよね?

    「うまく伝わらない」と判断した理由は、今回のケースこそ設計者の意図を伝えるために volatile を使うべきケースという個人的見解からです。

    ということで、やはりここは「問題が発生した事が判明したらそこで対処すればいい」ということに割り切った方が良い気がしてきましたw (最適化なしはさすがにC++を使ってる意味が薄くなるので)

    現実的には環境移行をする前提でコード記述したことはないので、ほぼそのようにしているのですが、個人的に使ってる指標は最適化したくない箇所です。

    実は無駄だったとしても、元々最適化されないのを明示することになるだけなので特に副作用もありませんし、コードの意図が判り易くなると思っています。

    判りにくいですかね?


    後、

    これがもし原文を引用したものであった場合は、calling a library I/O functionについても少なくともどう考えても「規格的には」side effectsでしょう。

    例文がどういった内容での言及なのかわかりませんが、外部ライブラリの振る舞いはコンパイラが知り得ない情報ですから、コンパイラが外部ライブラリの呼出しを最適化しないのは当然だと思いますよ。

    • 回答としてマーク mr.setup 2012年3月25日 13:58
    2012年3月25日 11:40
  • ありがとうございます。

    環境に依存しないようなコードを書きたいのですよね?

    「うまく伝わらない」と判断した理由は、今回のケースこそ設計者の意図を伝えるために volatile を使うべきケースという個人的見解からです。

    なるべく環境に依存せず、また意図通りの範囲の最大限の最適化がされやすいコードを書きたい

    ですね。

    今回のコードをそのままアプリケーションで使うわけではなく、またほんのわずかな書き方の差で結果が変わったので、「今回のケース」と「今回のケースとは言えない場合」の、はっきりした線引が簡単にできるのであればそれを知りたいのですが


    だいたい今までの情報で良い感じでしょうか?


    現実的には環境移行をする前提でコード記述したことはないので、ほぼそのようにしているのですが、個人的に使ってる指標は最適化したくない箇所です。

    実は無駄だったとしても、元々最適化されないのを明示することになるだけなので特に副作用もありませんし、コードの意図が判り易くなると思っています。

    判りにくいですかね?


    はい

    もし「だいたい」ではなく

    「最適化したくない箇所」が正確にわかるとしたら、それは大人数でない場合の、コンパイラ製作者に限った話だと思います。

    「最適化したくない箇所」というものは、もしそれを必要十分にピンポイントでvolatile指定するためには、そもそも「どういう場合にどういう最適化」がされる可能性があるのか、かなり正確に分かってないといけません。

    そうでなければ必要以上に最適化を妨げてしまうか、最適化によって意図しない動作になってしまう可能性があります。

    というより、今まで組み込み系でないコードを色々と見てきましたが、volatile指定がちょくちょくされてるプログラムをほとんど見たことがなかったので、今いち「ちょくちょくvolatile入れながら」という実感がつかめてない、という感じですね。

    「自分が今書いているこのコードは最適化の影響でどう変形し得るか」を常に考えながら書く、なんてことはちょっと現実的でないように思いますので、逆に

    とりあえずはVC++で確認できる「最適化なしとありの時で挙動が変わる」具体例が沢山あるといいのですが

    • 編集済み mr.setup 2012年3月25日 13:59
    2012年3月25日 13:58
  •  気になっていたので、その後調べたことを報告します。

     以前のアセンブリコードを提示していた箇所を見たのですが全部載っていなかったので
     VC++(Microsoft Visual Studio 2010 Version 10.0.40219.1 SP1Rel)を使い
     Releaseビルドしたところ以下のようになりました。(CheckTimeの部分のみ)
     CheckTimeはmain()のなかで以下の2箇所の呼び出し
             CheckTime<szt>( [](szt i, szt j, szt a){ return (i*3 + 123)/65 + a/1000 + j; }, "%u\n\n" );
             CheckTime<double>( lam2, "%f" );

     があるので、CheckTime<double>とCheckTime<double>の2つに展開されていますが、
     CheckTime<double>の部分だけ抜粋します。


     ; 25   : void CheckTime( const F& f, StrLiteral s ){

             push    ebp
             mov     ebp, esp
             and     esp, -64                                ; ffffffc0H
             sub     esp, 56                                 ; 00000038H
             push    esi

     ; 26   :       
     ; 27   :        constexpr szt LEN_ = 20000;
     ; 28   :
     ; 29   :         const clock_t be = clock();

             mov     esi, DWORD PTR __imp__clock
             push    edi
             call    esi                             ←clockの呼び出し、結果はeax
             mov     edi, eax                        ←結果をediへ保存      

     ; 30   :         const T t = Sample<T>(f,LEN_); //ココが問題
     ; 31   :         const clock_t af = clock();

             call    esi                             ←clockの呼び出し、結果はeax

     ; 32   :       
     ; 33   :        printf("time(ms): %d\n", (af-be)*1000/CLOCKS_PER_SEC );

             sub     eax, edi                        ←af-beの結果をeaxへ保存
             imul    eax, 1000                               ; 000003e8H
             mov     esi, DWORD PTR __imp__printf
             mov     ecx, eax
             mov     eax, 274877907                          ; 10624dd3H
             imul    ecx
             sar     edx, 6
             mov     eax, edx
             shr     eax, 31                                 ; 0000001fH
             add     eax, edx
             push    eax
             push    OFFSET ??_C@_0O@MDOCNBC@time?$CIms?$CJ?3?5?$CFd?6?$AA@
             call    esi                             ←printfの呼び出し
             call    ??$Sample@NV<lambda0>@?以下略   ←Sample<T>(f,LEN_)の呼び出し

     ; 34   :        printf(s, t);

             fstp    QWORD PTR [esp]
             push    OFFSET ??_C@_02NJPGOMH@?$CFf?$AA@
             call    esi                             ←printf(s, t)の呼び出し
             add     esp, 12                                 ; 0000000cH

     ; 35   :
     ; 36   : }

             pop     edi
             pop     esi
             mov     esp, ebp
             pop     ebp
             ret     0

     これを見ると、コンパイラは以下のように順序を変えています。

             const clock_t be = clock();
             const clock_t af = clock();
             printf("time(ms): %d\n", (af-be)*1000/CLOCKS_PER_SEC );

             const T t = Sample<T>(f,LEN_); //ココが問題
             printf(s, t);


     私の推測では、tはprintf(s, t);で初めて使われるので、このように順序を変えても
     プログラムの意味は変わらないと判断しているのだと思います。

     

     蛇足ですが、VC++のvolatile説明は以下のとおりです。
     http://msdn.microsoft.com/ja-jp/library/12a04hfd.aspx

     The volatile keyword is a type qualifier used to declare that
     an object can be modified in the program by something such as
     the operating system, the hardware, or a concurrently executing thread.

     以下は私の理解です。
     コンパイラは(コンパイル単位の)プログラムの文脈に沿って意味を変えないように
     最適化するので、その文脈の外で変更されるオブジェクトは考慮できない(しない)。
     CもC++も逐次処理なのでソースコードに書かれた順番が重要ですが、意味を変えないと
     判断すれば順序を変える。
     その文脈の外(ハードウェアや別スレッド)で変更されるオブジェクトがある場合は
     volatileでプログラマが明示的にコンパイラに指示する。

     


     

    2012年4月4日 3:19
  • ありがとうございます。

    上記やりとりから察していただけると思いますが、現在の私の見解と99%同じです。


    >意味を変えないと 判断すれば順序を変える。

    これが、実際に組む時に「じゃ具体的には、どういう基準で、どこに volatileをつければ確実なのか・・・?」ということが問題ですね。コードでないと禅問答になってしまいますが、つまり「文脈の内」とはどこまでのことを指すのかが問題、とも言えます。



    ただし

    >別スレッド

    に関しては、volatile修飾子を付けることでスレッドセーフでなかった変数がスレッドセーフになる状況は、かなり限定されるように思います。

    その操作(アクセス)が全ての個所でアトミックなものであるならば、確かになんら問題はないのですが、例えば


    ↓のように

    volatile宣言するだけではダメなんですか?

    >32ビット変数に対して8ビットMPUがアクセスをするときも通常アトミックになりません。


    「アトミックな操作」かどうかについては、volatile修飾は考慮されていない(というか出来ない)はずなので、そういう意味での事なら環境依存ではないでしょうか?

    したがってshinanoさんご提示の

     http://msdn.microsoft.com/ja-jp/library/12a04hfd.aspx

    while (Sentinel)

    Sleep(0); // volatile spin lock

    に関しては

    Visual Studio 2010 - Visual C++

    が対象とするプロセッサとコンパイラの事情から

    volatile bool Sentinel = true;

    のSentinelからの読み取りがアトミックだからOK

    あるいは、必ずしもそうでなかったとしても「このコードでは問題はない」

    というところが、話の上で「省略されている」、ということではないでしょうか。


    なので、環境依存のコードをガリガリ書く必要に迫られる可能性が高くなる組み込みとは事情が変わり

    移植をある程度視野に入れた大規模アプリケーションにおいて、スレッドセーフを考えようとするなら、例えその時点で問題がなかったとしても、機能変更の必要が発生したり環境が変わった場合の管理の楽さなどを考慮して

    よほどの状況以外はアトミックな操作となることを保証する(ことになっている)関数や排他制御用のオブジェクトを使う方が、注意力を余分に要求されない分メンテナンスは楽なんではないかな、と。

    (どっち道いくつも似た事を書く必要があったり移植を考慮するなら、ラッパーは欲しいですが)

    • 編集済み mr.setup 2012年4月4日 8:37
    2012年4月4日 8:19
  • さらに色々思考実験をしてみましたが、やはり決め手が欲しいので

    「今回の事例のような状況における、最もシンプルな」対処法を、言語仕様を振り返りつつシンプルにまとめてみると


    ・順番入れ替えが起こった処理同士が直交しているかどうか(あるいは関係性の度合いなどについて)は、色々試した結果、書き方と処理系に依存する可能性が高そうに思ったので、考慮外としておきます。


    ・それ以外に絞れば、試しにSample内のループカウンタを一つstaticにするとかだけでも順番入れ替えを阻止できた。やはりスタック外のメモリアクセスは副作用として扱ってくれるっぽい。

    つまりSample関数について、戻り値を受ける変数含む、処理内容全般にわたって全てがスタック上の話、であった事が非常に重要。 


    ここで、まともなコンパイラであれば

    他の処理との関係を考えなくても、「あくまでそれが『はっきりとした副作用』になり得るものならば、そこについてはvolatileを使う必要性はない」となるはずです。


    つまり、今回のような事例への対処という意味においては、volatileはコンパイル時定数に置き換られるようなconstexpr的な意味を持ち得る関数の戻り値や、実際に定数であるものを

    ローカル変数で受ける場合においてのみ、使用可能性がある、と考えて良いように思います。

    (ついでに言うと、そういうたぐいの物は今回のように「実行時に測定することは、リリース版ではありえない」と言えるでしょうし)

    しかも、「それで尚且つ順番が狂うとまずい場合」は、実際にはものすごく限られると思われます。


    なにしろ、逆説的には

    スタック上でなければならないと考えられる

    わけですから、「意図しなかった順番入れ替え」を「再現するため」には

    スタック外のメモリ上にオブジェクトを置くことはできないばかりか、一つも「今までの、スタック以外からの情報に触れることが出来ない」。

    となります。


    その上で、「順番が狂うとまずい場合」でなければならない、ということになります。

    ところが、スタック上だけで片がつかなければいけないため、よほど変なハックをしない限り(そしてそういう行動は普通のアプリケーションではまず必要ない)

    「順番が狂うとまずい」くせに「コンパイル時定数に置き換られるようなものでなければいけない」わけです。


    この考え方でいいのならば、対処法は

    ・ほとんどの場面で、そもそも必要ない。

    ・上記すべてに当てはまる、本当にごく稀な状況においてのみ、volatileをつければ良い。


    ということになると思われます。

    が、未確認なだけかもしれないので、上記予想を破るような再現コードをご存知の方がいらっしゃいましたら教えていただけるとありがたいです。

    • 編集済み mr.setup 2012年4月5日 4:52 一応意味的に脱字ともなり得ると思ったので「スタック外の」を追加補正
    2012年4月4日 18:22