unsafe の限界

サンプル コードのダウンロード サンプルコード (Unsafe1.exe) のダウンロード

Eric Gunnerson
Microsoft Corporation

October 9, 2001
日本語版最終更新日 2001 年 12 月 4 日

Download or browse the Unsafe1.exe in the MSDN Online Code Center から Unsafe1.exe サンプル ファイルを参照またはダウンロードしてください。

私は何年にも渡ってプログラミングに携わってきており、 多くの異なる言語でコードを記述してきました。 約 5 年前、 テスト プロジェクトの一環として、 テストを実行して結果を生成するために、 比較的複雑なハーネス コードを作成する必要がありました。 "バッチ ファイル" 形式の言語を使用しましたが、 それらの言語はあまり読みやすくなく、 その機能にも満足できませんでした。 C++ でハーネス コードを記述できることは知っていましたが、 やや困難で、あまり手ごろなものではありませんでした。 バッチ ファイル形式の言語よりは形式が整っていて、 C++ ほど作業量が多くない言語が必要だと思いました。

私は、調査中に偶然 Perl の例を見つけ、 そのプロジェクトでは Perl を使用することにしました。 物事をまとめるためのユーティリティ コードを記述するのに適した言語があまりないので、 ここで使用した Perl を長年に渡って拡張してきました。 Perl がこのような処理を行うのに適している理由の 1 つは、 Perl が実用的な言語であることです。 つまり、設計の確実性や簡潔性が適しているわけではなく、 Perl が単にこの処理を行えることが適しているだけです。

C# は Perl よりも形式が整った言語ですが、 私が Perl を好んでいたのと同じような実用的な考え方を持っています。 つまり、C# を使用すると、やややっかいな処理でも、 それが問題の解決を簡単にする場合は、 実行可能です。 それどころか、言語の正しい使い方をやや逸脱したり、 やや危険な処理を行うこともできます。

今後のいくつかのコラムでは、 C# がその他の言語と区別されるいくつかの機能についてお話しようと思います。 今月のコラムでは、"unsafe" コードとしても知られている C# のポインタの使い方についてお話しましょう。

なぜ C# はポインタを持っているのか?

C++ 言語では、ポインタの使用は必要不可欠です。 ポインタは C++ に非常に大きな能力と柔軟性を提供します。 これは C++ の強みであると同時に弱点でもあります。 C++ では、ポインタを使ってユーザーが希望する処理を自由に行えるという強みがあります。 ただし、自由に処理を行えることにより潜在的なバグを持つコードが作成されやすくなります。 これはプログラマの皆さんの注意事項です。

一方、Java では、 ポインタは、扱いが困難な問題を引き起こすものと見なされ、 プログラマには公開されません。 つまり、ポインタに関連するコードによく見られるバグを扱う必要がありません。 残念ながら、Java ではこの安全策が常に機能するので、ポインタの恩恵を受けることはできません。

C# は、ポインタに関して C++ と Java の中間の見解を持っています。 人々が記述する大部分のコードでは、 ポインタの欠点の方が利点よりも顕著です。 したがって、C# は "通常の" コードでポインタを使用することは許可していません。 ただし、実用的な観点から見ると、 ポインタが非常に役に立つ場合があります。 そのため、C# ではポインタを使用する必要がある場合に、 この安全策を回避する方法を用意しています。

この結果、C# は私たちプログラマに Java モデルが提供する安全性と、 必要な場合は C++ モデルの柔軟性の大部分の両方を提供します。 この柔軟性が必要になる状況は、次の 3 つのカテゴリに分類できます。

  • パフォーマンスを重視する場合
  • 既存のバイナリ構造を扱う場合
  • COM ライブラリまたはネイティブ ライブラリとの高度な相互運用性が必要な場合

上記の各カテゴリについてお話する前に、まず unsafe について説明しておきましょう。

なぜ "unsafe" は安全ではないのか?

技術的な観点から見ると、 用語 unsafe は、 プログラムが安全であることをわかっているかどうかを意味します。 プログラムが IL (中間言語) からネイティブ コードに変換される前に、 検証処理と呼ばれるランタイム セキュリティ システムの一部が IL を調べ、 コードを安全に実行できるかどうかを判断します。 このような状況で "安全である (safe)" とは、 IL が問題が生じそうな処理を実行しないことをこの検証処理が証明できることを意味します。

いくつかの Microsoft .NET の使用事例では IL の安全性が重要になります。 Web サイトからダウンロードした一連のコードが、 コンピュータに危害を加えないということを判断できるのはすばらしいことです。 (Web サイトまたはネット共有のいずれかからの) リモート コードの既定のポリシーでは、 コードを実行しても安全であることを確認する必要があると定めています。

状況によっては、安全性を確認できないコードを記述することが役に立つことがあります。 C# では、 COM interop やプラットフォームの起動など、 interop を使用した場合や、 ポインタを使用すると unsafe コードが生成されます。

C# では、 unsafe コードを無意識のうちに記述しないように、 ポインタを扱うコードを記述するときは、 クラスまたはメソッドで unsafeキーワードを使用する必要があります。 unsafe キーワードを使用すると、 結果として生成されるコードの IL は unsafe としてマークされ、 完全に信頼されている環境でしか実行できません (通常、セキュリティ ポリシーはローカル アセンブリのみを信頼します)。 現在のバージョンのランタイムでは、 unsafe はアセンブリ レベルで定義されます。 したがって、アセンブリ内に unsafe コードを持つと、 アセンブリ全体が安全ではなくなります。

マネージ環境でのポインタ

.NET などのマネージ環境では、 ポインタの使用に一部制限があります。 マネージ環境では、 メモリ内のオブジェクトの場所はランタイムが管理しており、 ガベージ コレクタ (GC) はオブジェクトを任意の時点で移動できます。 次のようなコードを記述したとします。


byte[] arr = new byte[512];
byte* pArr = arr;
// ここで pArr を使用します...

pArr ポインタは、 arr 配列の最初の要素を指します。 ただし、配列は GC によって移動される可能性があり、 その結果ポインタはどこか別の場所を指すことになります。 これは困りますね。

ここで必要なのが、 ガベージ コレクションが発生してもオブジェクトが移動されないように GC に通知する方法です。 これは "固定" と呼ばれる処理で行います。 オブジェクトに対してフラグを設定することによって、 GC はそのオブジェクトを移動しないことを認識します。 長期間に渡ってこのフラグを設定すると、 メモリ不足が生じます。 したがって、C# ではオブジェクトの固定は短期間しか行えません。 この処理は fixed ステートメントで行います。 上記の例は次のように書き直すことができます。


byte[] arr = new byte[512];
fixed (byte* pArr = arr)
{
// ここで pArr を使用します...
}

使用者は fixed ブロックの最初で、配列が固定され、 ポインタがこのブロック全体で有効であることを認識します。 この実装が優れている点は、 配列が固定されているときにガベージ コレクションが発生した場合にのみ、 ガベージ コレクタが影響を受けることです。 このような状況が発生する可能性はあまりないので、 この方法のオーバーヘッドは最小限にとどめられます。

配列を使った作業

これでポインタの使用に関する基礎についてはお話したので、 今度は例を挙げて説明しましょう。 この例のアプリケーションでは、 配列に格納されている数値をできるだけ高速に合計することが非常に重要だとします。 この処理を行ういくつか異なる方法について見ていこうと思います。

まず最初の方法では、foreach を使用して単純に配列全体を処理しています。


static int AddArrayForeach(int[] numbers)
{
   int result = 0;
   for (int it = 0; it < Iterations; it++)
   {
      foreach (int i in numbers)
      {
         result += i;
      }
   }
   return result;
}

内側のループでは、 単に foreach を使用して配列の各要素を繰り返し、 結果に値を加算しています。 これが実際の機能である場合は、行う処理はこれですべてです。 ただし、この実装をその他の実装とのベンチマーク テストの基準にしたいので、 合計処理時間が 1 秒以上になるように処理を行う必要があります。 そのため、処理全体を単純に繰り返す外側のループ内に foreach を埋め込んでいます。

細かい時間の計測を行うために、 Windows NT® パフォーマンス カウンタを使用する counter クラスによって時間の計測が行われます。 パフォーマンスを向上するために作業している場合は、 常に作業している機能のパフォーマンスを計測しておくことが非常に重要です。

時間を計測するコードは次のようになります。


Counter counter = new Counter();
int result;
counter.Clear();
counter.Start();
result = AddArrayForeach(numbers);
counter.Stop();
Console.WriteLine("Foreach {0}, {1} seconds", result, counter.Seconds);

foreach を使用した最初の実装は、 この操作を 1.84 秒で行います (配列内の 1,000,000 個の要素を 100 回繰り返します)。

foreach がどのように機能しているかについて少しでも知っていると、 独自に for ループを記述するよりも余分にオーバーヘッドがかかることがわかります。 少し異なるコードを記述してみましょう。


static int AddArrayFor(int[] numbers)
{
   int result = 0;
   for (int it = 0; it < Iterations; it++)
   {
      for (int index = 0; index < numbers.Length; index++)
      {
         result += numbers[index];
      }
   }
   return result;
}

この例では、配列のインデックスを使用して、配列の要素にアクセスしています。 numbers[index] 式は配列の特定の要素を返します。

上記のコードを実行すると、1.80 秒かかります。 約 3 パーセント早くなりました。 でも、それほど有効な改善ではありません。

今度は、unsafe コードを使用しておなじ機能を記述してみましょう。 unsafe コードを記述するには、 共通言語ランタイム (CLR:Common Language Runtime) で配列がどのように実装されているかについて少し理解しておく必要があります。

CLR で実装される配列は、 配列のすべての要素を保持するのに十分な大きさがある連続したメモリ ブロックです。 この例では、n 個の整数の配列を扱います。 したがって、n 個の整数を格納しているメモリ ブロックが存在します。 上記の例で配列インデックスの構文を使用すると、 array クラスはメモリ内の配列の開始アドレスを取得し、 指定したインデックスのオフセットを加算して、特定の要素を見つけ出します。 この結果、特定の要素のアドレスを取得でき、 その要素の値を抽出できます。

有効なインデックスを渡した場合、 array クラスはこのように機能します。 array クラスは安全である必要があるので、 array クラスは渡されたインデックスを評価する必要があります。 そのため、配列を使用すると若干のオーバーヘッドが追加されることになります。

unsafe コードを使用したコードは、 array クラスで生じるオーバーヘッドがないのでより高速な処理を行えます。 ここでは安全策を講じないで作業することになるので、 間違いを犯さないように注意しないとプログラムが不安定になります。 以下にコードを示します。


unsafe static int AddArrayUnsafe(int[] numbers)
{
   int result = 0;
   for (int it = 0; it < Iterations; it++)
   {
      fixed (int* pNumber = numbers)                    // 1
      {
         int* pCurrent = pNumber;                    // 2
         int* pLimit = pNumber + numbers.Length;     // 3
         while (pCurrent < pLimit)                   // 4
         {
            result += *pCurrent;                  // 5
            pCurrent++;                           // 6
         }
      }
   }
return result;
}

unsafe コードを記述する場合、 メソッドまたはクラスに unsafe 修飾子を使用する必要があります。 配列を処理するには、まず配列の最初の要素へのポインタを取得します。 最初の要素へのポインタの取得処理は、 ステートメント 1 で fixed キーワードを使用して行われています。 このブロック内では numbers 配列は移動されないことが分かるので、 ポインタを使用しても安全です。

ステートメント 2 では、 配列の先頭をローカル ポインタに代入しています。 fixed ステートメントで宣言したポインタは変更できないので、 このステートメントが必要になります。 ステートメント 3 は、配列のすべての要素の処理が完了したときのポインタの値を計算しています (つまり、最後の要素を処理すると、配列の要素数より 1 つ大きくなります)。 ステートメント 4 はこの条件を評価しています。

2 番目のポインタ変数と while ループを使用すると、 コードを少し単純化できます。 ただし、for ループを代わりに使用することもできます。

ループを設定できたので、 ステートメント 5 で加算に取り掛かることができます。 pCurrent は配列のある特定の要素を指しているので、 *pCurrent を使用してポインタが指している値を取得し、 その値を結果に加算できます。 最後に、ステートメント 6 はポインタを配列の次の要素に移動します。

ところで、 お気づきだと思いますが、 ポインタ変数のプレフィックスとして小文字の "p" を使用しました。 一般的には、 C# でハンガリー記法を使用することはあまり意味がないと思われます。 しかし、ポインタ型はめったに使用されず、 一般的な型とは異なる方法で機能するので、 ポインタ変数が一般的な型とは異なることを明確にするためにこの記法を使用しました。

このコードを実行には 1.55 秒かかります。 for ループを使用するコードよりも約 14 パーセント高速になりました。

まとめると、 理解しやすく、保守しやすいコードを例として挙げ、 これらのコードをより複雑なコードに置き換えました。 この処理では、 完全に信頼されたアセンブリとしてのみコードが実行されるようにしてきました。

元の安全なコードよりも 14 パーセント高速に処理を行うコードを作成するために、 これらすべてのことを行ってきました。これは価値のあることなのでしょうか?

この質問に対する答えは、アプリケーションによって異なるでしょう。 ただし、一般的には「いいえ、価値はありません。」という答えになるはずです。 大多数のアプリケーションでは、 単純なコードを記述して、 その後アルゴリズムを調整するのに時間を費やす方がより良い結果が得られます。 14 パーセントの向上が重要である場合、 unsafe を使うオプションが処理を高速にするかどうかを試す価値があります。

今月はここまでです。 来月は、イメージ処理などを考えることによって、 unsafe をさらに詳しく調べてみたいと思います。

リソース

いくつか新しい C# のリソースが利用できるようになりました。 Visual C#™ .NET のページが https://www.gotdotnet.com/team/csharp Leave-ms にあります。 C# チームは、次の C# 製品の企画を開始し、調査を行っています。 ぜひこのサイトを訪れて、調査にご協力ください。

最近、私は独自の C# ページを https://www.gotdotnet.com/team/ericgu Leave-ms に開設しました。 このページには、私のコラムへのリンクや、 他の興味深い情報へのリンク、およびすばらしいサンプルやデモがあります。