none
C++/CLIのビルドで生成されるコードはWin32ネイティブとMSILのチャンポンになるのか? RRS feed

  • 質問

  • 外池と申します。

     

    他のスレッドにおいて、C++/CLIにおけるCLRのクラスの定義と利用の方法について大いに議論されているところですが、以下の点について私の中で整理がつかなくなってきたので、ご存知の方にご教示いただきたく。

     

    1)さらに別のスレッドですが、「このままでは、MFCとCLRが両方混在した形になっています」というような指摘を受けるコーディングが示されていたことがありました。

     

    あるいは、

     

    2) CLRの配列であるarray <T>^ と、C++の標準的な配列と、同じコーディングの中に混在させて使用することがでるようです。

     

    まず、2)についての「混在可能」の理解は、正しいでしょうか?

     

    正しいとして、

     

    MFCの利用やC++の標準的な配列の利用には、プラットフォームに依存したポインタの操作とメモリ管理の手法が必要であり、そのためにはネイティブコードが必要だと思われます。一方で、CLRに準拠したオブジェクトを利用するにはMSILが必要と思われます。

     

    ということは、ビルドで生成されたコードは、Win32ネイティブとMSILのチャンポンになるのでしょうか?

     

    2008年2月15日 16:19

回答

すべての返信

  • こちらの記事が参考になるのではないでしょうか

     

    <参考>

    http://www.atmarkit.co.jp/fdotnet/special/cppcli/cppcli_01.html

    2008年2月15日 16:23
  • C.John様、大変明快な回答をありがとうございました。正に知りたかったことをすべて網羅した記事でした。

     

    しかし・・・、これは、麻薬ですな。

     

    「ここだけは、避けようがない」ということで、C++にアセンブリ言語を混ぜ込んで書いたように、やはり、避けようがないところだけ、Win32ネイティブとMSILのチャンポンをするべきのように思います。

     

    これを大々的に便利だからという理由でチャンポンを続けると、品質が守れないような気がします。少なくとも、私には無理。チームを組んで複数人でソフトを書いた経験がないので、大規模なものはわかりませんが・・・。

     

    C++で書かれたNumerical Recipeの第3版を入手して、最近ちょっとグラっと(VBやC#からC++へ)来ていたのですが、やはり、VBやC#の.Net Frameworkのみで行こう・・・、と思った次第。

     

    2008年2月15日 16:39
  • 個人的な感触ですが、C++/CLIはネイティブのラッパーに徹して、処理はC#なりに任せるのが王道かと思います。

    C++/CLIで処理が書けない訳ではないのですが、いかんせん言語として複雑すぎます。

    ただでさえ複雑なC++にさらに機能を追加したので当然といえば当然なんですが・・・。

    2008年2月16日 15:19
  • えー、「処理」の意味にもよるのですが・・・、私の場合は数値処理が主な応用なので、核の処理の部分がC++/CLIかな? で、GUIなどのインターフェイスがC#やVBという感じです。

     

    別のスレッドで、マネージ配列をメモリ上で動かさずにアンマネージなポインターで操作することも教わりましたので、特に速さを必要とするところだけをC++/CLIで書いて、しかも、C++/CLIで.Net Frameworkのクラスライブラリーになるように構成できれば、C#やVBから使いやすくなると考えるようになりました。

     

    2008年2月16日 16:21
  • 数値処理のみのライブラリをネイティブC++で作成、C++/CLIクラスライブラリでラップ、C# or VBでGUIという3モジュール構成がおススメです。

     

    【メリット】

    ・数値処理ライブラリが.Net Framework以外にもネイティブC++から呼び出せるので汎用性が高い

    ・チームで保守する場合では肝心の数値処理部分はC++が出来れば保守できる。(C++/CLIが使える人は少ないので)

    ・ネイティブC++で問題になるメモリリークの検出ツールがC++/CLIでは一部機能しないことがあるので

     メモリ確保などをする処理部分はC++/CLIにしたくない。

     

    【デメリット】

    ・ラッパー部分が増えるので呼び出しのオーバーヘッドが増える。

    ・アプリケーションを構成するモジュールが増える。

    2008年2月17日 1:18
  • ありがとうございます。私個人の用途(職場における内製)に限れば、メリットとデメリットの優先順位がほぼ明らかでして以下のような感じです。ほぼ、方針は定まっていました。

     

    【メリット】

    (低)・数値処理ライブラリが.Net Framework以外にもネイティブC++から呼び出せるので汎用性が高い

    基本的に、.Net Frameworkから呼び出せればよい。

     

    (高)・チームで保守する場合では肝心の数値処理部分はC++が出来れば保守できる。(C++/CLIが使える人は少ないので)

    核の部分のソース自体は、C++がよい。

     

    (低)・ネイティブC++で問題になるメモリリークの検出ツールがC++/CLIでは一部機能しないことがあるので

        メモリ確保などをする処理部分はC++/CLIにしたくない。

    大抵の処理は、In Placeな(つまり、与えた配列さえあれば、解析できる。作業用のメモリはスタックで足りる)ものなので、配列は.Net Frameworkで用意して、(他のスレッドでご教示頂いたのですが)、pinningして渡せばよい。

     

    【デメリット】

    (低だが、速いにこしたことはない)・ラッパー部分が増えるので呼び出しのオーバーヘッドが増える。

    ここで、質問があります。.Net FrameworkからP/InvokeでネイティブC++で書かれたDLLを呼び出す場合と、C++/CLIでひとつのアセンブリを作って、その中で、マネージなコードから、アンマネージな関数を呼び出す場合と、後者のほうが速いと想像していますが、正しいでしょうか?

     

    (高)・アプリケーションを構成するモジュールが増える。

    上述の質問とも関連しますが、C++/CLIで、核のアンマネージ関数(ほぼC++の言語使用の範囲を超えずに書く)と、ラップのためのマネージ部分をひとまとめにしてしまえば、モジュールは減らせそうに思います。

     

     

    2008年2月17日 3:57
  • > .Net FrameworkからP/InvokeでネイティブC++で書かれたDLLを呼び出す場合と、C++/CLIでひとつのアセンブリを作って、その中で、マネージなコードから、アンマネージな関数を呼び出す場合と、後者のほうが速いと想像していますが、正しいでしょうか?

     

    実験してみました。……が、
    私の設定が間違っているのかも知れませんが、
    #pragma unmanaged としてアンマネージの関数を定義しても、
    なぜか最適化されないんですよねぇ。
    同じ設定で Native C++ の DLL は最適化されるんですが。
    (「逆アセンブル」で確認しました)

     

    なお、呼び出しのコストは明らかに DLL のほうが高くつきます。

     

    #ん?じゃあスタティック リンクが一番良い?

     

    *追記

    unmanaged なコードを別のファイルに移動したら、最適化されました。

    恐らく、CLR の使用の有無で判別しているものと思われます(オブジェクト ファイルの単位で)。

    2008年2月17日 13:37
  • 外池です。

     

    そうしますと、私が考えているような構成に限れば、

        C++/CLIで一つのアセンブリにするのが良く、

        かつ、そのアセンブリを作るプロジェクトにおいては、unmanagedなコードは別のファイルにしておくべし、

    ということですね・・・。

     

    単純に配列の総和を計算するだけのメンバー関数を持つクラスですが、上手い具合に動作するものができました。

     

    いろいろ資料を読んでいてちょっと意外だったのは、本件とは無関係なのですが、pure CLRのオプションを立ててC++/CLIでアセンブリを作る場合でも、C++の標準ランタイムのDLLは必要になるんですね・・・。

     

    ------------

     

    んで・・・、CLRを使う場合には、スタティックリンクはできない、とのこと。/MTオプションは不可。/MDオプションのみです。

    2008年2月17日 14:17
  • # あとで,こっそり直すかもしれません。


    まず,大前提の話をすると,

    MSIL も実行時には,ネイティブコードにコンパイルされてから実行されます。
    つまり,途中はどうであれ,最終的には,
    メモリ上のどの場所に置かれるか? が違うというだけです。

     

    a. (スレッドごとの) スタック上 (実際の値 or ポインタ相当)
    b. (プロセス内の) マネジド・ヒープ上 (マネジド・クラスのオブジェクト)
    c. (プロセス内の) ネイティブ・ヒープ上 (ネイティブ・クラスのオブジェクト)

     


    で,

    System.Int32 などの Blittable型 と呼ばれているものやその(一次元)配列 の場合 や
    System.String 対しての BSTR などの UNICODE文字列を扱うもの の場合は,
    メモリ上の実体は基本的には同じものです。

     

    違いがあるのは,GC管理下にあるかないか ということ,つまり,
    メモリ上を移動することがあったり,GCされてしまうことがあったり
    するかしないかの違いだけです。

     

    GCHandle.Alloc( a, GCHandleType.Pinned ) で
    エラーにならずにピン止めできるものなら
    基本的には同じもので,

    同じものでないのなら,(たぶん)ピン止め時にエラーになります。
    かっこよく書くと Plain Old Data (POD) なクラスか否かで,
    可能だったりそうでなかったりします。
    ピン止めできないものは,直接は渡せないことになります。

     

    P/Invoke の場合も同じで,
    相互運用マーシャラ(interop marshaler) が,
    マーシャリングは必要ないと判断すれば,
    マネジド上のポインタをアンマネジド側へそのまま渡すこともあります。

     

    パフォーマンス モニタ でも,

    .NET CLR Memory パフォーマンス オブジェクトの

    # of Pinned Objects

    を観察できるようになっているのも,

    自動でピン止めされている量をチェックできるようにするためでしょう。

     

    (プロセスやアパートメントの種類が異なれば,
    マーシャリングが必要(いわゆるコピーが起きます)なので,
    そういうことはないです)

     

    また,クラスは, POD だったとしても,P/Invoke では渡せないので,
    C++/CLI内でピン止めして解決するようにしないといけません。

     


    で,管理されているといっても,仕組みをよく考えると,
    マネジドな世界は,マネジド側から見たときにのみ,管理された世界で,
    アンマネジド側からみたマネジドな世界は,
    管理されてるわけでも,安全なわけでもないとわかります。

    先の P/Invoke で言えば,
    悪意があるコードなら,そのポインタを使って破壊することは可能です。
    フレームワーク側からセキュリティのチェックがかかるのは,
    フレームワークが絡むメソッドの呼び出し時なので,
    そこを通らない場合は,ノーチェックになります。

     

    なにが言いたいのかというと,

    メモリ上にあるということに変わりはないということです。

    ポインタを直接渡せたりすると,一瞬,すごいと思うんですが,

    そうではなく,それ以上でも,それ以下でもないということです。

     


    >-----------------------------------------------------------
    話は替わりますが,
    いわゆる最初のコンパイル時に,
    MSIL にしてしまうか,ネイティブコンパイルまで進むかは,
    関数/メソッド単位にまでできたと思います。


    でも,実際には,
    チャンポンに見えたりするかもしれませんが,

    関数やメソッド内で,
    ローカル変数のいくつかが マネジド で,
    ローカル変数のいくつかが アンマネジド であっても,
    ローカル変数 というのは,スタック上 のできごとなので,
    マネジドヒープなのか,ネイティブヒープなのかというような
    「今はどのヒープ上なんだ?」 ということは関係してこないので,
    スタック上のもの "そのもの" には関係なく,
    それがオブジェクトを差している場合なら,
    マネジドヒープ上のもの と,ネイティブヒープ上 のもの
    をそれぞれ差している というだけなので
    それ以上の違いはなく,問題はないわけです。

     

    クラスの メンバ変数/フィールドメンバ は,
    クラスの実体がヒープ上に置かれる関係で,

    a. クラスの実体が マネジドヒープに置かれるのなら,
       つまり,クラスが マネジドクラス なら,GCの管理下の沿うもの

    b. クラスの実体が ネイティブヒープに置かれるのなら,
       つまり,クラスが ネイティブクラス なら,
       いわゆるネイティブとして扱えるものもの

    としてのそれぞれに合う データ型 になっていなといけないので,
    フィールドメンバは,チャンポン に(今のところ)できないわけです。


    マネジドクラスのフィールドメンバに
     IntPtr を置けるように,

    ポインタなら置けるので,ネイティブのメンバを置けます。
    で,当然,

    gcnew でなく new したものをそれにセットします。

     

    フィールドメンバにしても,ローカル変数にしても,

    チャンポンになっていても,

     

    マネジド・ヒープ上に確保するから gcnew

    ネイティブ・ヒープ上に確保するから new

     

    マネジド・オブジェクトを差すから ^ (ハット)

    ネイティブ・オブジェクトを差すから * (ポインタ)

     

    という風に,そんなにややこしくないわけです。

     


    ただ,
    アンマネジド・リソースを IntPtr で扱うときと同様に,
    開放漏れは起こり得ます。
    cf.
    自作の AutoPtr<>を作って,value type semantics の仕掛けで楽する例
    http://weblogs.asp.net/kennykerr/archive/2005/07/12/419102.aspx
    ただ,代入時に非同期例外が起きるとアウトでしょう。

     

    逆は,最初から,gcroot<T^> という飛び道具
    (マネジドハンドルのネイティブラッパーらしい)が
    用意されてます。

    2008年2月17日 17:49
  • 外池です。このまま本にしたいぐらいです。(感謝)

     

    で、ごくごく要点のみコメントさせて頂くと、

     

     yayadon さんからの引用

    GCHandle.Alloc( a, GCHandleType.Pinned ) で
    エラーにならずにピン止めできるものなら
    基本的には同じもので,

    同じものでないのなら,(たぶん)ピン止め時にエラーになります。

     

    ここまでのところ、VB.NET側でクラスを作ってピン止めして渡す方向のことしか考えてこなかったので、ピン止めできない例を思い描くことが難しいのですが・・・。

     

    「ピン止め」というのは、オブジェクトの「メモリー上の位置を固定する」意味のほかに、その内容も外部環境の影響から隔離されていることを意味するのでしょうか? だとすれば、ピン止めできない場合があることは、よく理解できます。

     

     yayadon さんからの引用

    ポインタを直接渡せたりすると,一瞬,すごいと思うんですが,

    そうではなく,それ以上でも,それ以下でもないということです。

     

    実のところ、「すごい」と思ったのは、そういう手法を採れる「余地を残してある」ことですね。マーシャリングの類の操作は、VB.NETを使っている限り、与えられたものを使う以外方法がなかったわけで、.Net Frameworkはそのようにして強固な構造になっていると思い込んでいました。

     

    特に、私は、与えられているクラスライブラリーは内部的には、アンマネージな操作が大量に駆使されているのかと思っていたのですが、ソースの一部を見て相当量がマネージで書かれていることを知って「そこまで徹底しているのか」と感心したぐらいだったので、C++/CLIで、ごっそり抜け穴を発見したような印象を持った次第です。

     

     yayadon さんからの引用

    ただ,
    アンマネジド・リソースを IntPtr で扱うときと同様に,
    開放漏れは起こり得ます。
    cf.
    自作の AutoPtr<>を作って,value type semantics の仕掛けで楽する例
    http://weblogs.asp.net/kennykerr/archive/2005/07/12/419102.aspx
    ただ,代入時に非同期例外が起きるとアウトでしょう。

     

    逆は,最初から,gcroot<T^> という飛び道具
    (マネジドハンドルのネイティブラッパーらしい)が
    用意されてます。

     

    すいません、ここに来て、急に理解できなくなってしまいました。また、勉強します。

    ----(追記)

    cf.
    自作の AutoPtr<>を作って,value type semantics の仕掛けで楽する例

    http://weblogs.asp.net/kennykerr/archive/2005/07/12/419102.aspx

    を斜め読みしてみましたが・・・、

     

    単に、PODなオブジェクトを指すポインターを単独で取り扱うだけでなく、アンマネージな構造体のメンバーにマネージなオブジェクトへのポインターを保持させる、逆に、マネージな構造体のメンバーにアンマネージなオブジェクトへのポインターを保持させる方法が示してある、ということですね?

    2008年2月18日 0:19
  •  

    クラスライブラリが C# などのマネジドコードで書かれていて,

    それが MSIL となっていても,

    実行時は,ネイティブコード です。

    managed (管理された) という意味の 75% ほどはすでに無くなっています。 

    MSIL のまま配布しないといけないのはそのためです。

     

    そのPCで,MSIL から ネイティブコードへコンパイルされる時に,

    データ型等のチェック や メソッド呼び出しの呼び出し先のチェックを行っていて,

    それ以降は,

    メソッド呼び出しの呼び出し先の連鎖チェック が必要なときに入るくらいです。

     

     

    あと,ピン止めされた ポインタ を渡すことができると書きましたが,

    正確には,アドレス を渡すことができる という意味です。

    ポインタ を渡せることに,それ以上もそれ以下もないという意味は,

    「アドレスを渡すことは可能,でも,ただそれだけ」

     という意味です。 

     

    例えば,マネジドクラス が, 実行時に,ネイティブコンパイル された後の

    フィールドメンバ の メモリ上の位置 が不明になってしまうものは,

    フィールドメンバ単位で,ピン止めしておかないといけません。

     

    クラス自体をピン止めできるということは,

    クラス自体が Blittable であるということが言えて,

    ネイティブ側で そのクラスのポインタにキャストして,

    メンバアクセスしても,

    問題のないメモリ上の配置になっているということです。

     

     

    >---------------------------------------------------------------------------------

    AutoPtr<> は,

    C++/CLI では,C#/VB の Dispose() を using{ } で楽することができる例の機能を

    value type semantics で書いた時に,

    自動で デストラクタ (C++/CLIのDispose相当) を呼んでくれるを

    クラスの フィールドレベル でやれるようにするものです。

    AutoPtr<> 自体は,値型セマンティクス で宣言しておけば,

    デストラクタ(~  / Dispose()相当 )を呼んでもらえます。

    ポインタを格納しておいて,そのデストラクタで開放してやれば,

    開放し忘れがなくなるだろう...というのです。

    2008年2月18日 9:58
  •  yayadon さんからの引用

    クラスライブラリが C# などのマネジドコードで書かれていて,それが MSIL となっていても,実行時は,ネイティブコード です。managed (管理された) という意味の 75% ほどはすでに無くなっています。 MSIL のまま配布しないといけないのはそのためです。

     

    そう・・・、実行時にはネィテイブコードにコンパイルされていることは承知していたのですが、イマイチ、ピンと来てなかったところがあります。ネイティブコードにコンパイルされていても、コードの実行を監視している「強力」な仕組みがあると思っていたのですが、それほどでもない(75%ほどはすでになくなっている)わけですね。

     

    別件で、今日は、純粋にMSILが吐き出されるVBのルーチンを書いたのですが、C++でWin32ネイティブなコードを生成するのとほぼ同じぐらいのパフォーマンスが出ていました。配列を使わないタイプのルーチンだったので、特にそうだったのでしょう。

     

    ところで、C#ではunsafeでポインターが使えますよね。これを使って配列境界チェック無しで大きな配列を扱う作業をすれば、やはりC++のWin32ネイティブコードに迫るパフォーマンスが出るのかもしれません。(試してみないと)

     

     yayadon さんからの引用

    AutoPtr<> は,C++/CLI では,C#/VB の Dispose() を using{ } で楽することができる例の機能をvalue type semantics で書いた時に,自動で デストラクタ (C++/CLIのDispose相当) を呼んでくれるをクラスの フィールドレベル でやれるようにするものです。AutoPtr<> 自体は,値型セマンティクス で宣言しておけば,デストラクタ(~  / Dispose()相当 )を呼んでもらえます。ポインタを格納しておいて,そのデストラクタで開放してやれば,開放し忘れがなくなるだろう...というのです。

     

    わかりやすい説明、ありがとうございました。

    2008年2月18日 14:14
  •  

    実行時にフレームワーク側からのチェックが入るのは,

    私が今思いつくのは,

     

    Security Demand が Demand (SecurityAction.Demand) の時,

    P/Invoke で,外部のメソッドを呼ぶときに,

    今その外部のメソッドを呼び出そうとしているメソッドを呼び出したメソッドの権限のチェック

    その権限のチェックをしたメソッドを呼び出したメソッドの権限のチェック 

    その権限のチェックをしたメソッドを呼び出したメソッドの権限のチェック 

    その権限のチェックをしたメソッドを呼び出したメソッドの権限のチェック 

      :

      :

      :

     

    のような行為(上位方向へのスタック・ウォーク)です。

     

    P/Invoke が遅くなるのは,

    まず,引数を渡すときにコピーが起きる時は,それが原因になります。

    ですが,コピーが起きなくてもこれが原因になります。

    ブログなどでよく例えに出てくると思うんだけど,

    鉄人28号 の場合,操縦器が悪人に渡ったら,それまでだけど,

    .NET Framework では,それを許さないだけでなく,

    金田正太郎? が操縦器を握っていたとしても,

    誰か悪いやつらに操られていたりしても許さないという感じになってるわけです。

    一操作ごとにチェックが入るので,パフォーマンス的には具合が悪い(performance hit)です。

     

    P/Invoke 以外では,

    例えば, FileIOPermission が必要なメソッドの呼び出しの場合でも,

    同じようなチェックが入ります。

     

    Security Demand が LinkDemand (SecuriytAction.LinkDemand) の時は,

    ネイティブコードになる時(JITコンパイル時) にチェックするだけです。

    このときは,さらに,ずっとたどるのでは無く,一つ前の呼び出しまでしかチェックしません。

    いずれにしても,実行時には関係なくなります。

     

    私の知る限りでは,メソッドの呼び出しには制約が残るけれど,

    メモリに実際にあるものは,無防備な状態になってます。

    ネイティブC++ で,例えば,

    int *a = ...;

    a += 1;

    とした時に,マネジド・ヒープ側でも,int分のバイト数だけポインタが移動した位置に

    次のintに相当するものがあれば,

    そのまま使えるということになるわけです。

    使えるか,使えないかを IsBlittable かどうかで判断するようなものを

    GCHandle.Alloc( a, GCHandleType.Pinned ) 内から呼ばれる先で

    判断しているということです。

     

    個別メンバに対するピン止めではなく,

    クラスに対してピン止めする場合は,

     value class だけでなく,ref class でも,

    LayoutStruct が Sequential だったり Explicit で FieldOffset を指定してあったりすれば,

    個別メンバに対して,さらにチェックをかけて,

    IsBlittable かどうかを判断しているようです。

     

     

    # VB が微妙に遅い気がするのは,

    # デフォルトで,整数のオーバーフローチェック が入っているので,

    # そのせいがかなりあります。 

    # C#の場合は,入っていないので,

    # checked( ... ) を使ってオーバーフローをチェックが必要だったりします。

     

     

    2008年2月18日 17:35
  • ヒープソートのアルゴリズムで、16.8メガ個程度のDouble型数値を並べ替える操作を、以下の考え方でプログラムしてみました。

     

    1) Visual Basic.NETでマネージド配列の添え字で操作する(境界チェックあり)

        13.5秒

    2) C#でマネージド配列の添え字で操作する(境界チェックあり)

        上とほぼ同じ

    3) C#でunsafeにして、配列をfixedにして、ポインターと添え字で操作する(境界チェックなし)

        13.2秒(ごく若干速くなったか?)

    4) C++/CLIで、マネージド配列をpinningして、アンマネージドな関数に渡してポインターと添え字で操作する(境界チェックなし)

        12.8秒(速くなってもこの程度)

     

    秒数の絶対値は、実行環境に大きく依存すると思いますので、これら4つの数字の相対値を参考にして頂ければ幸いです。無理して難しいことをする必要はないように感じてきました。

    2008年2月19日 8:24
  • おまけ的な話(今回のケースでは該当しません)ですが、オブジェクトの生成/破棄を繰り返すような処理ではマネージコードの方がメモリをプールしている分、ネイティブよりも高速な場合があります。

    なんでもネイティブにしたら早くなる訳ではないという事を覚えておくといいかもしれません。

    2008年2月19日 11:10