none
CLRに準拠してMSILを生成することを意図した場合のC++の「配列」の考え方について RRS feed

  • 質問

  • 外池と申します。(個別の問題として切り出して、論点を明確にすることにしました。)

     

    まず、純粋にCLRと言うか.Net Frameworkに準拠するアプリをC++で書きたいとします。つまり、.Net Frameworkのアプリを書きたいのだが、VB.NETや、C#ではなく、言語としてC++を選びたい、というわけです。このような場合の開発言語は、どのように呼べば一番誤解が少ないのでしょうか? C++マネージ拡張? C++/CLI? (どちらで呼んでも、C++のアンマネージな部分を排除できていないように思われるのですが・・・。とにかく、CLRを使うオプションを立てることとします。) 

     

    で、本題ですが、上述の、アンマネージな部分を排除できていない、に関連して、配列の考え方の質問です。

     

    単純に、プリミティブ型のInt32の変数を宣言したいと思って「int i;」と書いたとします。これは、正しい書き方ですか?(C++のint型を、.NetFrameworkのInt32型だと読み替える機能が備わっている) それとも、すでに誤っている?(そのような機能はない)

     

    次に、プリミティブ型のInt32の配列を定義したいと思って「int i[10];」と書いたとします。これは、正しい書き方ですか?(C++のint型を.Net FrameworkのInt32型だと読み替え、かつ、配列の宣言にあたって、.Net Framewokのarrayクラスのコンパイラーに対する機能を呼び込む機能が備わっている) それとも、誤っている?(そのような機能はない)

     

    他のスレッドの議論からして、「そのような機能はない」ということなのだろうと、理解しつつあるのですが、だとすれば、生成されるコードはどのようなものになるのかが、イメージがわかないのです。

     

    CLRのWindowアプリを作る意図で、プロジェクトを作りコードを書き始めたとします。Form1.hに対するコーデイングは、.Net FrameworkのSystem.Windows.Forms.Formクラスを継承する.Net Frameworkのクラスをつくる作業なわけですが、その中で、「int i;」とか、「int i[10];」とか宣言された「もの」の、コンパイル後の振る舞いは、Win32ネイティブ? MSIL?

     

    コード全体としては、概ねMSILが吐き出されるのでしょうけれども、ある部分だけが、自動的にWin32ネイティブになるのでしょうか?

     

    以上で、質問は終わりです。(Win32ネィテイブな関数を呼びにいくときは、自動的にP/Invokeで処理されることはわかったのですが、C++の言語に備わる要素としての型や配列までもがWin32ネイティブな扱いになるのかどうか・・・)

     

    2008年2月16日 0:59

回答

すべての返信

  • 通常、int は System::Int32 として扱われます。
    ただし unmanaged な関数の中では、純粋な int として扱われます。

     

    int i[10];
    については、Int32 の配列を宣言した事にはなりますが、
    その配列はマネージでありません。正しくは、
    array<int>^ i = gcnew array<int>( 10 );
    です。

     

    この辺の区別は、紛らわしいようでありますが、

    アンマネージな関数やデータを扱う機会の多い C++/CLI ならではとも言えます。

     

    ちなみに、
    int i[10];
    と宣言した配列に対して、
    i->Length
    と書くとコンパイルに失敗しますが、
    i[0].ToString()
    は成功します。

    2008年2月16日 1:56
  • Abstractさん、ありがとうございました。

     

    他のスレッドではご迷惑をおかけしてしまいましたが、この辺りの整理が無いままに突き進むと、私も墓穴ばかり開けていきそうで(落ちなくても穴ばかり増えて、他の局面で落ちる)、怖いところでした。

     

    もう少し、突っ込んで教えて頂ければ嬉しいです。

     

    マネージコードの部分でも、int i[10];とアンマネージな配列を宣言することは許容されている。そうしますと、マネージコードにおけるiに対する演算操作はどのようにコンパイルされるのでしょうか? i[n].ToString()は良い実例ですが、

    1. i[n]という、アドレスを演算してその中身を返す操作はアンマネージコード(Win32ネイティブ)で生成され、ToStringでMSILに切り替わる感じでしょうか?(めまぐるしくWin32ネイティブとMSILが切り替わるまだら構造になり得る?)
    2. それとも、基本的にすべてMSILだけれども、i[n]の部分だけがP/Invokeで処理される?(基本的にMSIL。ただ、多数P/Invokeが発生するならば、パフォーマンス的に不利か?)
    3. それとも、純粋にMSIL。つまりマネージな処理が省略されているだけで、i[n]の部分もMSIL?(基本的にMSILであり、概ねMSILの実行環境のパフォーマンスに支配される)

    この3種類ぐらいが想像されるのですが・・・、どれが一番近いのでしょう?

    2008年2月16日 2:36
  • 外池です。自己レスです。さきほどの質問については、とりあえず、「3.純粋にMSIL」と結論しました。(ただし、あくまで、スタックで確保する場合、と条件を限定しておきます。)

     

    CLRのコンソールアプリのプロジェクトで、配列とポインタを使う簡単なプログラムを書いて、ビルドとILDASMによる逆アセンブルをすると、様子がよくわかりました。ソースは

    Code Snippet

    #include "stdafx.h"
    using namespace System;

    int main(array ^args)
    {
        Console::WriteLine(L"Hello World");

     int j[4];
     int *i = j;

     *i = 10;
     i++;
     *i = 20;
     i++;
     *i = 30;
     i++;
     *i = 40;

     Console::WriteLine(j[0]);
     Console::WriteLine(j[1]);
     Console::WriteLine(j[2]);
     Console::WriteLine(j[3]);
     return 0;
    }

     

     

    これを、最適化を無効にしてコンパイルし、ILDASMで逆アセンブルすると

    Code Snippet
    .method assembly static int32  main(string[] args) cil managed
    {
      // コード サイズ       86 (0x56)
      .maxstack  2
      .locals ([0] int32* i,
               [1] int32 V_1,
               [2] valuetype ''.$ArrayType$$$BY03H j)
      IL_0000:  ldc.i4.0
      IL_0001:  stloc.1
      IL_0002:  ldstr      "Hello World"
      IL_0007:  call       void [mscorlib]System.Console::WriteLine(string)
      IL_000c:  ldloca.s   j
      IL_000e:  stloc.0
      IL_000f:  ldloc.0
      IL_0010:  ldc.i4.s   10
      IL_0012:  stind.i4
      IL_0013:  ldloc.0
      IL_0014:  ldc.i4.4
      IL_0015:  add
      IL_0016:  stloc.0
      IL_0017:  ldloc.0
      IL_0018:  ldc.i4.s   20
      IL_001a:  stind.i4
      IL_001b:  ldloc.0
      IL_001c:  ldc.i4.4
      IL_001d:  add
      IL_001e:  stloc.0
      IL_001f:  ldloc.0
      IL_0020:  ldc.i4.s   30
      IL_0022:  stind.i4
      IL_0023:  ldloc.0
      IL_0024:  ldc.i4.4
      IL_0025:  add
      IL_0026:  stloc.0
      IL_0027:  ldloc.0
      IL_0028:  ldc.i4.s   40
      IL_002a:  stind.i4
      IL_002b:  ldloca.s   j
      IL_002d:  ldind.i4
      IL_002e:  call       void [mscorlib]System.Console::WriteLine(int32)
      IL_0033:  ldloca.s   j
      IL_0035:  ldc.i4.4
      IL_0036:  add
      IL_0037:  ldind.i4
      IL_0038:  call       void [mscorlib]System.Console::WriteLine(int32)
      IL_003d:  ldloca.s   j
      IL_003f:  ldc.i4.8
      IL_0040:  add
      IL_0041:  ldind.i4
      IL_0042:  call       void [mscorlib]System.Console::WriteLine(int32)
      IL_0047:  ldloca.s   j
      IL_0049:  ldc.i4.s   12
      IL_004b:  add
      IL_004c:  ldind.i4
      IL_004d:  call       void [mscorlib]System.Console::WriteLine(int32)
      IL_0052:  ldc.i4.0
      IL_0053:  stloc.1
      IL_0054:  ldloc.1
      IL_0055:  ret
    } // end of method 'Global Functions'::main

     

     

    最適化O2(実行速度)を適用すると、

    Code Snippet

    .method assembly static int32  main(string[] args) cil managed
    {
      // コード サイズ       40 (0x28)
      .maxstack  1
      IL_0000:  ldstr      "Hello World"
      IL_0005:  call       void [mscorlib]System.Console::WriteLine(string)
      IL_000a:  ldc.i4.s   10
      IL_000c:  call       void [mscorlib]System.Console::WriteLine(int32)
      IL_0011:  ldc.i4.s   20
      IL_0013:  call       void [mscorlib]System.Console::WriteLine(int32)
      IL_0018:  ldc.i4.s   30
      IL_001a:  call       void [mscorlib]System.Console::WriteLine(int32)
      IL_001f:  ldc.i4.s   40
      IL_0021:  call       void [mscorlib]System.Console::WriteLine(int32)
      IL_0026:  ldc.i4.0
      IL_0027:  ret
    } // end of method 'Global Functions'::main

     

     

     

     

    2008年2月16日 4:52
  • MSIL レベルで見た両者の違いは、「その要素の取得方法」だけに思えます。
    例えば次の2つの配列を考えてみます。

     

    int native_array[ 10 ];
    array<int>^ managed_array = gcnew array<int>( 10 );

     

    ここで、native_array[ 3 ] を取得する MSIL は、

     

    ldloca.s native_array
    ldc.i4.s 12
    add
    ldind.i4

     

    となります。一方、managed_array[ 3 ] については、

     

    ldloc.0
    ldc.i4.3
    ldelem.i4

     

    となります(ldloc.0 の 0 については、場合によって変わります)。

    この結果得られた数値に対する扱いは、両者とも同じです。

     

    なお、Int32::ToString の呼び出しなどでアドレスが必要な場合、
    値を隠し変数に代入して、その変数のアドレスを渡すようです。

    2008年2月16日 5:19
  • アンマネージとマネージの配列の比較、ありがとうございます。

     

    MSILで見てさえも非常に簡単になってしまっていますが・・・、MSIL自体がマネージ配列を管理する機能も持っているから、という理解でよろしいんですよね?

     

    具体的には、マネージ配列のMSILの一番最後に現れる、ldelem.i4、この命令において、添え字の範囲のチェックも行われてしまう、という理解でよろしいんですよね?

     

     

    2008年2月16日 5:34
  • Yes.

    ちなみに配列を生成する命令も(わざわざ)用意されています。

    2008年2月16日 5:54
  • 最後の止めに、

     

    マネージクラス(VBLib.CommonClass)を公開するアセンブリ(DLL)をVBでつくっておいて、C++のCLRのコンソールアプリのプロジェクトでこれを参照&using namespace VBLibしつつ、ネイティブ配列にいれることを試してみます・・・。

     

    ----追記:     あかん・・・、っぽい

     

     

    2008年2月16日 6:10
  • ネイティブ配列にできるのは、ポインタを除くプリミティブ型に限られるようです。

    2008年2月16日 7:30
  • なるほど。

     

    array<VBLib.CommonClass^>^ には読み込むことができました。

    (まぁ、これはCLR同士なので、あたりまえ・・・、と)

     

    ということで、出来ることと、出来ないこと、整理ができました、ありがとうございました。

     

    狙いは・・・、(以下、もし、今後も、お知恵を拝借できるなら、参考にしていただけると幸いです)

     

    非常に大きな配列の操作(数学で言う行列の演算)を、できるだけ高速に行いたいのです。素のC++(Win32ネイティブ)でポインターを駆使して行えば最も良いパフォーマンスが得られることは知っているのですが、できれば、アプリのGUIの作りこみを考えれば.Net Frameworkの方が圧倒的に楽なことも知っています。GUIと内部の演算と上手く役割り分担させられれば良いのですが、もうひとつ、考えていることがあります。

     

    大規模な演算を行う配列の初期値のセットアップを、できれば、.Net Framework側でマネージ配列でやりたい。その上で、演算はアンマネージなポインターで高速にやりたい。虫が良すぎるとは承知の上ですが、この切り替えのところで、できれば、メモリ上での配列の転送(マネージ領域からアンマネージ領域へ、あと、演算結果の戻し)は避けたい。配列自体が大きなものなので転送もかなりの負荷になるからです。

     

    C++/CLIでこの辺りの解決策が見つからないか?と思った次第です。次は、C#にもポインターがあると聞いているので、調べてみようと思います。素のC++(Win32ネイティブ)のポインターほどにパフォーマンスは出ないでしょうが、トレードオフが見つかれば良いな、と。

     

     

    2008年2月16日 10:11
  • Marshal::UnsafeAddrOfPinnedArrayElement
    を使えば、どうにかなりそうな気もします(未検証)。
    いちおう、次のコードは意図どおりに動きました。

    data[2] は 3.14 になるはず。

    using namespace System;
    using namespace System::Runtime::InteropServices;

     

    #pragma unmanaged

     

    void __stdcall Calculate( float* p )
    {
      p[ 2 ] = 3.14f;
    }

     

    #pragma managed

     

    int main( array<String^>^ args )
    {
      array<float>^ data = gcnew array<float>( 10 );

     

      IntPtr pData = Marshal::UnsafeAddrOfPinnedArrayElement( data, 0 );
      float* pFirst = static_cast<float*>( pData.ToPointer() );

     

      Calculate( pFirst );

     

      return 0;
    }

     

     

    どこか解釈が間違っていましたらご指摘願います。

    2008年2月16日 11:14
  •  外池 さんからの引用
    大規模な演算を行う配列の初期値のセットアップを、できれば、.Net Framework側でマネージ配列でやりたい。その上で、演算はアンマネージなポインターで高速にやりたい。虫が良すぎるとは承知の上ですが、この切り替えのところで、できれば、メモリ上での配列の転送(マネージ領域からアンマネージ領域へ、あと、演算結果の戻し)は避けたい。配列自体が大きなものなので転送もかなりの負荷になるからです。

     

    pinning した配列要素のアドレスは,物理メモリ上で固定された「アンマネージ BLOB のポインタ」と見なせます.

    1. C# で配列の作成
    2. 配列の pinning
    3. 配列先頭アドレスをアンマネージ C++ コードに渡して計算
    4. pinning 解除

    で目的のことは達成できるように思います.

    C# ではなくて C++/CLI のみでも可能です.

    2008年2月16日 11:43
  • どうも例示ありがとうございます。

    (すいません、以下、独り言モードのように思ったままを書きなぐります)

     

    要するに・・・、マネージ配列の中を移動できるポインターが出来た、ということですよね?

    そのポインターを関数にも渡せる。

     

    で、これは、MSILに展開されるものですよね?

    いや、違う、渡した相手の関数は、アンマネージだ! おぉ・・・・・、これは、すごい。

     

    まとめると、

      出所の配列はマネージ配列で、

      その中を移動できるポインターをアンマネージの関数に引き渡し、

      アンマネージの関数では高速に処理できる。

     

    というこで、まったく適切に解釈して頂いています。感謝というより、感激に近いです。

     

    ----追記

     

    NyaRuRuさんも、pinningについてコメントいただき、ありがとうございます。

     

     

     

     

     

     

    2008年2月16日 12:04