読者です 読者をやめる 読者になる 読者になる

ユーザー定義のデフォルトコンストラクタと配列の初期化

.NET

(多くの) .NET 言語で,構造体にユーザー定義のデフォルトコンストラクタを作れないのはなんでよ? という話.
http://pc12.2ch.net/test/read.cgi/tech/1245836827/949-
自分の理解としては >>964 の人と同じ.
.NET アセンブリの仕様としては,構造体にユーザー定義のデフォルトコンストラクタを定義することは可能.ただ,仮に定義したとしても配列の初期化では呼び出されないという点だけで十分に罠過ぎる.メジャーどころの .NET 言語は,言語レベルで禁止ということで個人的には納得済み.
ちなみに値型配列について,ユーザー定義のデフォルトコンストラクタを適用したい場合は,Array.Initialize という専用メソッドを呼び出してやるか,勝手に Array.Initialize を挿入してくれる言語とコンパイラが必要.
ついでに 0 初期化についても少し.
0 初期化された巨大なデータ構造が必要なとき,もし OS にゼロページを要求できるなら,データ構造にゼロページを割り当てて,かつ必要になるまでメモリアクセスをしないのが御利益が多め.
以下のふたつのコードは,buf と buf2 の中身は同じでも,割り当てられたページの状態には大きな違いがある.

const int bufSize = 1024*1024*512;
char* buf = (char*)VirtualAlloc(NULL, bufSize, MEM_COMMIT, PAGE_READWRITE);

char* buf2 = new char[bufSize];
SecureZeroMemory(buf2, bufSize);

buf については,VirtualAlloc 直後の段階ではまだワーキングセットは増えていない.512 MB 分のゼロページが割り当てられただけ.一方 buf2 については SecureZeroMemory がメモリ書き込みを行うので,確実に 512 MB のダーティー領域を作り出してしまう.仮にこの SecureZeroMemory の直後にページライタが動き出すだすと,buf2 の中身である大量のゼロは律儀にページファイルへ書き出されることに*1
CLR はこの辺がしっかりしていて,Large Object Heap に取られるような大きな構造体配列を作成すると,初期状態でゼロページになっている模様.C++/CLI で実験してみる.

#using <System.dll>
#using <System.Drawing.dll>
#include <atlbase.h>
#include <atltypes.h>

using namespace System;
using namespace System::Drawing;
using namespace System::Diagnostics;

int main() {
  const int elements = 1024*1024*16;

  Process^ process = Process::GetCurrentProcess();

  process->Refresh();
  Console::WriteLine(L"WS: {0}", process->WorkingSet64);
  Console::WriteLine(L"new CPoint[elements]");

  CPoint* ps = new CPoint[elements];
  process->Refresh();
  Console::WriteLine(L"WS: {0}", process->WorkingSet64);

  Console::WriteLine(L"gcnew array<Point>(elements)");
  array<Point>^ ps2 = gcnew array<Point>(elements);
  process->Refresh();
  Console::WriteLine(L"WS: {0}", process->WorkingSet64);

  for (int i = 0; i < elements; ++i) {
    ps2[i].X = 0;
    ps2[i].Y = 0;
    if (i % (1024*1024*1) == 0) {
      process->Refresh();
      Console::WriteLine(L"WS: {0} (index = {1})", process->WorkingSet64, i);
    }
  }

  // call IDisposable.Dispose
  delete process;

  delete[] ps;

  String^ s = Console::ReadLine();
  return 0;
}

結果.

WS: 9404416
new CPoint[elements]
WS: 144723968
gcnew array<Point>(elements)
WS: 145436672
WS: 145477632 (index = 0)
WS: 153870336 (index = 1048576)
WS: 162471936 (index = 2097152)
WS: 171114496 (index = 3145728)
WS: 179765248 (index = 4194304)
WS: 188407808 (index = 5242880)
WS: 197050368 (index = 6291456)
WS: 205701120 (index = 7340032)
WS: 214343680 (index = 8388608)
WS: 222998528 (index = 9437184)
WS: 231641088 (index = 10485760)
WS: 240291840 (index = 11534336)
WS: 248938496 (index = 12582912)
WS: 257589248 (index = 13631488)
WS: 266231808 (index = 14680064)
WS: 274882560 (index = 15728640)

ATL の CPoint を使うと,配列作成直後にいきなりワーキングセットが増加しているのに対し,System::Drawing::Point は配列要素へのアクセスによって順次ワーキングセットが増えていく.

*1:ただし Windows Vista 以降のページライタは,全てゼロであるページをゼロページに差し替えるようになった.[http://d.hatena.ne.jp/NyaRuRu/20071025/p1:title=ホワイトペーパー: Windows のメモリ管理の進歩 - NyaRuRu の日記] 参照のこと