- PR -

構造体とクラスの選択

投稿者投稿内容
ひろし
会議室デビュー日: 2003/03/18
投稿数: 13
投稿日時: 2008-09-23 15:02
概要
配列およびジェネリックコレクション(System.Collections.Generic名前空間)の要素を設計する場合でクラスと構造体の両方を選択できる場合、パフォーマンスを考慮すると要素を格納する箱として構造体とクラスのどちらを使うのがより適切でしょうか?設計時に(a)(b)(c)が明確である場合、プロファイリングで確かめなくても設計段階で判断を下すことができますか?あるいは、そもそも、パフォーマンスを基準に検討すること自体がバランスを欠く、つまり、枝葉末節に囚われていますか?

(a)ジェネリックコレクションの種類
(b)要素のサイズ
(c)要素の数

質問
Q1.配列の場合、(b)(c)に関係なくクラスより構造体のほうが常に有利ですか?

Q2.CLR標準のジェネリックコレクション(例 List<T> Queue<T> Dictionary<T>等)についてガイドライン等、有用な情報源はありますか?

質問の背景
これまで、構造体とクラスの両方が選択可能な場合、値型である構造体のほうが軽量な感じがして無頓着に構造体を選択していました。しかし、本当は(a)(b)(c)を元に適切な判断を行い、使い分ける必要があるのではないかと思い質問しました、
Azulean
大ベテラン
会議室デビュー日: 2008/01/04
投稿数: 123
お住まい・勤務地: 大阪府
投稿日時: 2008-09-23 21:16
とりあえず、MSDNで関連しそうなページをリンクしておきます。

クラスまたは構造体の選択
http://msdn.microsoft.com/ja-jp/library/ms229017.aspx
ジェネリックの利点
http://msdn.microsoft.com/ja-jp/library/b5bx6xee.aspx

※これ以降の内容はきっちりとドキュメントで裏付けを取ってないので、間違っているかもしれません。

Q1:
要素のサイズが大きいと配列やジェネリックコレクションの要素を取り出すとき、あるいは格納するときのコピーコストが大きいのではないでしょうか。
(配列の場合は値型の参照を取得できるような特殊なパターンもありますが、これは除外して述べています)
動的に要素数が変わるパターンの場合、サイズが増えるときに重くなるのかな?

[ メッセージ編集済み 編集者: Azulean 編集日時 2008-09-23 21:18 ]
Tdnr_Sym
ぬし
会議室デビュー日: 2005/09/13
投稿数: 464
お住まい・勤務地: 明石・神戸
投稿日時: 2008-09-23 23:33
こんばんは。

このスレ主さんの質問を読んでいて
コンピュータサイエンスの基礎って大事だなぁと考えさせられました。
先週これ↓を読んだことも関係しているのですが…
システムエンジニア (Wikipedia)より引用
引用:

インドや韓国、モンゴル、その他欧米ではIT企業は知識集約的な産業であり、就職するためには大学で情報工学を専攻し卒業する必要がある。日本の上流工程に相当するポジションに就く者は例外なく、コンピュータサイエンスの基礎やC言語や関数型言語を修得している
日本では労働集約的な受託開発が中心であり、大学レベルの知識は必要とされない。システムエンジニアは資格独占業務ではないので、これといった資格がなくても仕事を受けることはできる。



と、余談をさせていただき
ご質問のほうは、適当にリンクさせてもらいます。

引用:

ひろしさんの書き込み (2008-09-23 15:02) より:
Q1.配列の場合、(b)(c)に関係なくクラスより構造体のほうが常に有利ですか?



参照型と値型の違いはなんでしょう?なぜ「参照」を使用するのでしょう? 参照 (情報工学) (Wikipedia)

引用:

Q2.CLR標準のジェネリックコレクション(例 List<T> Queue<T> Dictionary<T>等)についてガイドライン等、有用な情報源はありますか?



Queue -> キュー (Wikipedia)
Dictionary -> ハッシュテーブル (Wikipedia)
LinkedList -> 連結リスト (Wikipedia)
ひろし
ぬし
会議室デビュー日: 2002/09/16
投稿数: 390
お住まい・勤務地: 兵庫県
投稿日時: 2008-09-24 20:46
ご回答ありがとうございます。

実はこの質問をする前に、ご指摘のリンク先を見ていました。
そして「インスタンスのサイズが 16 バイト未満である。」の記述を見つけ、驚きました。

★クラスまたは構造体の選択
http://msdn.microsoft.com/ja-jp/library/ms229017.aspx

そして、下記の文献も読み返しました。
「プログラミングVisual C# 2005」(第5章配列とコレクション)日経BPソフトプレス
下記の基本原理は(一応)理解しているつもりです。
・スタックとヒープの違い(参照型→ヒープに格納、値型→参照に格納)
・値型と参照型とで、コピーに必要なコストが違う。

話を本題に戻します。構造体は非常に小さい(16バイト未満→intならたった4個分)サイズのものしか意味を持たないのか?常にそうだとすれば、構造体の出番は非常に少なくなります。私はこれまで構造体をジェネリックコレクションの要素として積極的に使ってきたのでかなり動揺してしまいました。
そして次の疑問が湧きました。果たして配列やジェネリックコレクションの要素であっても単純に上記の条件で判断すべきなのか?
例えば配列の場合は、構造体で要素を構成すればメモリ上ではひと固まりになります。一方、クラスで要素を構成すればはメモリ上で要素の数だけ分散してしまいます。後者のほうが、GCに大きな負荷をかけてしまうような気がします。では、配列では無く、FCLのジェネリックコレクションについてはどうでしょうか?多分配列より問題が複雑で、FCLの実装にも左右される可能性も考えられます。そうだとすれば、ジェネリックコレクションの種類ごとに上記の条件とは異なる基準が必要になるような気がします。
Jitta
ぬし
会議室デビュー日: 2002/07/05
投稿数: 6267
お住まい・勤務地: 兵庫県・海手
投稿日時: 2008-09-24 22:02
引用:

ひろしさんの書き込み (2008-09-23 15:02) より:
Q1.配列の場合、(b)(c)に関係なくクラスより構造体のほうが常に有利ですか?

Q2.CLR標準のジェネリックコレクション(例 List<T> Queue<T> Dictionary<T>等)についてガイドライン等、有用な情報源はありますか?


Q1
 次のようなサンプルを実行してみればわかります。
コード:
public struct A
{
    public string name;
}

public class AList : List<A>
{
}

main()
{
    AList l = new AList();
    for (int i = 0; i < 10; i++) {
        l.Add(new A());
        // 「l[i].name = i.ToString();」ではない理由は?
        A r = l[i];
        r.name = i.ToString();
    }
    foreach (A a in l) {
        // この出力が、どうなるかに注目
        System.Diagnostics.Debug.WriteLine(a.name);
    }
    // これとの違いを考えよう
    A[] arr = new A[10];
    for (int i = 0; i < arr.Length; i++) {
        arr[i] = new A();
        arr[i].name = i.ToString();
    }
    foreach (A a in arr) {
        System.Diagnostics.Debug.WriteLine(a.name);
    }
}


これの謎は、ボックス化とボックス化解除 (C# プログラミング ガイド)<microsoft.com>あたりで。


Q2
ジェネリック クラスの、どのクラスを使うかという問題でしょうか。つまり、「List と Queue と、どちらを使えばよいか」という問題でしょうか。それならば、Tdnr_Symさんのリンク先で。
ジェネリックそのものについてなら、MSDN ライブラリでは、こちら→ジェネリック (C# プログラミング ガイド)<microsoft.com>

それとも、「ジェネリック コレクションでは、クラスまたは配列のどちらがよいか」ということでしょうか。こっちの意図であれば、Q1の内容から明らかです。

※ 20:46 の投稿以前に用意していたもの
Jitta
ぬし
会議室デビュー日: 2002/07/05
投稿数: 6267
お住まい・勤務地: 兵庫県・海手
投稿日時: 2008-09-24 22:11
引用:

ひろしさんの書き込み (2008-09-24 20:46) より:

例えば配列の場合は、構造体で要素を構成すればメモリ上ではひと固まりになります。一方、クラスで要素を構成すればはメモリ上で要素の数だけ分散してしまいます。後者のほうが、GCに大きな負荷をかけてしまうような気がします。


関係ありません。
自分でメモリ管理をしてみれば、わかります。C言語の教科書的参考書なら、基本的なメモリ管理の方法が書いてあると思います。

じゃなかった。「一塊」と、「分散」ね。
配列にすると、要素数を増加させられないですよね。

引用:

では、配列では無く、FCLのジェネリックコレクションについてはどうでしょうか?多分配列より問題が複雑で、FCLの実装にも左右される可能性も考えられます。そうだとすれば、ジェネリックコレクションの種類ごとに上記の条件とは異なる基準が必要になるような気がします。


先の投稿の、サンプルコードを打ち込んで見てください。コレクションの種類を変えるのは、発展問題と言うことで。

[ メッセージ編集済み 編集者: Jitta 編集日時 2008-09-24 22:13 ]
ひろし
ぬし
会議室デビュー日: 2002/09/16
投稿数: 390
お住まい・勤務地: 兵庫県
投稿日時: 2008-09-27 16:17
ご回答ありがとうございます。

Jittaさんのサンプルについての解釈を試みました。

 構造体は値型なので、値そのものがコピーされます。コピー先であるに値を代入してもコピー元が書き換えられることはありません。したがって、l(エル)は代入後もnullのままである。lに値を設定したい場合は、lを参照型であるクラスに変更する。コンストラクタで値を設定する。lに直接代入する。等を検討する必要があります。
 構造体とクラスではコピー操作の振る舞いが異なるため注意が必要で、目的に応じて両者を使い分けます。

 さて、クラスと構造体の振るまいの違いが問題にならない次のような場面で、パフォーマンスを考慮しなければならない場合、構造体とクラスのどちらするべきか迷います。サンプルを下記に示します。サンプルでは配列、List<T>、Queue<T>について、要素を作成し、取り出しています。

 サンプルについて、指摘されたリンク先のルールについて考察します。
思い違いや見落とし等があればご指摘いただければ幸いです。

クラスまたは構造体の選択
http://msdn.microsoft.com/ja-jp/library/ms229017.aspx

型が次に挙げるすべての特性を持たない場合、構造体は定義しません。
(A)プリミティブ型 (整数、倍精度浮動小数点数など) に似た単一の値を論理的に表す。
(B)インスタンスのサイズが 16 バイト未満である。
(C)変更できない。
(D)頻繁にボックス化する必要がない。

 フィールドをdecimal型2個にしたのには意図があります。ClassAとStructAははdecimal型のフィールドを2個持っており32byteを占有します。(B)を満たさないため、構造体よりクラスが望ましいと結論できます。もし、フィールドがdecimal型2個では無くint型2個であったなら占有メモリは8byteで済み(A)(B)(C)を満たすことになり、(D)を満たすような使い方であれば構造体が望ましいと結論できます。

 それでは、対象が単体のオブジェクトでは無く、配列の要素である場合も同じルールが適用して本当に良いのでしょうか?サンプルのclassArrayとstructArrayについてヒープの状態を比較してみます。classArrayとstructArrayは下記のように格納されると思います。(正しいでしょうか?)

★classArrayの場合

(1)ヒープ上に参照先のアドレスが格納された400byteの領域(アドレス4byte×100要素)が1個確保される。
(2)ヒープ上に各クラスのフィールドの格納場所として32byteの領域が100個確保される。(3)フィールドにアクセスする場合、配列への参照とクラスへの参照の2回の参照をたどる必要がある。
(4)GCは(1)と(2)の合計101個の領域を管理する。
(5)配列の初期化時には要素ごとに領域を確保する必要がある。

★structArrayの場合

(1)ヒープ上に構造体のフィールドが格納された3200byteの領域(32byte×100要素)が1個確保される。
(2)フィールドにアクセスする場合、配列への参照を1回たどる必要がある。
(3)GCは1個の領域を管理する。
(4)配列の初期化時の領域を確保は1回である。

 フィールドにアクセスするためにはclassArrayの方がstructArrayより参照が1回余計にかかります。また、GCにとってメモリ上で領域が要素の数だけ分散しているclassArrayの方が1箇所で済んでいるstructArrayより負荷が大きいように思えます。それから、要素を生成する場合もclassArrayは要素ごとに領域を新たに確保しなければならないため1度で済むstructArrayより多くの手間がかかりそうです。つまり、対象が配列の要素の場合、配列特有の事情も考慮しなければいけないのではないか?対象が単体のオブジェクトの場合と配列の要素の場合とでは事情が異なるのでは無いか?

 また、配列以外のジェネリックコレクション、例えばList<T>やQueue<T>等については単体の場合と同じルールが適用できるのでしょうか?あるいは各ジェネリックコレクション特有の実装を反映した個別のルールが必要になるのでしょうか?

 私は構造が単純な配列を例にとって比較を行いましたが、配列だけが特異的で配列以外、つまり単体のオブジェクトとジェネリックコレクションは単体と同じルールが適用できるのでしょうか?

 それとも、「いやいやそんなことは無い、配列の要素、あるいはどんなジェネリックコレクションの要素であっても元を正せば単体のオブジェクトが集合しただけだから、単体のオブジェクトに適用されるルールを無条件に適用すれば良い。」のでしょうか?

やはり
・単体の場合のルール
・配列の場合のルール
・ジェネリックコレクションの場合のルール
は3つとも同じなのでしょうか?

 同じである場合、私の勘違いの原因も併せてご指摘いただければありがたいです。

【サンプル】

using System;
using System.Collections.Generic;

namespace ConsoleApplication1
{
// クラスでも構造体でも同じ振る舞いをする。
// 配列、List<T>、Queue<T>の要素として
// ClassAとStructAのどちらがより適切だろうか?
//
// 要素のサイズ=32byte 要素数=100

// クラスによる箱
public class ClassA
{
private decimal _param1; // 16byte
private decimal _param2; // 16byte

public ClassA(decimal param1, decimal param2)
{
_param1 = param1;
_param2 = param2;
}

public decimal Param1 { get { return _param1; } }
public decimal Param2 { get { return _param2; } }
}

// 構想対による箱
public struct StructA
{
private decimal _param1; // 16byte
private decimal _param2; // 16byte

public StructA(decimal param1, decimal param2)
{
_param1 = param1;
_param2 = param2;
}

public decimal Param1 { get { return _param1; } }
public decimal Param2 { get { return _param2; } }
}

class Program
{
static void Main(string[] args)
{
const int ARRAY_SIZE = 100;

// 配列の場合クラスと構造体のどちらを選択するべきか
ClassA[] classArray = new ClassA[ARRAY_SIZE];
StructA[] structArray = new StructA[ARRAY_SIZE];

// List<T>の場合クラスと構造体のどちらを選択するべきか
List<ClassA> classList = new List<ClassA>();
List<StructA> structList = new List<StructA>();

// Queue<T>の場合クラスと構造体のどちらを選択するべきか
Queue<ClassA> classQueue = new Queue<ClassA>();
Queue<StructA> structQueue = new Queue<StructA>();

for (int index = 0; index < ARRAY_SIZE; index++)
{
classArray[index] = new ClassA(index, index);
structArray[index] = new StructA(index, index);

classList.Add(new ClassA(index, index));
structList.Add(new StructA(index,index));

classQueue.Enqueue(new ClassA(index,index));
structQueue.Enqueue(new StructA(index,index));
}

foreach (ClassA a in classArray)
{
System.Diagnostics.Debug.WriteLine(String.Format("classArray=({0},{1})", a.Param1, a.Param2));
}

foreach (StructA a in structArray)
{
System.Diagnostics.Debug.WriteLine(String.Format("structArray=({0},{1})", a.Param1, a.Param2));
}

foreach (ClassA a in classList)
{
System.Diagnostics.Debug.WriteLine(String.Format("classList=({0},{1})", a.Param1, a.Param2));
}

foreach (StructA a in structList)
{
System.Diagnostics.Debug.WriteLine(String.Format("structList=({0},{1})", a.Param1, a.Param2));
}

while (classQueue.Count > 0)
{
ClassA a = classQueue.Dequeue();
System.Diagnostics.Debug.WriteLine(String.Format("structQueue=({0},{1})", a.Param1, a.Param2));
}

while (structQueue.Count > 0)
{
StructA a = structQueue.Dequeue();
System.Diagnostics.Debug.WriteLine(String.Format("structQueue=({0},{1})", a.Param1, a.Param2));
}

}
}
}


Tdnr_Sym
ぬし
会議室デビュー日: 2005/09/13
投稿数: 464
お住まい・勤務地: 明石・神戸
投稿日時: 2008-09-27 21:06
こんばんは。

なんだか悩みすぎなのではと思いつつ、
こういう奥深い話は好きなので調査してみました。
とりあえず、使用メモリ量のみをプロファイラから探ってみました。

#ちなみに、Listの実装は、内部的に「配列」でデータを保持しており、サイズの自動拡張がなされます。
#Queueの代わりにLinkedListで調査しました。なぜQueueなのか意図がわからなかったので。


Widows Vista Ultimate(32ビット)
VisualStudio2008
CLR Profiler 2.0

--------------------------------------------
1.配列

構造体の場合、トータル3,212バイト
クラスの場合、トータル4,416バイト
 StructA[] 3,212bytes - 1instances
 ClassA[] 416bytes - 1instances
 ClassA 4,000byte - 100instances 40bytes
コード:

StructA[] structArray = new StructA[ARRAY_SIZE];
ClassA[] classArray = new ClassA[ARRAY_SIZE];
for (int index = 0; index < ARRAY_SIZE; index++)
{
classArray[index] = new ClassA(index, index);
structArray[index] = new StructA(index, index);
}




--------------------------------------------
2.List(初期量指定なし)

構造体の場合、トータル8,744バイト
クラスの場合、トータル5,144バイト
 List<StructA> 24bytes - 1instances
 StructA[] 8,720bytes - 7instances
 List<ClassA> 24bytes - 1instances
 ClassA[] 1,120bytes 7instances
 ClassA 4,000byte - 100instances 40bytes
コード:

List<ClassA> classList = new List<ClassA>();
List<StructA> structList = new List<StructA>();
for (int index = 0; index < ARRAY_SIZE; index++)
{
classList.Add(new ClassA(index, index));
structList.Add(new StructA(index, index));
}



--------------------------------------------
3.List(初期量指定あり)

構造体の場合、トータル3,248バイト
クラスの場合、トータル4,440バイト
 List<StructA> 24bytes - 1instances
 StructA[] 3,224bytes - 2instances
 List<ClassA> 24bytes - 1instances
 ClassA[] 416bytes 1instances
 ClassA 4,000byte - 100instances 40bytes
コード:

List<ClassA> classList = new List<ClassA>(ARRAY_SIZE);
List<StructA> structList = new List<StructA>(ARRAY_SIZE);
for (int index = 0; index < ARRAY_SIZE; index++)
{
classList.Add(new ClassA(index, index));
structList.Add(new StructA(index, index));
}



--------------------------------------------
4.LinkedList

構造体の場合、トータル5,228バイト
クラスの場合、トータル6,428バイト
 LinkedList<StructA> 28bytes - 1instances
 LinkedListNode<StructA> 5,200bytes - 100instances 52bytes
 LinkedList<ClassA> 28bytes - 1instances
 LinkedListNode<ClassA> 2,400bytes - 100instances 24bytes
 ClassA 4,000byte - 100instances 40bytes
コード:

LinkedList<ClassA> classLinkedList = new LinkedList<ClassA>();
LinkedList<StructA> structLinkedList = new LinkedList<StructA>();
for (int index = 0; index < ARRAY_SIZE; index++)
{
classLinkedList.AddLast(new ClassA(index, index));
structLinkedList.AddLast(new StructA(index, index));
}




[ メッセージ編集済み 編集者: Tdnr_Sym 編集日時 2008-09-27 22:28 ]

スキルアップ/キャリアアップ(JOB@IT)