トップ回答者
GCHandleでSystem.Stringを管理するより高速な方法

質問
-
現在マネージドオブジェクトをメンバに含まない値型にSystem.Stringを格納するためにGCHandleの形で保存してあります。
struct A{ private GCHandle _o; public String o{ get{ _o.Target as String; } } }
System.Stringを必要に応じてGCHandle.Target as System.Stringとしていあります。
System.String以外が入る可能性もありますがとりあえずはSystem.Stringのみです。
必要に応じてasでキャストするとstructで定義した値型のメンバにSystem.Stringを含むものの方が高速です。
struct B{ public String o; }
- 理由は恐らくobject型のtargetをキャストするオーバヘッド
- GCHandleを解して毎回アクセスするオーバヘッド
があると思います。
GCに対し指定したポインタはGC対象であることを伝えるのがGCHandleの役割だと思います。
高速化のためにオブジェクトを取り出すときにGCHandleを介さないで取得できる方法はないものでしょうか?
回答
-
うちでは64bit前提で 1.6GB (要素数800*1024*1024個)の Char 配列をバッファに使い、開始位置と長さを持つ構造体をもとにハッシュテーブルを構築してそれ上で CompairOrdinal 相当のコードポイントでの比較で文字列のユニーク化、および文字列検索などをやっていますが、実質オーバーヘッドになるような事が無く実装できています。(GITコンパイルされたIA86アセンブリコードで確認&パフォーマンスカウンタでのGC回数やGCでのCPU使用率をみて)
これを 32K要素数のChar配列(64KB)を複数利用して作っても間接参照が一段増えるだけで実現できそうに思えるし、そもそもGCチューニングではGCHandleの個数を減らす事が重要だと思うので、GCHandleを持つという作りそのものが変な気がします。
コードポイントでの比較以上のUnicode処理が必要になるとstringのインスタンスにしなきゃいけないとかありそうな気もしますけど、NLS Unicode APIを C++/CLIで呼びをかける形で書けばstringのインスタンス化もしないでいけそうな気がします。
こちらでの事例にすぎませんが、参考までに。
Kazuhiko Kikuchi- 回答としてマーク 和和和 2009年7月12日 16:11
-
ベンチマークはVirtualAllocについてとGCHandleについての2つのうちVirtualAllocのテストコードとテスト結果を伝えます。
以下は多分コピペでいけるでしょう。
using System; using System.Text; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Diagnostics; using System.Reflection; using System.Security; using System.Reflection.Emit; public enum FreeType { DECOMMIT=0x4000, RELEASE=0x8000 } public enum AllocationType { COMMIT=0x1000, PHYSICAL=0x400000, RESERVE=0x2000, RESET=0x80000, TOP_DOWN=0x100000, WRITE_WATCH=0x200000 } public enum eProtect:uint { PAGE_NOACCESS=0x0001, PAGE_READONLY=0x0002, PAGE_READWRITE=0x0004, PAGE_WRITECOPY=0x0008, PAGE_EXECUTE=0x0010, PAGE_EXECUTE_READ=0x0020, PAGE_EXECUTE_READWRITE=0x0040, PAGE_EXECUTE_WRITECOPY=0x0080, PAGE_GUARD=0x0100, PAGE_NOCACHE=0x0200, PAGE_WRITECOMBINE=0x0400 } public static class テスト { [SuppressUnmanagedCodeSecurity] [DllImport("kernel32")] public static extern bool GetSystemInfo(out SYSTEM_INFO lpSystemInfo); public struct SYSTEM_INFO { public Int32 dwOemId; public Int32 dwPageSize; public Int32 lpMinimumApplicationAddress; public Int32 lpMaximumApplicationAddress; public Int32 dwActiveProcessorMask; public Int32 dwNumberOfProcessors; public Int32 dwProcessorType; public Int32 dwAllocationGranularity; public Int32 dwProcessorLevel; public Int32 dwProcessorRevision; } public static SYSTEM_INFO SYSTEM_INFO変数; [SuppressUnmanagedCodeSecurity] [DllImport("kernel32")] public static extern IntPtr VirtualAlloc(IntPtr lpAddress,Int32 dwSize,AllocationType flAllocationType,eProtect flProtect); [SuppressUnmanagedCodeSecurity] [DllImport("kernel32")] public static extern bool VirtualFree(IntPtr lpAddress,Int32 dwSize,FreeType dwFreeType); public static void 初期化() { GetSystemInfo(out SYSTEM_INFO変数); 下位を残すANDビット=SYSTEM_INFO変数.dwAllocationGranularity-1; 上位を残すANDビット=~下位を残すANDビット; } const Int32 バイト数=1024*1024*1024; static void 解放(IntPtr p,Int32 ブロックバイト数) { if(VirtualFree(p,ブロックバイト数,FreeType.DECOMMIT)==false) { throw new ApplicationException(); } if(VirtualFree(p,0,FreeType.RELEASE)==false) { throw new ApplicationException(); } } static void 確保テスト(Int32 ブロックバイト数) { var ブロック数=バイト数/ブロックバイト数; var ブロック配列=new IntPtr[ブロック数]; var VirtualAlloc速度=Stopwatch.StartNew(); for(var a=0;a<ブロック数;a++) { ブロック配列[a]=VirtualAlloc(IntPtr.Zero,ブロックバイト数,AllocationType.RESERVE|AllocationType.COMMIT,eProtect.PAGE_READWRITE); } VirtualAlloc速度.Stop(); Debug.Print("VirtualAlloc:ブロックバイト数{0},ブロック数{1},{2}ms",ブロックバイト数,ブロック数,VirtualAlloc速度.ElapsedMilliseconds); var VirtualFree速度=Stopwatch.StartNew(); for(var a=0;a<ブロック数;a++) { var p=ブロック配列[a]; 解放(ブロック配列[a],ブロックバイト数); ブロック配列[a]=IntPtr.Zero; } VirtualFree速度.Stop(); Debug.Print("VirtualFree:ブロックバイト数{0},ブロック数{1},{2}ms",ブロックバイト数,ブロック数,VirtualFree速度.ElapsedMilliseconds); var r=new Random(1); //var 試行バイト数=1024*1024*1024; var 試行ブロック数=1000000; var VirtualAlloc_VirtualFreeランダム速度=Stopwatch.StartNew(); for(var a=0;a<試行ブロック数;a++) { var i=r.Next(ブロック数); var p=ブロック配列[i]; if(p==IntPtr.Zero) { ブロック配列[i]=VirtualAlloc(IntPtr.Zero,ブロックバイト数,AllocationType.RESERVE|AllocationType.COMMIT,eProtect.PAGE_READWRITE); } else { 解放(p,ブロックバイト数); ブロック配列[i]=IntPtr.Zero; } } VirtualAlloc_VirtualFreeランダム速度.Stop(); Debug.Print("VirtualAlloc_VirtualFreeランダム速度:ブロックバイト数{0},ブロック数{1},{2}ms",ブロックバイト数,ブロック数,VirtualAlloc_VirtualFreeランダム速度.ElapsedMilliseconds); for(var a=0;a<ブロック数;a++) { var p=ブロック配列[a]; if(p!=IntPtr.Zero) { 解放(p,ブロックバイト数); } } } static void 空きメモリ無駄なく確保テスト(Int32 ブロックバイト数) { var ブロックList=new List<IntPtr>(); var 空きメモリ無駄なく確保テストランダム速度=Stopwatch.StartNew(); while(true) { var p=VirtualAlloc(IntPtr.Zero,ブロックバイト数,AllocationType.RESERVE,eProtect.PAGE_READWRITE); if(p==IntPtr.Zero) break; ブロックList.Add(p); } 空きメモリ無駄なく確保テストランダム速度.Stop(); for(var a=0;a<ブロックList.Count;a++) { 解放(ブロックList[a],ブロックバイト数); } Debug.Print("空きメモリ無駄なく確保テストランダム速度:ブロックバイト数{0},ブロック数{1},確保できたバイト数{2},{3}ms",ブロックバイト数,ブロックList.Count,ブロックバイト数*ブロックList.Count,空きメモリ無駄なく確保テストランダム速度.ElapsedMilliseconds); } static void Main() { GetSystemInfo(out SYSTEM_INFO変数); for(var a=65536;;a<<=1) { 空きメモリ無駄なく確保テスト(a); 確保テスト(a); } } }
この結果は
空きメモリ無駄なく確保テストランダム速度:ブロックバイト数65536,ブロック数29781,確保できたバイト数1951727616,172ms VirtualAlloc:ブロックバイト数65536,ブロック数16384,77ms VirtualFree:ブロックバイト数65536,ブロック数16384,214ms VirtualAlloc_VirtualFreeランダム速度:ブロックバイト数65536,ブロック数16384,9226ms 空きメモリ無駄なく確保テストランダム速度:ブロックバイト数131072,ブロック数13928,確保できたバイト数1825570816,53ms VirtualAlloc:ブロックバイト数131072,ブロック数8192,34ms VirtualFree:ブロックバイト数131072,ブロック数8192,107ms VirtualAlloc_VirtualFreeランダム速度:ブロックバイト数131072,ブロック数8192,9303ms 空きメモリ無駄なく確保テストランダム速度:ブロックバイト数262144,ブロック数6948,確保できたバイト数1821376512,27ms VirtualAlloc:ブロックバイト数262144,ブロック数4096,16ms VirtualFree:ブロックバイト数262144,ブロック数4096,60ms VirtualAlloc_VirtualFreeランダム速度:ブロックバイト数262144,ブロック数4096,9636ms 空きメモリ無駄なく確保テストランダム速度:ブロックバイト数524288,ブロック数3464,確保できたバイト数1816133632,13ms VirtualAlloc:ブロックバイト数524288,ブロック数2048,8ms VirtualFree:ブロックバイト数524288,ブロック数2048,34ms VirtualAlloc_VirtualFreeランダム速度:ブロックバイト数524288,ブロック数2048,10746ms 空きメモリ無駄なく確保テストランダム速度:ブロックバイト数1048576,ブロック数1718,確保できたバイト数1801453568,6ms VirtualAlloc:ブロックバイト数1048576,ブロック数1024,4ms VirtualFree:ブロックバイト数1048576,ブロック数1024,22ms VirtualAlloc_VirtualFreeランダム速度:ブロックバイト数1048576,ブロック数1024,12700ms 空きメモリ無駄なく確保テストランダム速度:ブロックバイト数2097152,ブロック数849,確保できたバイト数1780482048,3ms VirtualAlloc:ブロックバイト数2097152,ブロック数512,3ms VirtualFree:ブロックバイト数2097152,ブロック数512,14ms VirtualAlloc_VirtualFreeランダム速度:ブロックバイト数2097152,ブロック数512,16781ms 空きメモリ無駄なく確保テストランダム速度:ブロックバイト数4194304,ブロック数417,確保できたバイト数1749024768,1ms VirtualAlloc:ブロックバイト数4194304,ブロック数256,1ms VirtualFree:ブロックバイト数4194304,ブロック数256,13ms VirtualAlloc_VirtualFreeランダム速度:ブロックバイト数4194304,ブロック数256,25298ms 空きメモリ無駄なく確保テストランダム速度:ブロックバイト数8388608,ブロック数202,確保できたバイト数1694498816,0ms VirtualAlloc:ブロックバイト数8388608,ブロック数128,0ms VirtualFree:ブロックバイト数8388608,ブロック数128,9ms VirtualAlloc_VirtualFreeランダム速度:ブロックバイト数8388608,ブロック数128,41185ms 空きメモリ無駄なく確保テストランダム速度:ブロックバイト数16777216,ブロック数96,確保できたバイト数1610612736,0ms VirtualAlloc:ブロックバイト数16777216,ブロック数64,0ms VirtualFree:ブロックバイト数16777216,ブロック数64,9ms VirtualAlloc_VirtualFreeランダム速度:ブロックバイト数16777216,ブロック数64,72376ms 空きメモリ無駄なく確保テストランダム速度:ブロックバイト数33554432,ブロック数45,確保できたバイト数1509949440,0ms VirtualAlloc:ブロックバイト数33554432,ブロック数32,0ms VirtualFree:ブロックバイト数33554432,ブロック数32,8ms スレッド 0x16a4 はコード 0 (0x0) で終了しました。 VirtualAlloc_VirtualFreeランダム速度:ブロックバイト数33554432,ブロック数32,134747ms 空きメモリ無駄なく確保テストランダム速度:ブロックバイト数67108864,ブロック数21,確保できたバイト数1409286144,0ms VirtualAlloc:ブロックバイト数67108864,ブロック数16,0ms VirtualFree:ブロックバイト数67108864,ブロック数16,8ms VirtualAlloc_VirtualFreeランダム速度:ブロックバイト数67108864,ブロック数16,261282ms 空きメモリ無駄なく確保テストランダム速度:ブロックバイト数134217728,ブロック数9,確保できたバイト数1207959552,0ms VirtualAlloc:ブロックバイト数134217728,ブロック数8,0ms VirtualFree:ブロックバイト数134217728,ブロック数8,8ms VirtualAlloc_VirtualFreeランダム速度:ブロックバイト数134217728,ブロック数8,512765ms 空きメモリ無駄なく確保テストランダム速度:ブロックバイト数268435456,ブロック数4,確保できたバイト数1073741824,0ms VirtualAlloc:ブロックバイト数268435456,ブロック数4,0ms VirtualFree:ブロックバイト数268435456,ブロック数4,8ms VirtualAlloc_VirtualFreeランダム速度:ブロックバイト数268435456,ブロック数4,1027149ms 空きメモリ無駄なく確保テストランダム速度:ブロックバイト数536870912,ブロック数1,確保できたバイト数536870912,0ms VirtualAlloc:ブロックバイト数536870912,ブロック数2,0ms 'System.ApplicationException' の初回例外が VirtualAllocの粒度.exe で発生しました。
という結果でした。
この結果から分かることはブロックサイズを大きくすれば容量あたりに必要な呼び出しオーバーヘッドが小さくなり、容量の効率は下がるということです。
その関係は素直でパラメータ設定により最適なバランスは算出できると思います。
断片化したメモリに対してはテストしていません。恐らくその場合は小さなブロックの容量の効率はいいでしょう。- 回答としてマーク 和和和 2009年7月12日 16:11
-
>コードポイントでの比較以上のUnicode処理が必要になるとstringのインスタンスにしなきゃいけないとかありそうな気もしますけど、NLS Unicode APIを C++/CLIで呼びをかける形で書けばstringのインスタンス化もしないでいけそうな気がします。
コードポイントってString.CompareOrdinalと同等のUnicode文字コードによる比較ってことですよね?
実はこれと同等の実装はしていましたが、Compareでいろいろな比較ができなかったのでSystem.StringをGCHandleで参照したいと思っていたのです。でも遅いというジレンマ
>NLS Unicode API
初耳なので調べました。C,C++でもポインタによる文字列形式からそういった複雑なメソッドを提供するものであると判断しました。
これが使えるのならば検討してみても良いと考えました。
参考になりますありがとうございました。- 回答としてマーク 和和和 2009年7月12日 16:11
-
ざっとしか読んでいませんが、マネージ・アンマネージコードの比較とされている箇所は、newで確保するか、stackallocで確保するかの違いですよね?
試しに、「マネージドのテスト」という名前の関数をnewで確保し、fixedで固定し、固定されたポインタを相手に操作したら、「アンマネージドのテスト」という名前の関数と同程度の時間になるように見受けられました。
恐らくは、配列をstelem命令でアクセスするか、ポインタベースでアクセスするかの差ではないでしょうか?
http://msdn.microsoft.com/ja-jp/library/system.reflection.emit.opcodes.stelem(VS.80).aspx
マネージ配列にアクセスする場合は、通常、Nullチェックやインデックスの範囲外チェック等の各種チェックが入ります。
繰り返し回数が膨大になると、このコストはそこそこ重くなります。
このコストが気になる場合は、部分的にunsafeコンテキストにすることで対応することになります。(アンマネージとunsafeは微妙に違います)
ところで、VirtualAllocとどのように組み合わせているのでしょうか?
GCHandleを使ったところで、Stringのインスタンスはマネージメモリに存在し、GCHandle.ToIntPtr/FromIntPtrではマネージメモリへのアクセスのための数値(ハンドル?)をやりとりするだけだと考えられるため、そのデータ構造を表す領域はマネージメモリと、VirtualAllocによるアンマネージメモリの両方が存在して成り立つことになります。
このあたりは懸念するほどの有意の差にはなっていないということでしょうか。
(参考)
fixedで固定する際は下記のようにしました。なお、直接Stringはfixed指定できない型であったため、省略しています。
var 間接String配列_ = new 間接String[行数];
fixed (間接String* 間接String配列 = 間接String配列_)
{
※「マネージドのテスト」という名前の関数の中身
}
解決した場合は、参考になった返信に「回答としてマーク」のボタンを利用して、回答に設定しましょう(複数に設定できます)。- 回答としてマーク 和和和 2009年7月12日 16:11
すべての返信
-
何に使っているか?ですが、
オンメモリのデータベースエンジンを作っています。
表毎に異なる列を持つわけですが、以前は高速な独自の文字列クラスを使っていました。
しかしSystem.Stringのようなカルチャ情報を使った比較や多彩なメソッドをいちいち実装するのはたいへんです。
そのため現在はSystem.StringをGCHandleを列に埋め込むことで実現しています。
しかし単純なCompareOrdinalでもGCHandleのSystem.Stringは遅かったのです。
テストのために先に示した値型で比較してみましたが当然ですがGCHandleのが遅かったのです。
CompareOrdinalだけでも目に見えて遅かったのでこれを早くしたい言う要望でした。 -
何に使っているか?ですが、
申し訳ないですが、この要求のどこにGCHandleを使う必要があるのかが見えていません。
オンメモリのデータベースエンジンを作っています。
表毎に異なる列を持つわけですが、以前は高速な独自の文字列クラスを使っていました。
しかしSystem.Stringのようなカルチャ情報を使った比較や多彩なメソッドをいちいち実装するのはたいへんです。
そのため現在はSystem.StringをGCHandleを列に埋め込むことで実現しています。
GCHandleを使わないと成り立たないところが知りたいのです。
(GCHandleを使わなくても良いのであれば、なぜ遅くなるGCHandleを使っているのかが分からなくなるため)
普通にSystem.Stringをメンバに持つと何が問題になるのでしょうか?
それとも持たせることができない何らかの理由があるのでしょうか?
解決した場合は、参考になった返信に「回答としてマーク」のボタンを利用して、回答に設定しましょう(複数に設定できます)。 -
説明不足で申し訳ありませんでした。
GCHandleの必要性ですが、論理メモリは特に32bitOSは限りがあるのでVirtualAllocで64KB単位で確保しています。
64KBというのは最小予約粒度なので都合が良いサイズだからです。
64KBをリストで接続して表の行を格納しています。
このメモリはアンマネージのために直接Stringを格納することが出来ません。その為値型であるGCHandleで格納しています。
カタログ表に索引情報を格納する表があります。
ここにもマネージドオブジェクトである比較関数デリゲートを格納するためにGCHandleを使っています。これは頻繁に読み書きするわけではないのであまり問題にはなりませんがStringは多いので問題なのです。 -
はい。まさに64KBごとに呼び出しています。
mallocも最終的に呼び出すapiはそこに行き着くのでカーネルに手をつけない範囲では最も低レベルなメモリ管理だと思います。
クラスを必要なら動的に生成するのはやってみました。
通常のDBは起動し続けるのが前提のためDROP TABLEしてもカレントアセンブリから型を削除できません。
別ドメインに作るのはやってみましたがアクセスが遅かったです。
あとスケーラビリティがどれほどあるかテストして見ましたがList<T>などの.NETライブラリは何GBもあるデータを効率的に操作することは出来ないようです。
>なんていうか、アンマネージやGCHandleを使わないで実装した方が早いような…さすがにそういうことはありませんか?
これは十分に検証しましたが大量のデータになればなるほど独自メモリ管理との速度の差が開いていきます。
64bit環境はまだ試していませんがこの傾向は変わらないだろうと思います。 -
>>なんていうか、アンマネージやGCHandleを使わないで実装した方が早いような…さすがにそういうことはありませんか?
>これは十分に検証しましたが大量のデータになればなるほど独自メモリ管理との速度の差が開いていきます。メモリ確保・解放だけでなく、実際のシナリオに近いデータアクセスも含めてみました??
データアクセスを含めると、API 呼び出しの際のコンテクストスイッチのコスト以外にも
・VirtualAlloc() した領域へのデータ書き込みの際に発生するマーシャリング
・VirtualAlloc() した領域からのデータ読み込みの際に発生するマーシャリングなどのコストが結構かさんでくるように思うんですが。
独自にメモリ管理をするにしても、もっと大きな単位でガバッとマネージメモリを確保して、そこから自分で切りだしてくるような管理にした方が速いような気がします。
(最初の方で菊池さんが提示していたような手順) -
>データアクセスを含めると、API 呼び出しの際のコンテクストスイッチのコスト以外にも
これは正確にはゲートウェイコールですよね?
>メモリ確保・解放だけでなく、実際のシナリオに近いデータアクセスも含めてみました??
はいメモリ確保・解放はばらばらの順番でVirtualAlloc,VirtualFreeでやりました。
データアクセスに関しては64KBのブロックをそれぞればらばらのアドレスで確保して64B-1KBの行サイズとしてGCHandleのStringを==比較、CompareOrdinal呼び出し。
もう一つは同じく64KBのマネージドメモリを確保して64B-1KBのStringをメンバにもつ値型を==比較、CompareOrdinal呼び出し。
具体的なデータはすいません失いましたが前者はたしか2倍ほど遅かったです。
文字列の長さ自体は1~8文字程度でした。
たかだが2倍程度なら許容してもいいともいえるかもしれませんが気になります。
>・VirtualAlloc() した領域へのデータ書き込みの際に発生するマーシャリング
>・VirtualAlloc() した領域からのデータ読み込みの際に発生するマーシャリング
マーシャリングは発生しません。値型はSByte,Int16,Int32,Int64,Single,Double,Decimal,DateTime,Booleanはコストなしにやり取りできますよね。
問題はStringだけと考えています。
>独自にメモリ管理をするにしても、もっと大きな単位でガバッとマネージメモリを確保して、そこから自分で切りだしてくるような管理にした方が速いような気がします。
これやって見ました。大きな単位で確保すると容量あたりの確保速度は速くなりますが、論理アドレス(仮想アドレス)が枯渇してくると遅くなります。
さらに無駄なく使えなくなります。
64KBに整列されている、という前提を利用することで行える最適化もしているので変えることが出来ないという事情もあります。
確保単位を大きくするにはラージページが使えるなら試してみたいです。
例えばSQL Serverのページサイズ8KBです。最大64KBのエクステントもディスク上の理由からかバッファのアドレスの事情からかその単位で確保するのはそういった事情があるものと思います。
mallocでも大きなオブジェクトで64KBを超えるとVirtualAllocで確保するほうが効率的であるという文献をみたことがあります。ちょっとソースは忘れました。- 編集済み 和和和 2009年7月9日 5:22
-
>>データアクセスを含めると、API 呼び出しの際のコンテクストスイッチのコスト以外にも
>
>これは正確にはゲートウェイコールですよね?具体的には
・Windows API 呼び出しに必要な P/Invoke のオーバーヘッド
・Windows API 呼び出しに伴うユーザモード→カーネルモード遷移(+逆方向)のオーバーヘッドです。
>マーシャリングは発生しません。
>値型はSByte,Int16,Int32,Int64,Single,Double,Decimal,DateTime,Booleanはコストなしにやり取りできますよね。てことは、アンマネージメモリ上に配置した構造体イメージへのアクセスは、usafe コンテキストですべて手動(メンバ操作の際のオフセット計算なんかも自前)で行うってことですか?
>mallocでも大きなオブジェクトで64KBを超えるとVirtualAllocで確保するほうが効率的であるという文献をみたことがあります。ちょっとソースは忘れました。
確かに、一定サイズ以上の領域を確保する場合、下手に自分で管理するよりも VirtualAlloc した方が速いケースはあると思うんですが、敷居値が 64KB だったかどうかは僕も覚えてないですねぇ。
-
>データアクセスを含めると、API 呼び出しの際のコンテクストスイッチのコスト以外にも
ゲートウェイコール時(と戻る時)に必要になるコンテキストスイッチのつもりでした。意味は通じているようなのでどちらの表現でも構いません。
これは正確にはゲートウェイコールですよね?
>メモリ確保・解放だけでなく、実際のシナリオに近いデータアクセスも含めてみました??
VirtualFree()ってことはもしかして空きメモリはプールせず即座に解放してますか?
はいメモリ確保・解放はばらばらの順番でVirtualAlloc,VirtualFreeでやりました。
データアクセスに関しては64KBのブロックをそれぞればらばらのアドレスで確保して64B-1KBの行サイズとしてGCHandleのStringを==比較、CompareOrdinal呼び出し。
前者が遅かったのですか? それとも書き間違い…?
もう一つは同じく64KBのマネージドメモリを確保して64B-1KBのStringをメンバにもつ値型を==比較、CompareOrdinal呼び出し。
具体的なデータはすいません失いましたが前者はたしか2倍ほど遅かったです。
文字列の長さ自体は1~8文字程度でした。
たかだが2倍程度なら許容してもいいともいえるかもしれませんが気になります。
遅かった原因はGCHandleにあるのでしょうか? もちろんGCHandleも遅くする要因ですが、説明を読む限り独自に用意しているメモリマネージャにも原因がありそうな。
それから後者もマネージドメモリの確保とかせず、普通のclassで実装するとどうなんでしょう。
数GBまでスケールすることを知らなかったのでList<T>と書きましたが、まぁそんなときはさすがに別のデータ構造もまぜます。
というか数GBまで64KB単位でVirtualAlloc() / VirtualFree()を繰り返すのですか?
>独自にメモリ管理をするにしても、もっと大きな単位でガバッとマネージメモリを確保して、そこから自分で切りだしてくるような管理にした方が速いような気がします。
「枯渇」とか「無駄なく使えなく」ってことはコンパクションとかやってなさそう。
これやって見ました。大きな単位で確保すると容量あたりの確保速度は速くなりますが、論理アドレス(仮想アドレス)が枯渇してくると遅くなります。
さらに無駄なく使えなくなります。
もちろん実装が大変なのはわかりますが、やらないとページフォールトが増えますよね。
64KBに整列されている、という前提を利用することで行える最適化もしているので変えることが出来ないという事情もあります。
SQL Serverのページサイズは8KBですが、だからといってSQL Serverが8KB単位や64KB単位でVirtualAlloc()などのOS呼び出しはしていないでしょう。
確保単位を大きくするにはラージページが使えるなら試してみたいです。
例えばSQL Serverのページサイズ8KBです。最大64KBのエクステントもディスク上の理由からかバッファのアドレスの事情からかその単位で確保するのはそういった事情があるものと思います。
mallocでも大きなオブジェクトで64KBを超えるとVirtualAllocで確保するほうが効率的であるという文献をみたことがあります。ちょっとソースは忘れました。
malloc()やVirtualAlloc()などは、メモリ確保時にスレッド間ロックを発生させるので、マルチコアでのマルチスレッドではコストがかさみます。そのためOSから大きなブロックを確保した上で、それをスレッドごとのヒープに分け、各スレッドが独立して確保できるようにした方が効率がよ い、と言われています。
# FreeBSDのjemallocやgoogle mallocね。
いろいろ書きましたが、知識として知っているだけで、どれが性能に対して支配的なのかは実は知りません。なので的外れなことを言っていたらごめんなさい。 -
>VirtualFree()ってことはもしかして空きメモリはプールせず即座に解放してますか?
はい、即座に解放しています。そこらへんを上手く使いまわすことで管理のオーバヘッドをペイ出来るか疑問でしたが実行速度を見た限りそれほどかわらないということでした。勿論プーリングが下手であれば当然だとは思いますが。
データアクセスに関しては
(1)64B-1KBの行を64KBのブロック内に複数入れ、ブロックはばらばらのアドレスで複数VirtualAlloc確保して行にはGCHandleを入れておきます。GCHandle.Target as System.Stringを==比較、CompareOrdinal呼び出し。
(2)64B-1KBの行のStringをメンバにもつ値型を↑と同じ行数文newで確保しメンバを==比較、CompareOrdinal呼び出し。
(3)64B-1KBの行のGCHandleをメンバにもつ値型を↑と同じ行数文newで確保しGCHandle.Target as System.Stringを==比較、CompareOrdinal呼び出し。
具体的なデータはすいません失いましたが(1),(3)はほぼ同じ速度で(2)に比べ2倍ほど遅かったです。
つまり早さは
(1)=(3)<(2)
でした。
文字列の長さ自体は1~8文字程度でした。
たかだが2倍程度なら許容してもいいともいえるかもしれませんが気になります。>遅かった原因はGCHandleにあるのでしょうか? もちろんGCHandleも遅くする要因ですが、説明を読む限り独自に用意しているメモリマネージャにも原因がありそうな。
メモリは既に確保した上たでのアクセス速度測定であるということからGCHandleが原因であるらしいと判断しました。
>それから後者もマネージドメモリの確保とかせず、普通のclassで実装するとどうなんでしょう。
これはまだ試していません(試したかもしれませんが残っていません)。
>というか数GBまで64KB単位でVirtualAlloc() / VirtualFree()を繰り返すのですか?
そうですね。一定の粒度にするのは単純化や論理アドレスの断片化を防ぐ目的もあります。
それに行は64KBの「大きな」ブロックを確保して行の追加、削除を行うという動作のためそれなりにメモリプーリングが行われているといえるでしょう。>独自にメモリ管理をするにしても、もっと大きな単位でガバッとマネージメモリを確保して、そこから自分で切りだしてくるような管理にした方が速いような気がします。
>>これやって見ました。大きな単位で確保すると容量あたりの確保速度は速くなりますが、論理アドレス(仮想アドレス)が枯渇してくると遅くなります。
無駄になる、というのは例えば64KB単位で確保すれば64KB未満のメモリが無駄になる可能性があります。粒度が大きければそれだけ無駄になる量が多いということです。
>>さらに無駄なく使えなくなります。
>「枯渇」とか「無駄なく使えなく」ってことはコンパクションとかやってなさそう。
>もちろん実装が大変なのはわかりますが、やらないとページフォールトが増えますよね。
各表はメモリブロックをリストでつないでいます。行が空いたら最後のブロックの末尾行を開き場所に移動します。
つまり表1つにつき最大で64KBのメモリが無駄になる可能性がありますが、それなりに効率的になっています。.NETランタイムが64KBを超える単位でVirtualAllocしていれば断片化の影響を受けます。
>SQL Serverのページサイズは8KBですが、だからといってSQL Serverが8KB単位や64KB単位でVirtualAlloc()などのOS呼び出しはしていないでしょう。
枯渇に関しても論理アドレスの枯渇や断片化により使えない論理アドレスが増えるのを避けたいためブロック単位を64KBにしました。SQL Serverはディスクベースであるのでメモリ管理に関してはまた違った方法かもしれません。
>malloc()やVirtualAlloc()などは、メモリ確保時にスレッド間ロックを発生させるので、マルチコアでのマルチスレッドではコストがかさみます。そのためOSから大きなブロックを確保した上で、それをスレッドごとのヒープに分け、各スレッドが独立して確保できるようにした方が効率がよ い、と言われています。
現在マルチユーザを想定していますが「データベースロック」ということでシングルユーザで並列化可能な処理だけ並列化する予定です。例えばソート、同一表の別索引、別表の走査などです。
この場合VirtualAlloc内部は確かにシングルスレッド処理になってしまいますがこれは妥協します。
64KBというのは偶然では有りますがそこそこのサイズであると判断しております。メモリ管理というのはトレードオフが多い分野だと思います。アクセス方法、確保の規則から高性能であるが一定の制約を許容できることもあり汎用的なメモリマネージャが向かない例であると考えました。
64KBを超えるVirtualAlloc,VirtualFreeが果たして64KB単位でやるのに比べコスト的にどうか?
GCHandleと直接Stringとの速度差。
これを詳しくわかりやすいベンチマークをもう一度作ってみたいと思います。 -
>・Windows API 呼び出しに必要な P/Invoke のオーバーヘッド
>・Windows API 呼び出しに伴うユーザモード→カーネルモード遷移(+逆方向)のオーバーヘッド
このオーバーヘッドがが他の要因で正当化できると判断しました。
>てことは、アンマネージメモリ上に配置した構造体イメージへのアクセスは、usafe コンテキストですべて手動(メンバ操作の際のオフセット計算なんかも自前)で行うってことですか?
おっしゃるとおりです。
>>mallocでも大きなオブジェクトで64KBを超えるとVirtualAllocで確保するほうが効率的であるという文献をみたことがあります。ちょっとソースは忘れました。
>確かに、一定サイズ以上の領域を確保する場合、下手に自分で管理するよりも VirtualAlloc した方が速いケースはあると思うんですが、敷居値が 64KB だったかどうかは僕も覚えてないですねぇ。整列が64KBであることを利用しての最適化、断片化による確保不能を回避などのメリットを考慮してこのサイズにしていますので速度的に最良かはわかりません。
-
データアクセスに関しては
(1)64B-1KBの行を64KBのブロック内に複数入れ、ブロックはばらばらのアドレスで複数VirtualAlloc確保して行にはGCHandleを入れておきます。GCHandle.Target as System.Stringを==比較、CompareOrdinal呼び出し。
(2)64B-1KBの行のStringをメンバにもつ値型を↑と同じ行数文newで確保しメンバを==比較、CompareOrdinal呼び出し。
(3)64B-1KBの行のGCHandleをメンバにもつ値型を↑と同じ行数文newで確保しGCHandle.Target as System.Stringを==比較、CompareOrdinal呼び出し。
具体的なデータはすいません失いましたが(1),(3)はほぼ同じ速度で(2)に比べ2倍ほど遅かったです。
つまり早さは
(1)=(3)<(2)
でした。
文字列の長さ自体は1~8文字程度でした。
たかだが2倍程度なら許容してもいいともいえるかもしれませんが気になります。
この説明をせずに(3)だけ書かれますと「何をトンチンカンなことを」と誤解します。というかしました。
コードを見るべきではありますが、なんとなくGCHandleが原因に見えますね。
(1)についてですが、実際にはGCHandle.FromIntPtr(...).Targetですね? Pinnedで固定した上で、String*をアンマネージメモリに格納してしまえば、参照時にGCHandleを経由せずにアクセスできます。というか私からの一番最初の回答です。
この場合の欠点についても既にコメントしていて、GCがこのstringを移動したくても固定されたままになってしまう点です。string.Internを使うと移動できなくても構わなくなりますがそれはそれで別のコストが。
>というか数GBまで64KB単位でVirtualAlloc() / VirtualFree()を繰り返すのですか?
そうですね。一定の粒度にするのは単純化や論理アドレスの断片化を防ぐ目的もあります。
それに行は64KBの「大きな」ブロックを確保して行の追加、削除を行うという動作のためそれなりにメモリプーリングが行われているといえるでしょう。
データサイズが数GBとか64bitといった言葉が出てきていますから、その頃の10倍ぐらいの搭載メモリを期待していますよね。
もちろん、手元のVistaでも割り当て単位は64KBですので、予約粒度の点では効率的ですが。
思ったのは、VirtualAlloc()ではなくHeapAlloc()を使ってみることです。その際、同一ヒープに対して同時に呼び出さないことを保証した上で、HEAP_NO_SERIALIZEも指定します。
どっちがいいのかはわかりませんがw
あとは既に書きましたが、テーブルに対するクラス生成とクエリに対するILを生成をすることでマネージドな世界に閉じてしまう方法。もちろん別AppDomainでなく。
64KBを超えるVirtualAlloc,VirtualFreeが果たして64KB単位でやるのに比べコスト的にどうか?
GCHandleと直接Stringとの速度差。
これを詳しくわかりやすいベンチマークをもう一度作ってみたいと思います。
言いたい放題ですみません。 -
うちでは64bit前提で 1.6GB (要素数800*1024*1024個)の Char 配列をバッファに使い、開始位置と長さを持つ構造体をもとにハッシュテーブルを構築してそれ上で CompairOrdinal 相当のコードポイントでの比較で文字列のユニーク化、および文字列検索などをやっていますが、実質オーバーヘッドになるような事が無く実装できています。(GITコンパイルされたIA86アセンブリコードで確認&パフォーマンスカウンタでのGC回数やGCでのCPU使用率をみて)
これを 32K要素数のChar配列(64KB)を複数利用して作っても間接参照が一段増えるだけで実現できそうに思えるし、そもそもGCチューニングではGCHandleの個数を減らす事が重要だと思うので、GCHandleを持つという作りそのものが変な気がします。
コードポイントでの比較以上のUnicode処理が必要になるとstringのインスタンスにしなきゃいけないとかありそうな気もしますけど、NLS Unicode APIを C++/CLIで呼びをかける形で書けばstringのインスタンス化もしないでいけそうな気がします。
こちらでの事例にすぎませんが、参考までに。
Kazuhiko Kikuchi- 回答としてマーク 和和和 2009年7月12日 16:11
-
ベンチマークはVirtualAllocについてとGCHandleについての2つのうちVirtualAllocのテストコードとテスト結果を伝えます。
以下は多分コピペでいけるでしょう。
using System; using System.Text; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Diagnostics; using System.Reflection; using System.Security; using System.Reflection.Emit; public enum FreeType { DECOMMIT=0x4000, RELEASE=0x8000 } public enum AllocationType { COMMIT=0x1000, PHYSICAL=0x400000, RESERVE=0x2000, RESET=0x80000, TOP_DOWN=0x100000, WRITE_WATCH=0x200000 } public enum eProtect:uint { PAGE_NOACCESS=0x0001, PAGE_READONLY=0x0002, PAGE_READWRITE=0x0004, PAGE_WRITECOPY=0x0008, PAGE_EXECUTE=0x0010, PAGE_EXECUTE_READ=0x0020, PAGE_EXECUTE_READWRITE=0x0040, PAGE_EXECUTE_WRITECOPY=0x0080, PAGE_GUARD=0x0100, PAGE_NOCACHE=0x0200, PAGE_WRITECOMBINE=0x0400 } public static class テスト { [SuppressUnmanagedCodeSecurity] [DllImport("kernel32")] public static extern bool GetSystemInfo(out SYSTEM_INFO lpSystemInfo); public struct SYSTEM_INFO { public Int32 dwOemId; public Int32 dwPageSize; public Int32 lpMinimumApplicationAddress; public Int32 lpMaximumApplicationAddress; public Int32 dwActiveProcessorMask; public Int32 dwNumberOfProcessors; public Int32 dwProcessorType; public Int32 dwAllocationGranularity; public Int32 dwProcessorLevel; public Int32 dwProcessorRevision; } public static SYSTEM_INFO SYSTEM_INFO変数; [SuppressUnmanagedCodeSecurity] [DllImport("kernel32")] public static extern IntPtr VirtualAlloc(IntPtr lpAddress,Int32 dwSize,AllocationType flAllocationType,eProtect flProtect); [SuppressUnmanagedCodeSecurity] [DllImport("kernel32")] public static extern bool VirtualFree(IntPtr lpAddress,Int32 dwSize,FreeType dwFreeType); public static void 初期化() { GetSystemInfo(out SYSTEM_INFO変数); 下位を残すANDビット=SYSTEM_INFO変数.dwAllocationGranularity-1; 上位を残すANDビット=~下位を残すANDビット; } const Int32 バイト数=1024*1024*1024; static void 解放(IntPtr p,Int32 ブロックバイト数) { if(VirtualFree(p,ブロックバイト数,FreeType.DECOMMIT)==false) { throw new ApplicationException(); } if(VirtualFree(p,0,FreeType.RELEASE)==false) { throw new ApplicationException(); } } static void 確保テスト(Int32 ブロックバイト数) { var ブロック数=バイト数/ブロックバイト数; var ブロック配列=new IntPtr[ブロック数]; var VirtualAlloc速度=Stopwatch.StartNew(); for(var a=0;a<ブロック数;a++) { ブロック配列[a]=VirtualAlloc(IntPtr.Zero,ブロックバイト数,AllocationType.RESERVE|AllocationType.COMMIT,eProtect.PAGE_READWRITE); } VirtualAlloc速度.Stop(); Debug.Print("VirtualAlloc:ブロックバイト数{0},ブロック数{1},{2}ms",ブロックバイト数,ブロック数,VirtualAlloc速度.ElapsedMilliseconds); var VirtualFree速度=Stopwatch.StartNew(); for(var a=0;a<ブロック数;a++) { var p=ブロック配列[a]; 解放(ブロック配列[a],ブロックバイト数); ブロック配列[a]=IntPtr.Zero; } VirtualFree速度.Stop(); Debug.Print("VirtualFree:ブロックバイト数{0},ブロック数{1},{2}ms",ブロックバイト数,ブロック数,VirtualFree速度.ElapsedMilliseconds); var r=new Random(1); //var 試行バイト数=1024*1024*1024; var 試行ブロック数=1000000; var VirtualAlloc_VirtualFreeランダム速度=Stopwatch.StartNew(); for(var a=0;a<試行ブロック数;a++) { var i=r.Next(ブロック数); var p=ブロック配列[i]; if(p==IntPtr.Zero) { ブロック配列[i]=VirtualAlloc(IntPtr.Zero,ブロックバイト数,AllocationType.RESERVE|AllocationType.COMMIT,eProtect.PAGE_READWRITE); } else { 解放(p,ブロックバイト数); ブロック配列[i]=IntPtr.Zero; } } VirtualAlloc_VirtualFreeランダム速度.Stop(); Debug.Print("VirtualAlloc_VirtualFreeランダム速度:ブロックバイト数{0},ブロック数{1},{2}ms",ブロックバイト数,ブロック数,VirtualAlloc_VirtualFreeランダム速度.ElapsedMilliseconds); for(var a=0;a<ブロック数;a++) { var p=ブロック配列[a]; if(p!=IntPtr.Zero) { 解放(p,ブロックバイト数); } } } static void 空きメモリ無駄なく確保テスト(Int32 ブロックバイト数) { var ブロックList=new List<IntPtr>(); var 空きメモリ無駄なく確保テストランダム速度=Stopwatch.StartNew(); while(true) { var p=VirtualAlloc(IntPtr.Zero,ブロックバイト数,AllocationType.RESERVE,eProtect.PAGE_READWRITE); if(p==IntPtr.Zero) break; ブロックList.Add(p); } 空きメモリ無駄なく確保テストランダム速度.Stop(); for(var a=0;a<ブロックList.Count;a++) { 解放(ブロックList[a],ブロックバイト数); } Debug.Print("空きメモリ無駄なく確保テストランダム速度:ブロックバイト数{0},ブロック数{1},確保できたバイト数{2},{3}ms",ブロックバイト数,ブロックList.Count,ブロックバイト数*ブロックList.Count,空きメモリ無駄なく確保テストランダム速度.ElapsedMilliseconds); } static void Main() { GetSystemInfo(out SYSTEM_INFO変数); for(var a=65536;;a<<=1) { 空きメモリ無駄なく確保テスト(a); 確保テスト(a); } } }
この結果は
空きメモリ無駄なく確保テストランダム速度:ブロックバイト数65536,ブロック数29781,確保できたバイト数1951727616,172ms VirtualAlloc:ブロックバイト数65536,ブロック数16384,77ms VirtualFree:ブロックバイト数65536,ブロック数16384,214ms VirtualAlloc_VirtualFreeランダム速度:ブロックバイト数65536,ブロック数16384,9226ms 空きメモリ無駄なく確保テストランダム速度:ブロックバイト数131072,ブロック数13928,確保できたバイト数1825570816,53ms VirtualAlloc:ブロックバイト数131072,ブロック数8192,34ms VirtualFree:ブロックバイト数131072,ブロック数8192,107ms VirtualAlloc_VirtualFreeランダム速度:ブロックバイト数131072,ブロック数8192,9303ms 空きメモリ無駄なく確保テストランダム速度:ブロックバイト数262144,ブロック数6948,確保できたバイト数1821376512,27ms VirtualAlloc:ブロックバイト数262144,ブロック数4096,16ms VirtualFree:ブロックバイト数262144,ブロック数4096,60ms VirtualAlloc_VirtualFreeランダム速度:ブロックバイト数262144,ブロック数4096,9636ms 空きメモリ無駄なく確保テストランダム速度:ブロックバイト数524288,ブロック数3464,確保できたバイト数1816133632,13ms VirtualAlloc:ブロックバイト数524288,ブロック数2048,8ms VirtualFree:ブロックバイト数524288,ブロック数2048,34ms VirtualAlloc_VirtualFreeランダム速度:ブロックバイト数524288,ブロック数2048,10746ms 空きメモリ無駄なく確保テストランダム速度:ブロックバイト数1048576,ブロック数1718,確保できたバイト数1801453568,6ms VirtualAlloc:ブロックバイト数1048576,ブロック数1024,4ms VirtualFree:ブロックバイト数1048576,ブロック数1024,22ms VirtualAlloc_VirtualFreeランダム速度:ブロックバイト数1048576,ブロック数1024,12700ms 空きメモリ無駄なく確保テストランダム速度:ブロックバイト数2097152,ブロック数849,確保できたバイト数1780482048,3ms VirtualAlloc:ブロックバイト数2097152,ブロック数512,3ms VirtualFree:ブロックバイト数2097152,ブロック数512,14ms VirtualAlloc_VirtualFreeランダム速度:ブロックバイト数2097152,ブロック数512,16781ms 空きメモリ無駄なく確保テストランダム速度:ブロックバイト数4194304,ブロック数417,確保できたバイト数1749024768,1ms VirtualAlloc:ブロックバイト数4194304,ブロック数256,1ms VirtualFree:ブロックバイト数4194304,ブロック数256,13ms VirtualAlloc_VirtualFreeランダム速度:ブロックバイト数4194304,ブロック数256,25298ms 空きメモリ無駄なく確保テストランダム速度:ブロックバイト数8388608,ブロック数202,確保できたバイト数1694498816,0ms VirtualAlloc:ブロックバイト数8388608,ブロック数128,0ms VirtualFree:ブロックバイト数8388608,ブロック数128,9ms VirtualAlloc_VirtualFreeランダム速度:ブロックバイト数8388608,ブロック数128,41185ms 空きメモリ無駄なく確保テストランダム速度:ブロックバイト数16777216,ブロック数96,確保できたバイト数1610612736,0ms VirtualAlloc:ブロックバイト数16777216,ブロック数64,0ms VirtualFree:ブロックバイト数16777216,ブロック数64,9ms VirtualAlloc_VirtualFreeランダム速度:ブロックバイト数16777216,ブロック数64,72376ms 空きメモリ無駄なく確保テストランダム速度:ブロックバイト数33554432,ブロック数45,確保できたバイト数1509949440,0ms VirtualAlloc:ブロックバイト数33554432,ブロック数32,0ms VirtualFree:ブロックバイト数33554432,ブロック数32,8ms スレッド 0x16a4 はコード 0 (0x0) で終了しました。 VirtualAlloc_VirtualFreeランダム速度:ブロックバイト数33554432,ブロック数32,134747ms 空きメモリ無駄なく確保テストランダム速度:ブロックバイト数67108864,ブロック数21,確保できたバイト数1409286144,0ms VirtualAlloc:ブロックバイト数67108864,ブロック数16,0ms VirtualFree:ブロックバイト数67108864,ブロック数16,8ms VirtualAlloc_VirtualFreeランダム速度:ブロックバイト数67108864,ブロック数16,261282ms 空きメモリ無駄なく確保テストランダム速度:ブロックバイト数134217728,ブロック数9,確保できたバイト数1207959552,0ms VirtualAlloc:ブロックバイト数134217728,ブロック数8,0ms VirtualFree:ブロックバイト数134217728,ブロック数8,8ms VirtualAlloc_VirtualFreeランダム速度:ブロックバイト数134217728,ブロック数8,512765ms 空きメモリ無駄なく確保テストランダム速度:ブロックバイト数268435456,ブロック数4,確保できたバイト数1073741824,0ms VirtualAlloc:ブロックバイト数268435456,ブロック数4,0ms VirtualFree:ブロックバイト数268435456,ブロック数4,8ms VirtualAlloc_VirtualFreeランダム速度:ブロックバイト数268435456,ブロック数4,1027149ms 空きメモリ無駄なく確保テストランダム速度:ブロックバイト数536870912,ブロック数1,確保できたバイト数536870912,0ms VirtualAlloc:ブロックバイト数536870912,ブロック数2,0ms 'System.ApplicationException' の初回例外が VirtualAllocの粒度.exe で発生しました。
という結果でした。
この結果から分かることはブロックサイズを大きくすれば容量あたりに必要な呼び出しオーバーヘッドが小さくなり、容量の効率は下がるということです。
その関係は素直でパラメータ設定により最適なバランスは算出できると思います。
断片化したメモリに対してはテストしていません。恐らくその場合は小さなブロックの容量の効率はいいでしょう。- 回答としてマーク 和和和 2009年7月12日 16:11
-
>コードポイントでの比較以上のUnicode処理が必要になるとstringのインスタンスにしなきゃいけないとかありそうな気もしますけど、NLS Unicode APIを C++/CLIで呼びをかける形で書けばstringのインスタンス化もしないでいけそうな気がします。
コードポイントってString.CompareOrdinalと同等のUnicode文字コードによる比較ってことですよね?
実はこれと同等の実装はしていましたが、Compareでいろいろな比較ができなかったのでSystem.StringをGCHandleで参照したいと思っていたのです。でも遅いというジレンマ
>NLS Unicode API
初耳なので調べました。C,C++でもポインタによる文字列形式からそういった複雑なメソッドを提供するものであると判断しました。
これが使えるのならば検討してみても良いと考えました。
参考になりますありがとうございました。- 回答としてマーク 和和和 2009年7月12日 16:11
-
GCHandleをメンバに持ちTargetからStringを参照する場合とSysttem.Stringを直接メンバにもつ場合
using System; using System.Diagnostics; using System.Runtime.InteropServices; public unsafe static class テスト { [StructLayout(LayoutKind.Sequential)] struct 直接String { public String 文字列; } [StructLayout(LayoutKind.Sequential)] struct 間接String { public GCHandle 文字列; } const Int32 行数=100000; const Int32 試行回数=1000; static void Main() { マネージドのテスト(); アンマネージドのテスト(); } static void マネージドのテスト() { var 直接String配列=new 直接String[行数]; var 間接String配列=new 間接String[行数]; for(var a=0;a<行数;a++) { 直接String配列[a].文字列=a.ToString("X16"); 間接String配列[a].文字列=GCHandle.Alloc(a.ToString("X16")); } var 直接String速度=Stopwatch.StartNew(); for(var b=0;b<試行回数;b++) { for(var a=0;a<行数;a++) { var s=直接String配列[a].文字列; } } 直接String速度.Stop(); Debug.Print("マネージド直接String速度 {0}ms",直接String速度.ElapsedMilliseconds); var 間接String速度_classcast=Stopwatch.StartNew(); for(var b=0;b<試行回数;b++) { for(var a=0;a<行数;a++) { var s=(String)間接String配列[a].文字列.Target; } } 間接String速度_classcast.Stop(); Debug.Print("マネージド間接String速度_classcast {0}ms",間接String速度_classcast.ElapsedMilliseconds); var 間接String速度_isinst=Stopwatch.StartNew(); for(var b=0;b<試行回数;b++) { for(var a=0;a<行数;a++) { var s=間接String配列[a].文字列.Target as String; } } 間接String速度_isinst.Stop(); Debug.Print("マネージド間接String速度_isinst {0}ms",間接String速度_isinst.ElapsedMilliseconds); var 間接String速度_GCHandle_Target=Stopwatch.StartNew(); for(var b=0;b<試行回数;b++) { for(var a=0;a<行数;a++) { var s=間接String配列[a].文字列.Target; } } 間接String速度_GCHandle_Target.Stop(); Debug.Print("マネージド間接String速度_GCHandle_Target {0}ms",間接String速度_GCHandle_Target.ElapsedMilliseconds); var 間接String速度_GCHandle=Stopwatch.StartNew(); for(var b=0;b<試行回数;b++) { for(var a=0;a<行数;a++) { var s=間接String配列[a].文字列; } } 間接String速度_GCHandle.Stop(); Debug.Print("マネージド間接String速度_GCHandle {0}ms",間接String速度_GCHandle.ElapsedMilliseconds); } static void アンマネージドのテスト() { var 間接String配列=stackalloc 間接String[行数]; for(var a=0;a<行数;a++) { 間接String配列[a].文字列=GCHandle.Alloc(a.ToString("X16")); } var 間接String速度_classcast=Stopwatch.StartNew(); for(var b=0;b<試行回数;b++) { for(var a=0;a<行数;a++) { var s=(String)間接String配列[a].文字列.Target; } } 間接String速度_classcast.Stop(); Debug.Print("アンマネージド間接String速度_classcast {0}ms",間接String速度_classcast.ElapsedMilliseconds); var 間接String速度_isinst=Stopwatch.StartNew(); for(var b=0;b<試行回数;b++) { for(var a=0;a<行数;a++) { var s=間接String配列[a].文字列.Target as String; } } 間接String速度_isinst.Stop(); Debug.Print("アンマネージド間接String速度_isinst {0}ms",間接String速度_isinst.ElapsedMilliseconds); var 間接String速度_GCHandle_Target=Stopwatch.StartNew(); for(var b=0;b<試行回数;b++) { for(var a=0;a<行数;a++) { var s=間接String配列[a].文字列.Target; } } 間接String速度_GCHandle_Target.Stop(); Debug.Print("アンマネージド間接String速度_GCHandle_Target {0}ms",間接String速度_GCHandle_Target.ElapsedMilliseconds); var 間接String速度_GCHandle=Stopwatch.StartNew(); for(var b=0;b<試行回数;b++) { for(var a=0;a<行数;a++) { var s=間接String配列[a].文字列; } } 間接String速度_GCHandle.Stop(); Debug.Print("アンマネージド間接String速度_GCHandle {0}ms",間接String速度_GCHandle.ElapsedMilliseconds); } }
結果は、
マネージド直接String速度 686ms
でした。IDE上で実行したものです。
マネージド間接String速度_classcast 3630ms
マネージド間接String速度_isinst 3611ms
マネージド間接String速度_GCHandle_Target 1893ms
マネージド間接String速度_GCHandle 637ms
アンマネージド間接String速度_classcast 2288ms
アンマネージド間接String速度_isinst 2267ms
アンマネージド間接String速度_GCHandle_Target 1627ms
アンマネージド間接String速度_GCHandle 560ms
まず直接Stringを埋め込んだほうが圧倒的に速いです。
全般的にアンマネージのほうが速いです。これは当然と思われます。
キャスト方法による速度差を見ました。
あるサイトで実験した人によれば(String)xというclasscastのほうが速いとありましたが、その速度差は非常に小さなものだと思われます。
キャストはどちらも大きなコストが必要です。
またTargetプロパティ参照もより大きなコストが必要です。
.NET FrameworkのGCHandleがGenericにGCHandle<T>でもあればよかったんですけどね。
ソースは公開しているので自作のGCHandle<T>でも作れないものかと思いました。
出来たら文字列だけでなく全てのマネージドオブジェクトを高速にアンマネージメモリに格納できるためマニアックな需要はありそうです。- 編集済み 和和和 2009年7月12日 3:15
-
ざっとしか読んでいませんが、マネージ・アンマネージコードの比較とされている箇所は、newで確保するか、stackallocで確保するかの違いですよね?
試しに、「マネージドのテスト」という名前の関数をnewで確保し、fixedで固定し、固定されたポインタを相手に操作したら、「アンマネージドのテスト」という名前の関数と同程度の時間になるように見受けられました。
恐らくは、配列をstelem命令でアクセスするか、ポインタベースでアクセスするかの差ではないでしょうか?
http://msdn.microsoft.com/ja-jp/library/system.reflection.emit.opcodes.stelem(VS.80).aspx
マネージ配列にアクセスする場合は、通常、Nullチェックやインデックスの範囲外チェック等の各種チェックが入ります。
繰り返し回数が膨大になると、このコストはそこそこ重くなります。
このコストが気になる場合は、部分的にunsafeコンテキストにすることで対応することになります。(アンマネージとunsafeは微妙に違います)
ところで、VirtualAllocとどのように組み合わせているのでしょうか?
GCHandleを使ったところで、Stringのインスタンスはマネージメモリに存在し、GCHandle.ToIntPtr/FromIntPtrではマネージメモリへのアクセスのための数値(ハンドル?)をやりとりするだけだと考えられるため、そのデータ構造を表す領域はマネージメモリと、VirtualAllocによるアンマネージメモリの両方が存在して成り立つことになります。
このあたりは懸念するほどの有意の差にはなっていないということでしょうか。
(参考)
fixedで固定する際は下記のようにしました。なお、直接Stringはfixed指定できない型であったため、省略しています。
var 間接String配列_ = new 間接String[行数];
fixed (間接String* 間接String配列 = 間接String配列_)
{
※「マネージドのテスト」という名前の関数の中身
}
解決した場合は、参考になった返信に「回答としてマーク」のボタンを利用して、回答に設定しましょう(複数に設定できます)。- 回答としてマーク 和和和 2009年7月12日 16:11
-
>このコストが気になる場合は、部分的にunsafeコンテキストにすることで対応することになります。(アンマネージとunsafeは微妙に違います)
自分の解釈ではfixed,stackalloc,API等を使ってポインタを介して操作する時はunsafe。
アンマネージはfixed,stackallocは含まないと判断していますが正しいでしょうか。
>ところで、VirtualAllocとどのように組み合わせているのでしょうか?
今回のテストでstackallocで確保,new→fixedで確保している部分をそっくりVirtualAllocに置き換えて使います。
>GCHandleを使ったところで、Stringのインスタンスはマネージメモリに存在し、GCHandle.ToIntPtr/FromIntPtrではマネージメモリへのアクセスのための数値(ハンドル?)をやりとりするだけだと考えられるため、そのデータ構造を表す領域はマネージメモリと、VirtualAllocによるアンマネージメモリの両方が存在して成り立つことになります。
このあたりは懸念するほどの有意の差にはなっていないということでしょうか。
マネージドメモリアロケータ(new)を自作できればもしかして解決できるかもしれませんがどうやってやるかがまだ見えません。
newによる確保とVirtulAllocでどれだけ特性が違うか、またベンチマークかいてみます。
fixedテストはありがとうございました。実験はしていませんでしたが予想通りの結果で安心しました。 -
newとVirtualAllocの速度を量りました。前のコードに追加したので長いですが
using System; using System.Diagnostics; using System.Runtime.InteropServices; using System.Collections.Generic; using System.Security; public unsafe static class テスト { [StructLayout(LayoutKind.Sequential)] struct 直接String { public String 文字列; } [StructLayout(LayoutKind.Sequential)] struct 間接String { public GCHandle 文字列; } const Int32 行数=100000; const Int32 試行回数=1000; struct 最小予約粒度 { public fixed byte a[65536]; } [SuppressUnmanagedCodeSecurity] [DllImport("kernel32")] public static extern IntPtr VirtualAlloc(IntPtr lpAddress,Int32 dwSize,AllocationType flAllocationType,eProtect flProtect); [SuppressUnmanagedCodeSecurity] [DllImport("kernel32")] public static extern bool VirtualFree(IntPtr lpAddress,Int32 dwSize,FreeType dwFreeType); public enum FreeType { DECOMMIT=0x4000, RELEASE=0x8000 } public enum AllocationType { COMMIT=0x1000, PHYSICAL=0x400000, RESERVE=0x2000, RESET=0x80000, TOP_DOWN=0x100000, WRITE_WATCH=0x200000 } public enum eProtect:uint { PAGE_NOACCESS=0x0001, PAGE_READONLY=0x0002, PAGE_READWRITE=0x0004, PAGE_WRITECOPY=0x0008, PAGE_EXECUTE=0x0010, PAGE_EXECUTE_READ=0x0020, PAGE_EXECUTE_READWRITE=0x0040, PAGE_EXECUTE_WRITECOPY=0x0080, PAGE_GUARD=0x0100, PAGE_NOCACHE=0x0200, PAGE_WRITECOMBINE=0x0400 } static void Main() { newとVirtualAllocの速度(); マネージドのテスト(); アンマネージドのテスト(); } static void newとVirtualAllocの速度() { var new_List=new List<byte[]>(); var VirtualAlloc_List=new List<IntPtr>(); var VirtualAlloc速度=Stopwatch.StartNew(); for(var a=0;a<試行回数;a++) { VirtualAlloc_List.Add(VirtualAlloc(IntPtr.Zero,65536,AllocationType.RESERVE|AllocationType.COMMIT,eProtect.PAGE_READWRITE)); } VirtualAlloc速度.Stop(); for(var a=0;a<試行回数;a++) { var p=VirtualAlloc_List[a]; VirtualFree(p,65536,FreeType.DECOMMIT); VirtualFree(p,0,FreeType.RELEASE); } var new速度=Stopwatch.StartNew(); for(var a=0;a<試行回数;a++) { new_List.Add(new byte[65536]); } new速度.Stop(); Debug.Print("VirtualAlloc速度 {0}ms",VirtualAlloc速度.ElapsedMilliseconds); Debug.Print("new速度 {0}ms",new速度.ElapsedMilliseconds); } static void マネージドのテスト() { var 直接String配列=new 直接String[行数]; var 間接String配列=new 間接String[行数]; for(var a=0;a<行数;a++) { 直接String配列[a].文字列=a.ToString("X16"); 間接String配列[a].文字列=GCHandle.Alloc(a.ToString("X16")); } var 直接String速度=Stopwatch.StartNew(); for(var b=0;b<試行回数;b++) { for(var a=0;a<行数;a++) { var s=直接String配列[a].文字列; } } 直接String速度.Stop(); Debug.Print("マネージド直接String速度 {0}ms",直接String速度.ElapsedMilliseconds); var 間接String速度_classcast=Stopwatch.StartNew(); for(var b=0;b<試行回数;b++) { for(var a=0;a<行数;a++) { var s=(String)間接String配列[a].文字列.Target; } } 間接String速度_classcast.Stop(); Debug.Print("マネージド間接String速度_classcast {0}ms",間接String速度_classcast.ElapsedMilliseconds); var 間接String速度_isinst=Stopwatch.StartNew(); for(var b=0;b<試行回数;b++) { for(var a=0;a<行数;a++) { var s=間接String配列[a].文字列.Target as String; } } 間接String速度_isinst.Stop(); Debug.Print("マネージド間接String速度_isinst {0}ms",間接String速度_isinst.ElapsedMilliseconds); var 間接String速度_GCHandle_Target=Stopwatch.StartNew(); for(var b=0;b<試行回数;b++) { for(var a=0;a<行数;a++) { var s=間接String配列[a].文字列.Target; } } 間接String速度_GCHandle_Target.Stop(); Debug.Print("マネージド間接String速度_GCHandle_Target {0}ms",間接String速度_GCHandle_Target.ElapsedMilliseconds); var 間接String速度_GCHandle=Stopwatch.StartNew(); for(var b=0;b<試行回数;b++) { for(var a=0;a<行数;a++) { var s=間接String配列[a].文字列; } } 間接String速度_GCHandle.Stop(); Debug.Print("マネージド間接String速度_GCHandle {0}ms",間接String速度_GCHandle.ElapsedMilliseconds); } static void アンマネージドのテスト() { var 間接String配列=stackalloc 間接String[行数]; for(var a=0;a<行数;a++) { 間接String配列[a].文字列=GCHandle.Alloc(a.ToString("X16")); } var 間接String速度_classcast=Stopwatch.StartNew(); for(var b=0;b<試行回数;b++) { for(var a=0;a<行数;a++) { var s=(String)間接String配列[a].文字列.Target; } } 間接String速度_classcast.Stop(); Debug.Print("アンマネージド間接String速度_classcast {0}ms",間接String速度_classcast.ElapsedMilliseconds); var 間接String速度_isinst=Stopwatch.StartNew(); for(var b=0;b<試行回数;b++) { for(var a=0;a<行数;a++) { var s=間接String配列[a].文字列.Target as String; } } 間接String速度_isinst.Stop(); Debug.Print("アンマネージド間接String速度_isinst {0}ms",間接String速度_isinst.ElapsedMilliseconds); var 間接String速度_GCHandle_Target=Stopwatch.StartNew(); for(var b=0;b<試行回数;b++) { for(var a=0;a<行数;a++) { var s=間接String配列[a].文字列.Target; } } 間接String速度_GCHandle_Target.Stop(); Debug.Print("アンマネージド間接String速度_GCHandle_Target {0}ms",間接String速度_GCHandle_Target.ElapsedMilliseconds); var 間接String速度_GCHandle=Stopwatch.StartNew(); for(var b=0;b<試行回数;b++) { for(var a=0;a<行数;a++) { var s=間接String配列[a].文字列; } } 間接String速度_GCHandle.Stop(); Debug.Print("アンマネージド間接String速度_GCHandle {0}ms",間接String速度_GCHandle.ElapsedMilliseconds); } }
これで結果は
VirtualAlloc速度 10ms new速度 145ms マネージド直接String速度 454ms マネージド間接String速度_classcast 3073ms マネージド間接String速度_isinst 3071ms マネージド間接String速度_GCHandle_Target 1237ms マネージド間接String速度_GCHandle 425ms アンマネージド間接String速度_classcast 1640ms アンマネージド間接String速度_isinst 1707ms アンマネージド間接String速度_GCHandle_Target 1080ms アンマネージド間接String速度_GCHandle 374ms
になりました。newは配列でなくサイズ64Kの構造体の場合List<>がnewできなかったので配列で行いました。
確保のみで解放の速度はGCとVirtualFreeでは測るのが難しいのでやっていません。
GCやマネージドアロケータを自作できる情報があればありがたいです。
-
NyaRuRuさんのblogでこういった記事を見たことがあります。
お望みのことができるかどうかは分かりません。
http://d.hatena.ne.jp/NyaRuRu/20060616/p2
# マネージコードでかなりこだわるぐらいなら、大容量のメモリを扱う部分をネイティブで書いたら良いのではと思ったり。できるかどうかまでは検討できていませんが…。
解決した場合は、参考になった返信に「回答としてマーク」のボタンを利用して、回答に設定しましょう(複数に設定できます)。