連載

C#入門

第5 C#のデータ型

(株)ピーデー
川俣 晶
2001/05/26


値型と参照型

 ここまで、System.Int32などを紹介してきたが、あえてクラスとは呼んでいなかった。メソッドなどがあるのだから、当然クラスだろうと思った人もいるだろう。ところが、実はこれらはクラスではないのである。そうではなく、これは「Structs」と呼ばれるものなのである(どういう日本語が定着するのか分からないので、ここでは「Structs」という表記で通す)。Structsは、Structsからの継承ができないなどの制約を除けば、見かけ上、クラスにそっくりである。宣言するためにclassキーワードではなく、structキーワードを使う。

 structというと、C/C++の経験者なら「ああ、それなら知っている」と思うかもしれないが、C#のstructは、C/C++のstructとはまったくの別物なので注意されたい。

 C#でのStructsとクラスの違いは、それが値型なのか、参照型なのかという機能面での違いである。

 値型とは、情報を引き渡すときに情報をコピーする方式を取るデータ型である。例えば、今回紹介するデータ型はみな値型である。これに対して参照型とは、データの実体がある場所を保存しておき、情報を引き渡す必要がある場合は、データの本体をコピーせず、データの場所だけを伝えるという方式である。この2つは、動作も微妙に違うが、それ以上に大きく違うのは、処理効率である。情報を渡すときにいちいちコピーすると言うのは、小さなデータなら問題ないのだが、大きなデータでは処理速度の低下という問題を招いてしまう。逆にデータが十分に小さいとすると、データの場所を渡すと言うような回りくどい方法を取るよりも、データそのものをコピーする方が素早い、と言うこともある。そのため、どちらが正解と言うことはない。主にデータ量という観点で使い分けることが好ましいものである。

 さて、処理の効率が違うといっても、具体的にどれぐらいの差が出るのだろうか。同じ処理をクラスとStructsで処理する一例を以下に示す。まずはクラスを使った例から。

  1: namespace ConsoleApplication7
  2: {
  3:   using System;
  4:
  5:   public class Test
  6:   {
  7:     public int v;
  8:   }
  9:   public class Class1
 10:   {
 11:     public static int Main(string[] args)
 12:     {
 13:       Console.WriteLine( DateTime.Now );
 14:       int count = 10000000;
 15:       Test [] test = new Test[count];
 16:       for( int i=0; i<count; i++ )
 17:       {
 18:         test[i] = new Test();
 19:         test[i].v = i;
 20:       }
 21:       int sum = 0;
 22:       for( int i=0; i<count; i++ )
 23:       {
 24:         sum += test[i].v;
 25:       }
 26:       Console.WriteLine( DateTime.Now );
 27:       return 0;
 28:     }
 29:   }
 30: }
クラスを使った場合の実行性能を調査するサンプル・プログラム
参照型であるクラスを使用して実行性能を調査する。具体的には、整数型変数を1つ含むクラスを宣言し、このクラスを要素とする1000万個の配列を作って、要素番号を変数に代入する。その後、全要素のクラス変数の値を加算していく。
5:
int型変数を1つ含むTestクラスの宣言。
13:
「DateTime.Now」は現在時刻を返す。プログラムが実行を開始されたら、まずはこのプロパティを表示し、処理を開始する直前の時刻を表記する。
14:
今回宣言する配列の要素数を保持する変数。今回は1000万とした。
15:
1000万個の要素を持つTestクラスの配列を作成し、変数「test」に代入する。
16〜20:
test配列の各要素となるTestクラスを作成し、そのインスタンス変数「v」にカウンタの値を代入する。
21:
計算処理に使用する整数型変数。ただし今回は計算処理の実行性能を調べるのが目的なので、計算の内容自体には意味はない。
22〜25:
test配列の各要素であるTestオブジェクトのv変数の値を次々と加算し、結果を変数「sum」に代入していく。
26:
処理終了の時刻を表示する。

 処理内容は単純である。整数を1個だけ保持するクラスを宣言して、それを1000万個持つ配列を確保して、中に数値を入れて、シンプルな計算を実行するだけである。ちなみに計算の内容に意味はない。

 13行目と26行目のDateTime.Nowは、現在時刻を返すプロパティである。これを使って、処理前と処理後の時刻を表示させ、処理に要した時間を調べようというわけである。

 これを実行した結果は以下のとおりである。

サンプル・プログラムの実行結果
クラスを使用して処理を行った場合には、16秒の時間がかかった。ただしこのプログラムの実行中、激しいスワップが発生した。したがった今回の結果は、CPUでの計算処理時間だけでなく、ハードディスクへのスワップにかかっている時間も含まれている。逆にいえば、参照型を使用するクラスの方法では、メモリも大量に消費されるということである。

 デバッグ・ビルド(デバッグ用としてコンパイルした実行ファイル)での所要時間は約16秒である。今回のテストに使用したマシンは、メモリを256Mbytesしか搭載していなかったので、プログラムの実行中に激しいスワップも発生した。参照型を使用するクラスの方法では、メモリ消費量も莫大なのである。

 では、Structsを使うとどうなるだろうか。以下にStructsを使用する形で書き直したソースを示す。

  1: namespace ConsoleApplication7
  2: {
  3:   using System;
  4:
  5:   public struct Test
  6:   {
  7:     public int v;
  8:   }
  9:   public class Class1
 10:   {
 11:     public static int Main(string[] args)
 12:     {
 13:       Console.WriteLine( DateTime.Now );
 14:       int count = 10000000;
 15:       Test [] test = new Test[count];
 16:       for( int i=0; i<count; i++ )
 17:       {
 18:         test[i] = new Test();
 19:         test[i].v = i;
 20:       }
 21:       int sum = 0;
 22:       for( int i=0; i<count; i++ )
 23:       {
 24:         sum += test[i].v;
 25:       }
 26:       Console.WriteLine( DateTime.Now );
 27:       return 0;
 28:     }
 29:   }
 30: }
Structsを使用した場合の実行性能を調査するサンプル・プログラム
5行目の宣言で「class」の代わりに「struct」としていることを除けば、基本的には、前出のクラスを使用するサンプルと同等である。

 これを実行した結果は以下のとおりである。

サンプル・プログラムの実行結果
プログラムでの処理自体はたいして変わらないにもかかわらず、値型であるStructsを使うと、クラスの例では16秒もかかっていた処理が、わずか1秒で終わっている。さらに言えば、Structsを使う場合には、18行目で行っているオブジェクトの作成も不要である。これを省略すれば、さらに実行速度は向上する。

 デバッグ・ビルドでの実行時間は、わずか1秒。クラスを用いた場合の16秒とは桁違いである。しかも、実はこのソースコードの18行目は、実はなくてもまったく問題ないものである。これを取り去れば、差はもっと開く。

 どうして、これほど大きな差が付くのか。その理由は、処理の過程で確保されるメモリの個数の差にある。クラスを用いる場合、1個のインスタンスは、1つの独立したメモリ領域として確保される。つまり、メモリ確保という処理が1000万回実行されるのである。これに対して、Structsの場合は、配列を作成した時点で、1000万個分のStructsを納めるたった1個の巨大なメモリを確保している。つまり、小さな処理を1000万回行うか、大きな処理を1回行うかの差が、この結果なのである。この違いを図示すると次のようになる。





クラスとStructsでの確保されるメモリ領域の違い
クラスを用いた場合には(図[上])、各インスタンスごとに独立したメモリ領域が確保され、値が格納される。配列の各要素はそれぞれのメモリ領域への参照を保持する。これに対してStructsを用いた場合は(図[下])、配列を作成した時点で、要素数分のStructsを納めるための巨大なメモリ領域が確保され、その中に値が格納される。

 実はStructsは、C#がJavaに対して先進的と言える機能の1つである。Javaには、C#のクラスに相当する機能しかなく、そのため、小さなオブジェクトからなる巨大な配列を使用するような処理を効率的に行うのが困難である。巨大なメモリを1個だけ確保して、これを無数のインスタンスが利用することはC++でも可能なことであり、C#がそれを実現したことは、C#がJavaではなくC++の進化形と考えれば素直に納得できる。

 反面、Structsの利用は危険も伴う。Structsは、制限の厳しいクラスのようなものだが、クラスそのものではない。クラスと思い込んで扱ってしまうと、思わぬトラブルが起きる場合もある。例えば、巨大なStructsを作成してしまうと、Structsは値型なので、別の変数に代入する場合などには、丸ごと中身をコピーする羽目になり、処理効率を著しく落とす。Structsはツボにはまれば効率アップできるが、一歩間違えると処理効率を大きく下げかねない危険があるという事実をよく認識しておいていただきたい。


 INDEX
  C#入門 第5回 C#のデータ型
    1.整数型からメソッドを呼ぶ
  2.値型と参照型
    3. ボクシング(boxing)
 
「C#入門」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間