はじめに
これまで何度か.NET Framework上で画像処理を行う際の記事を書いてきました。
.NETによる画像処理の高速化Tips:非unsafe編
.NETによる画像処理の高速化Tips:unsafe編(改稿:2015/11/08)
.NETでBitmap、BitmapDataを扱う場合の例外処理について
.NET Framework上で画像処理を高速で行おうとした場合、どうしてもアンマネージドなメモリの利用を考慮する必要がでてきます。
また、それ以外にもアンマネージドなメモリを利用したいケースというのは存在します。
.NET Frameworkでアンマネージドなメモリを確保しようとした場合、通常はSystem.Runtime.InteropServices.MarshalクラスのAllocHGlobalメソッドかAllocCoTaskMemメソッドを使う事が多いでしょう。
(PInvokeを利用するなど他の手段ももちろんありますが、今回は割愛します)
ではAllocHGlobalとAllocCoTaskMem、どちらを使うべきなのか?
本稿ではそこを検討した結果を記載します。
2015/12/12 追記
本記事を参考にされる際は、以下もあわせてご一読いただくことを推奨いたします。
アンマネージドリソースのリークを回避する方法
検討結果
結論を最初に書いてしまうと、特別な理由がない場合を除き、AllocCoTaskMemを利用した方が良さそうです。
ただし、パフォーマンス的には大きな差異は見受けられませんでした。
詳細な検証結果の数字は後半に記載します。
まずは理論的なところを整理します。
AllocHGlobalは内部でLocalAlloc関数を利用しています。
それに対してAllocCoTaskMemの場合、CoTaskMemAlloc関数を利用しています。
両者はいずれも、実行プロセスのデフォルトのヒープ領域にメモリを確保するため、結果的に利用するメモリは同じ領域のメモリになります。
では、何が違うのかというと、メモリの割り当て管理をしているアロケータが異なります。(以下のリンクを参照)
MSDN ヒープ : 喜びと苦悩
上記のサイトの日本語は読みにくいので英語サイトも併読したほうが分かりやすいかもしれません。
MSDN Heap: Pleasures and Pains
MSDNのLocalAlloc関数の記事を見ると、次のような記載があります。
ヒープから、指定されたバイト数を確保します。Win32 のメモリ管理には、ローカルヒープとグローバルヒープを個別に提供する機能がありません。
注 この関数は、16 ビット版 Windows との互換性のために提供されています。
MSDN LocalAlloc関数
LocalAlloc関数はそもそもWin16との互換性維持のために残されているようです。
また上記の通り、LocalAlloc関数で利用されるローカルヒープと、GlobalAlloc関数で利用されるグローバルヒープは内部的に同一のものです。
そしてGlobalAlloc関数の記述を見ると
注意 グローバル関数は他のメモリ管理関数より低速で、提供する機能も多くありません。そのため、新しいアプリケーションは(ヒープ関数)を使うべきです。しかし、DDE 関数とクリップボード関数では、依然としてグローバル関数が使われています。
MSDN GlobalAlloc関数
という記述があります。
まとめると
- AllocHGlobalは内部でLocalAlloc関数を利用している
- LocalAlloc関数はWin16との互換性維持のために残されている
- LocalAllocのメモリ管理の実態はGlobalAllocと同じものである
- GlobalAllocもクリップボードやDDEといったレガシーサポートの為で、それ以外の用途にはパフォーマンス等の理由で推奨されていない
ということになります。
対してAllocCoTaskMemはどうか?
MSDN ヒープ : 喜びと苦悩でも見て取れるように、AllocCoTaskMemはCOMタスクメモリアロケータを利用しています。
これはCOMのオートメーションなどでも利用されているもので、現在も頻繁?に利用されています。
これらの点から、特定の用途を除き、AllocHGlobalではなくAllocCoTaskMemを利用すべきだと言えるでしょう。
さて、MSDNには
グローバル関数は他のメモリ管理関数より低速で
という記述がありますが、具体的にどの程度遅いのでしょうか?
次節ではその点について検証してみます。
検証内容
以下の評価を、AllocHGlobalとAllocCoTaskMemの双方でそれぞれ500回指向しその平均値を求めます。
その際、確保するメモリーサイズは2480×3508×3[byte](A4 300dpi 24bitカラーの想定)で扱います。
- メモリーの確保と解放を100回ずつ実行
- 同サイズのメモリを2つ作成し、1byteずつforループでコピー
- 同サイズのメモリを2つ作成し、MarshalクラスのCopyメソッドを利用して一括コピー
- 同サイズのメモリを2つ作成し、UnmanagedMemoryStreamでbyte配列経由で一括コピー
- 同サイズのメモリを2つ作成し、UnmanagedMemoryStreamでCopyToで一括コピー
- 同サイズのメモリを2つ作成し、kernel32.dllのCopyMemory関数を利用して一括コピー
検証に利用したソースコードは最後にまとめて記載します。
なお、3.と4.はヒープ領域上のbyte配列を一旦経由してコピーするコードになります。
前提条件
環境1
- CPU:Intel Core i7-4770(3.4GHz)
- メモリ:24GB
- .NET Framework 4.5.2
- Visual Studio 2015
- Windows 8.1
- Releaseビルドでコードの最適化を有効化
環境2
- CPU:Intel Core i7-4770(3.4GHz)
- メモリ:4GB
- .NET Framework 4.0
- Visual Studio 2015
- Windows 7 SP1 32bit
- Releaseビルドでコードの最適化を有効化
- 環境1のVMWare上に構築された仮想環境
検証結果
検証した結果は以下の通りになります。
環境1
No. | テスト項目 | AllocHGlobal | AllocCoTaskMem |
---|---|---|---|
1 | メモリ確保と解放 | 3.6[ms] | 3.6[ms] |
2 | 1byteずつforループでコピー | 9.1[ms] | 9.1[ms] |
3 | MarshalクラスのCopyメソッドで一括コピー | 15.5[ms] | 11.9[ms] |
4 | UnmanagedMemoryStreamでbyte配列経由で一括コピー | 11.2[ms] | 16.9[ms] |
5 | UnmanagedMemoryStreamでCopyToで一括コピー | 3.7[ms] | 3.7[ms] |
6 | kernel32.dllのCopyMemory関数を利用して一括コピー | 3.0[ms] | 3.0[ms] |
環境2
No. | テスト項目 | AllocHGlobal | AllocCoTaskMem |
---|---|---|---|
1 | メモリ確保と解放 | 0.4[ms] | 0.4[ms] |
2 | 1byteずつforループでコピー | 9.6[ms] | 9.5[ms] |
3 | MarshalクラスのCopyメソッドで一括コピー | 15.1[ms] | 15.1[ms] |
4 | UnmanagedMemoryStreamでbyte配列経由で一括コピー | 14.9[ms] | 14.8[ms] |
5 | UnmanagedMemoryStreamでCopyToで一括コピー | 6.4[ms] | 10.1[ms] |
6 | kernel32.dllのCopyMemory関数を利用して一括コピー | 3.4[ms] | 3.4[ms] |
考察
う~ん。。。あまり思ったようになっていませんね。
3.と4.のヒープ上のバイト配列を経由したものが、一律遅いのは想定通りです。
やはりヒープ上のオブジェクトへの操作はオーバーヘッドが大きいですね。
逆に、いくつかは疑問の残る結果もあります。
具体的には以下の通りです。
- 環境1にて「UnmanagedMemoryStreamでbyte配列経由で一括コピー」したときにAllocCoTaskMemの方が遅い
- 環境2にて「UnmanagedMemoryStreamでCopyToで一括コピー」したときにAllocCoTaskMemの方が遅い
- 環境2は環境1のVM上のゲストOSなのに、「メモリ確保と解放」が環境2の方が早い
1.に関しては、実際のところ.NET Framework4.0以上であればCopyToを使うと思うのであまり気にする必要もないのかもしれません。
2.に関しては微妙です。ここはAllocCoTaskMemの方が早い結果になって欲しかったですが、.NET4.0はもうオワコンなので気にする必要はないのかもしれません。
また、そこまでパフォーマンスを気にするなら、kernel32.dllのCopyMemory関数を利用すればいいわけで。。。
3.に関しては追試で.NET Framework4.0でビルドしたモジュールを環境1で実行してみましたが、結果は上の検証結果と大差ありませんでした。
ここだけ見ると、Windows8.1の64bit版は、Windows7の32bit版よりメモリ確保・解放が遅いということになります。
が、まぁ今回の検証の主旨とは関係のない点ではあります。
結論をまとめると
「パフォーマンス的に多少の前後はあるが、AllocHGlobalとAllocCoTaskMemに大きな違いはあまりない。
LocalAllocは互換性維持のために残されていることから、特別な理由がない限り、AllocCoTaskMemを利用したほうが好ましい。」
ということになりました。
ソースコードサンプル
以下に検証で利用したソースコードのサンプルを記載します。
なお共通の定数として以下が宣言されてあります。
/// <summary>
/// A4 300dpi 24bit画像の想定
/// </summary>
private const int AllocSize = 3 * 2480 * 3508;
メモリ確保と解放
const int AllocCount = 100;
// AllocHGlobal
for (int i = 0; i < AllocCount; i++)
{
IntPtr intPtr = Marshal.AllocHGlobal(AllocSize);
Marshal.FreeHGlobal(intPtr);
}
// AllocCoTaskMem
for (int i = 0; i < AllocCount; i++)
{
IntPtr intPtr = Marshal.AllocCoTaskMem(AllocSize);
Marshal.FreeCoTaskMem(intPtr);
}
1byteずつforループでコピー
private static unsafe void UnsafeLoopCopy(IntPtr src, IntPtr dest)
{
byte* srcPointer = (byte*)src;
byte* destPointer = (byte*)dest;
for (int i = 0; i < AllocSize; i++)
{
destPointer[i] = srcPointer[i];
}
}
MarshalクラスのCopyメソッドで一括コピー
private static void MarshalCopy(IntPtr src, IntPtr dest)
{
byte[] temp = new byte[AllocSize];
Marshal.Copy(src, temp, 0, AllocSize);
Marshal.Copy(temp, 0, dest, AllocSize);
}
UnmanagedMemoryStreamでbyte配列経由で一括コピー
private unsafe static void UnsafeStream(IntPtr src, IntPtr dest)
{
using (UnmanagedMemoryStream streamSrc = new UnmanagedMemoryStream((byte*)src, AllocSize))
using (UnmanagedMemoryStream streamDst = new UnmanagedMemoryStream((byte*)dest, AllocSize, AllocSize, FileAccess.Write))
{
byte[] temp = new byte[AllocSize];
streamSrc.Read(temp, 0, AllocSize);
streamDst.Write(temp, 0, AllocSize);
}
}
UnmanagedMemoryStreamでCopyToで一括コピー
private unsafe static void CopyTo(IntPtr src, IntPtr dest)
{
using (UnmanagedMemoryStream streamSrc = new UnmanagedMemoryStream((byte*)src, AllocSize))
using (UnmanagedMemoryStream streamDst = new UnmanagedMemoryStream((byte*)dest, AllocSize, AllocSize, FileAccess.Write))
{
streamSrc.CopyTo(streamDst);
}
}
kernel32.dllのCopyMemory関数を利用して一括コピー
[DllImport("kernel32.dll")]
private static extern void CopyMemory(IntPtr dst, IntPtr src, int size);
private static void CopyMemory(IntPtr src, IntPtr dest)
{
CopyMemory(src, dest, AllocSize);
}