CアプリケーションをC#に移植しています。 Cアプリは、サードパーティのDLLから多くの関数を呼び出すため、C#でこれらの関数のP/Invokeラッパーを作成しました。これらのC関数の一部は、C#アプリで使用する必要があるデータを割り当てるため、IntPtr
の- Marshal.PtrToStructure
および Marshal.Copy
ネイティブデータ(配列と構造体)を管理変数にコピーします。
残念ながら、C#アプリはCバージョンよりもかなり遅いことが判明しました。迅速なパフォーマンス分析により、上記のマーシャリングベースのデータコピーがボトルネックであることが示されました。 代わりにポインターを使用するように書き換えることにより、C#コードを高速化することを検討しています。C#で安全でないコードとポインターの経験がないため、次の質問に関する専門家の意見が必要です。
unsafe
とIntPtr
ingの代わりにMarshal
コードとポインターを使用することの欠点は何ですか?たとえば、それはより安全ではありませんか?人々はマーシャリングを好むようですが、その理由はわかりません。状況をより明確にするために、小さなサンプルコードを一緒にハックしました(実際のコードははるかに複雑です)。この例が、「安全でないコードとポインター」対「IntPtrとMarshal」について話しているときの意味を示してくれることを願っています。
MyLib.h
#ifndef _MY_LIB_H_
#define _MY_LIB_H_
struct MyData
{
int length;
unsigned char* bytes;
};
__declspec(dllexport) void CreateMyData(struct MyData** myData, int length);
__declspec(dllexport) void DestroyMyData(struct MyData* myData);
#endif // _MY_LIB_H_
MyLib.c
#include <stdlib.h>
#include "MyLib.h"
void CreateMyData(struct MyData** myData, int length)
{
int i;
*myData = (struct MyData*)malloc(sizeof(struct MyData));
if (*myData != NULL)
{
(*myData)->length = length;
(*myData)->bytes = (unsigned char*)malloc(length * sizeof(char));
if ((*myData)->bytes != NULL)
for (i = 0; i < length; ++i)
(*myData)->bytes[i] = (unsigned char)(i % 256);
}
}
void DestroyMyData(struct MyData* myData)
{
if (myData != NULL)
{
if (myData->bytes != NULL)
free(myData->bytes);
free(myData);
}
}
Main.c
#include <stdio.h>
#include "MyLib.h"
void main()
{
struct MyData* myData = NULL;
int length = 100 * 1024 * 1024;
printf("=== C++ test ===\n");
CreateMyData(&myData, length);
if (myData != NULL)
{
printf("Length: %d\n", myData->length);
if (myData->bytes != NULL)
printf("First: %d, last: %d\n", myData->bytes[0], myData->bytes[myData->length - 1]);
else
printf("myData->bytes is NULL");
}
else
printf("myData is NULL\n");
DestroyMyData(myData);
getchar();
}
IntPtr
およびMarshal
を使用するC#アプリケーションProgram.cs
using System;
using System.Runtime.InteropServices;
public static class Program
{
[StructLayout(LayoutKind.Sequential)]
private struct MyData
{
public int Length;
public IntPtr Bytes;
}
[DllImport("MyLib.dll")]
private static extern void CreateMyData(out IntPtr myData, int length);
[DllImport("MyLib.dll")]
private static extern void DestroyMyData(IntPtr myData);
public static void Main()
{
Console.WriteLine("=== C# test, using IntPtr and Marshal ===");
int length = 100 * 1024 * 1024;
IntPtr myData1;
CreateMyData(out myData1, length);
if (myData1 != IntPtr.Zero)
{
MyData myData2 = (MyData)Marshal.PtrToStructure(myData1, typeof(MyData));
Console.WriteLine("Length: {0}", myData2.Length);
if (myData2.Bytes != IntPtr.Zero)
{
byte[] bytes = new byte[myData2.Length];
Marshal.Copy(myData2.Bytes, bytes, 0, myData2.Length);
Console.WriteLine("First: {0}, last: {1}", bytes[0], bytes[myData2.Length - 1]);
}
else
Console.WriteLine("myData.Bytes is IntPtr.Zero");
}
else
Console.WriteLine("myData is IntPtr.Zero");
DestroyMyData(myData1);
Console.ReadKey(true);
}
}
unsafe
コードとポインターを使用するC#アプリケーションProgram.cs
using System;
using System.Runtime.InteropServices;
public static class Program
{
[StructLayout(LayoutKind.Sequential)]
private unsafe struct MyData
{
public int Length;
public byte* Bytes;
}
[DllImport("MyLib.dll")]
private unsafe static extern void CreateMyData(out MyData* myData, int length);
[DllImport("MyLib.dll")]
private unsafe static extern void DestroyMyData(MyData* myData);
public unsafe static void Main()
{
Console.WriteLine("=== C# test, using unsafe code ===");
int length = 100 * 1024 * 1024;
MyData* myData;
CreateMyData(out myData, length);
if (myData != null)
{
Console.WriteLine("Length: {0}", myData->Length);
if (myData->Bytes != null)
Console.WriteLine("First: {0}, last: {1}", myData->Bytes[0], myData->Bytes[myData->Length - 1]);
else
Console.WriteLine("myData.Bytes is null");
}
else
Console.WriteLine("myData is null");
DestroyMyData(myData);
Console.ReadKey(true);
}
}
少し古いスレッドですが、最近、C#でマーシャリングを行って過度のパフォーマンステストを行いました。何日もかけてシリアルポートから大量のデータを非整列化する必要があります。メモリリークがないことが重要でした(数百万回の呼び出し後に最小のリークが大きくなるため)。また、非常に大きな構造体(> 10kb)を使用して、そのため(いいえ、10kbの構造体は決して持ってはいけません:-))
次の3つのアンマーシャリング戦略をテストしました(マーシャリングもテストしました)。ほとんどすべての場合、最初の1つ(MarshalMatters)が他の2つよりも優れていました。 Marshal.Copyは常に最も遅く、他の2つはほとんどがレースで非常に接近していました。
安全でないコードを使用すると、重大なセキュリティリスクが生じる可能性があります。
最初:
public class MarshalMatters
{
public static T ReadUsingMarshalUnsafe<T>(byte[] data) where T : struct
{
unsafe
{
fixed (byte* p = &data[0])
{
return (T)Marshal.PtrToStructure(new IntPtr(p), typeof(T));
}
}
}
public unsafe static byte[] WriteUsingMarshalUnsafe<selectedT>(selectedT structure) where selectedT : struct
{
byte[] byteArray = new byte[Marshal.SizeOf(structure)];
fixed (byte* byteArrayPtr = byteArray)
{
Marshal.StructureToPtr(structure, (IntPtr)byteArrayPtr, true);
}
return byteArray;
}
}
第二:
public class Adam_Robinson
{
private static T BytesToStruct<T>(byte[] rawData) where T : struct
{
T result = default(T);
GCHandle handle = GCHandle.Alloc(rawData, GCHandleType.Pinned);
try
{
IntPtr rawDataPtr = handle.AddrOfPinnedObject();
result = (T)Marshal.PtrToStructure(rawDataPtr, typeof(T));
}
finally
{
handle.Free();
}
return result;
}
/// <summary>
/// no Copy. no unsafe. Gets a GCHandle to the memory via Alloc
/// </summary>
/// <typeparam name="selectedT"></typeparam>
/// <param name="structure"></param>
/// <returns></returns>
public static byte[] StructToBytes<T>(T structure) where T : struct
{
int size = Marshal.SizeOf(structure);
byte[] rawData = new byte[size];
GCHandle handle = GCHandle.Alloc(rawData, GCHandleType.Pinned);
try
{
IntPtr rawDataPtr = handle.AddrOfPinnedObject();
Marshal.StructureToPtr(structure, rawDataPtr, false);
}
finally
{
handle.Free();
}
return rawData;
}
}
第3:
/// <summary>
/// http://stackoverflow.com/questions/2623761/marshal-ptrtostructure-and-back-again-and-generic-solution-for-endianness-swap
/// </summary>
public class DanB
{
/// <summary>
/// uses Marshal.Copy! Not run in unsafe. Uses AllocHGlobal to get new memory and copies.
/// </summary>
public static byte[] GetBytes<T>(T structure) where T : struct
{
var size = Marshal.SizeOf(structure); //or Marshal.SizeOf<selectedT>(); in .net 4.5.1
byte[] rawData = new byte[size];
IntPtr ptr = Marshal.AllocHGlobal(size);
Marshal.StructureToPtr(structure, ptr, true);
Marshal.Copy(ptr, rawData, 0, size);
Marshal.FreeHGlobal(ptr);
return rawData;
}
public static T FromBytes<T>(byte[] bytes) where T : struct
{
var structure = new T();
int size = Marshal.SizeOf(structure); //or Marshal.SizeOf<selectedT>(); in .net 4.5.1
IntPtr ptr = Marshal.AllocHGlobal(size);
Marshal.Copy(bytes, 0, ptr, size);
structure = (T)Marshal.PtrToStructure(ptr, structure.GetType());
Marshal.FreeHGlobal(ptr);
return structure;
}
}
相互運用性の考慮事項 マーシャリングが必要な理由とタイミング、および費用を説明しています。見積もり:
- マーシャリングは、呼び出し元と呼び出し先が同じデータインスタンスを操作できない場合に発生します。
- 繰り返しマーシャリングすると、アプリケーションのパフォーマンスに悪影響を与える可能性があります。
したがって、あなたの質問に答えれば
... P/Invokingにポインターを使用して、マーシャリングを使用するよりも本当に速く...
最初に、マネージコードがアンマネージメソッドの戻り値インスタンスを操作できるかどうかを自問します。答えが「はい」の場合、マーシャリングおよび関連するパフォーマンスコストは不要です。おおよその時間節約は、O(n)function wherenマーシャリングされたインスタンスのサイズ。さらに、メソッドの期間中に、管理されたデータブロックと管理されていないデータブロックの両方をメモリに同時に保持しない(「IntPtr and Marshal」の例)と、追加のオーバーヘッドとメモリプレッシャーがなくなります。
安全でないコードとポインターを使用することの欠点は何ですか?.
欠点は、ポインターを介して直接メモリにアクセスすることに伴うリスクです。 CまたはC++でポインターを使用することほど安全ではありません。必要に応じて使用し、意味があります。詳細は こちら です。
提示された例には「安全」に関する懸念が1つあります。マネージコードエラーが発生した後、割り当てられたアンマネージメモリの解放は保証されません。ベストプラクティスは
CreateMyData(out myData1, length);
if(myData1!=IntPtr.Zero) {
try {
// -> use myData1
...
// <-
}
finally {
DestroyMyData(myData1);
}
}
まだ読んでいる人のために、
私は答えのいずれかで見たとは思わない何か-安全でないコードはセキュリティリスクの何かを提示します。これは大きなリスクではなく、悪用するのは非常に難しいことです。ただし、私のようにPCI準拠の組織で働いている場合、この理由により、安全でないコードはポリシーによって許可されません。
マネージコードは通常、非常に安全です。これは、CLRがメモリの場所と割り当てを処理し、想定外のメモリへのアクセスや書き込みを防ぐためです。
Unsafeキーワードを使用し、「/ unsafe」でコンパイルし、ポインターを使用すると、これらのチェックをバイパスし、アプリケーションを使用して、実行中のマシンへのある程度の不正アクセスを誰かが取得する可能性が生じます。バッファオーバーラン攻撃のようなものを使用して、コードをだましてメモリの領域に命令を書き込んでから、プログラムカウンタによってアクセス(コードインジェクション)するか、単にマシンをクラッシュさせる可能性があります。
何年も前、SQLサーバーは、実際には想定よりはるかに長いTDSパケットで配信される悪意のあるコードの餌食になりました。パケットを読み取るメソッドは長さをチェックせず、予約されたアドレス空間を超えて内容を書き込み続けました。余分な長さとコンテンツは、次のメソッドのアドレスでプログラム全体をメモリに書き込むように慎重に作成されました。その後、攻撃者は、最高レベルのアクセス権を持つコンテキスト内で、SQLサーバーによって実行される独自のコードを入手しました。脆弱性はトランスポートレイヤースタックのこのポイントより下にあるため、暗号化を解除する必要さえありませんでした。
2つの答え、
安全でないコードは、CLRによって管理されていないことを意味します。使用するリソースを管理する必要があります。
パフォーマンスに影響を与える要因は非常に多いため、パフォーマンスをスケーリングすることはできません。しかし、間違いなくポインターの使用ははるかに高速です。
あなたのコードはサードパーティのDLLを呼び出すと述べたので、 unsafe コードはあなたのシナリオにより適していると思います。 struct
で可変長配列をスワップするという特定の状況に遭遇しました。私は知っています、私はこの種の使用が常に起こることを知っています、しかしそれは結局 always の場合ではありません。これに関するいくつかの質問を見たいかもしれません、例えば:
可変サイズの配列を含む構造体をC#にマーシャリングするにはどうすればよいですか?
この特定のケースのためにサードパーティのライブラリを少し変更できるとしたら、次の使用方法を検討してください。
_using System.Runtime.InteropServices;
public static class Program { /*
[StructLayout(LayoutKind.Sequential)]
private struct MyData {
public int Length;
public byte[] Bytes;
} */
[DllImport("MyLib.dll")]
// __declspec(dllexport) void WINAPI CreateMyDataAlt(BYTE bytes[], int length);
private static extern void CreateMyDataAlt(byte[] myData, ref int length);
/*
[DllImport("MyLib.dll")]
private static extern void DestroyMyData(byte[] myData); */
public static void Main() {
Console.WriteLine("=== C# test, using IntPtr and Marshal ===");
int length = 100*1024*1024;
var myData1 = new byte[length];
CreateMyDataAlt(myData1, ref length);
if(0!=length) {
// MyData myData2 = (MyData)Marshal.PtrToStructure(myData1, typeof(MyData));
Console.WriteLine("Length: {0}", length);
/*
if(myData2.Bytes!=IntPtr.Zero) {
byte[] bytes = new byte[myData2.Length];
Marshal.Copy(myData2.Bytes, bytes, 0, myData2.Length); */
Console.WriteLine("First: {0}, last: {1}", myData1[0], myData1[length-1]); /*
}
else {
Console.WriteLine("myData.Bytes is IntPtr.Zero");
} */
}
else {
Console.WriteLine("myData is empty");
}
// DestroyMyData(myData1);
Console.ReadKey(true);
}
}
_
ご覧のとおり、元のマーシャリングコードの多くがコメント化され、対応する変更された外部アンマネージ関数CreateMyDataAlt(byte[], ref int)
のCreateMyDataAlt(BYTE [], int)
が宣言されています。一部のデータコピーとポインターチェックは不要になります。つまり、コードはさらにシンプルになり、おそらくより高速に実行できるようになります。
それで、変更で何がそんなに違うのですか?バイト配列は、struct
にワープせずに直接マーシャリングされ、アンマネージド側に渡されます。アンマネージコード内でメモリを割り当てるのではなく、データを埋めるだけです(実装の詳細は省略)。そして、呼び出し後、必要なデータは管理側に提供されます。データが入力されておらず、使用すべきでないことを示したい場合は、単にlength
をゼロに設定して管理側に通知することができます。バイト配列はマネージドサイド内で割り当てられるため、いつか収集されるので、気にする必要はありません。
私の経験をこの古いスレッドに加えたかっただけです。録音ソフトウェアでマーシャリングを使用しました。ミキサーからネイティブバッファーにリアルタイムのサウンドデータを受け取り、byte []にマーシャリングしました。それは本当のパフォーマンスキラーでした。タスクを完了するための唯一の方法として、安全でない構造体への移動を余儀なくされました。
大規模なネイティブ構造体がなく、すべてのデータが2回満たされることを気にしない場合-マーシャリングはよりエレガントで、はるかに安全なアプローチです。
今日も同じ質問があり、具体的な測定値を探していましたが、見つかりませんでした。だから私は自分のテストを書いた。
このテストでは、10k x 10k RGBイメージのピクセルデータをコピーしています。画像データは300 MB(3 * 10 ^ 9バイト)です。このデータを10回コピーするメソッドもあれば、より高速であるため100回コピーするメソッドもあります。使用されるコピー方法には、
テスト環境:
CPU:Intel Core i7-3630QM @ 2.40 GHz
OS:Win 7 Pro x64 SP1
Visual Studio 2015.3、コードはC++/CLI、ターゲットの.netバージョンは4.5.2、デバッグ用にコンパイルされています。
試験結果:
すべての方法で1コアのCPU負荷は100%です(合計CPU負荷12.5%に相当)。
速度と実行時間の比較:
_method speed exec.time
Marshal.Copy (1*300MB) 100 % 100%
Buffer.BlockCopy (1*300MB) 98 % 102%
Pointer 4.4 % 2280%
Buffer.BlockCopy (1e9*3B) 1.4 % 7120%
Marshal.Copy (1e9*3B) 0.95% 10600%
_
以下のコードにコメントとして記述されている実行時間と計算された平均スループット。
_//------------------------------------------------------------------------------
static void CopyIntoBitmap_Pointer (array<unsigned char>^ i_aui8ImageData,
BitmapData^ i_ptrBitmap,
int i_iBytesPerPixel)
{
char* scan0 = (char*)(i_ptrBitmap->Scan0.ToPointer ());
int ixCnt = 0;
for (int ixRow = 0; ixRow < i_ptrBitmap->Height; ixRow++)
{
for (int ixCol = 0; ixCol < i_ptrBitmap->Width; ixCol++)
{
char* pPixel = scan0 + ixRow * i_ptrBitmap->Stride + ixCol * 3;
pPixel[0] = i_aui8ImageData[ixCnt++];
pPixel[1] = i_aui8ImageData[ixCnt++];
pPixel[2] = i_aui8ImageData[ixCnt++];
}
}
}
//------------------------------------------------------------------------------
static void CopyIntoBitmap_MarshallLarge (array<unsigned char>^ i_aui8ImageData,
BitmapData^ i_ptrBitmap)
{
IntPtr ptrScan0 = i_ptrBitmap->Scan0;
Marshal::Copy (i_aui8ImageData, 0, ptrScan0, i_aui8ImageData->Length);
}
//------------------------------------------------------------------------------
static void CopyIntoBitmap_MarshalSmall (array<unsigned char>^ i_aui8ImageData,
BitmapData^ i_ptrBitmap,
int i_iBytesPerPixel)
{
int ixCnt = 0;
for (int ixRow = 0; ixRow < i_ptrBitmap->Height; ixRow++)
{
for (int ixCol = 0; ixCol < i_ptrBitmap->Width; ixCol++)
{
IntPtr ptrScan0 = IntPtr::Add (i_ptrBitmap->Scan0, i_iBytesPerPixel);
Marshal::Copy (i_aui8ImageData, ixCnt, ptrScan0, i_iBytesPerPixel);
ixCnt += i_iBytesPerPixel;
}
}
}
//------------------------------------------------------------------------------
void main ()
{
int iWidth = 10000;
int iHeight = 10000;
int iBytesPerPixel = 3;
Bitmap^ oBitmap = gcnew Bitmap (iWidth, iHeight, PixelFormat::Format24bppRgb);
BitmapData^ oBitmapData = oBitmap->LockBits (Rectangle (0, 0, iWidth, iHeight), ImageLockMode::WriteOnly, oBitmap->PixelFormat);
array<unsigned char>^ aui8ImageData = gcnew array<unsigned char> (iWidth * iHeight * iBytesPerPixel);
int ixCnt = 0;
for (int ixRow = 0; ixRow < iHeight; ixRow++)
{
for (int ixCol = 0; ixCol < iWidth; ixCol++)
{
aui8ImageData[ixCnt++] = ixRow * 250 / iHeight;
aui8ImageData[ixCnt++] = ixCol * 250 / iWidth;
aui8ImageData[ixCnt++] = ixCol;
}
}
//========== Pointer ==========
// ~ 8.97 sec for 10k * 10k * 3 * 10 exec, ~ 334 MB/s
int iExec = 10;
DateTime dtStart = DateTime::Now;
for (int ixExec = 0; ixExec < iExec; ixExec++)
{
CopyIntoBitmap_Pointer (aui8ImageData, oBitmapData, iBytesPerPixel);
}
TimeSpan tsDuration = DateTime::Now - dtStart;
Console::WriteLine (tsDuration + " " + ((double)aui8ImageData->Length * iExec / tsDuration.TotalSeconds / 1e6));
//========== Marshal.Copy, 1 large block ==========
// 3.94 sec for 10k * 10k * 3 * 100 exec, ~ 7617 MB/s
iExec = 100;
dtStart = DateTime::Now;
for (int ixExec = 0; ixExec < iExec; ixExec++)
{
CopyIntoBitmap_MarshallLarge (aui8ImageData, oBitmapData);
}
tsDuration = DateTime::Now - dtStart;
Console::WriteLine (tsDuration + " " + ((double)aui8ImageData->Length * iExec / tsDuration.TotalSeconds / 1e6));
//========== Marshal.Copy, many small 3-byte blocks ==========
// 41.7 sec for 10k * 10k * 3 * 10 exec, ~ 72 MB/s
iExec = 10;
dtStart = DateTime::Now;
for (int ixExec = 0; ixExec < iExec; ixExec++)
{
CopyIntoBitmap_MarshalSmall (aui8ImageData, oBitmapData, iBytesPerPixel);
}
tsDuration = DateTime::Now - dtStart;
Console::WriteLine (tsDuration + " " + ((double)aui8ImageData->Length * iExec / tsDuration.TotalSeconds / 1e6));
//========== Buffer.BlockCopy, 1 large block ==========
// 4.02 sec for 10k * 10k * 3 * 100 exec, ~ 7467 MB/s
iExec = 100;
array<unsigned char>^ aui8Buffer = gcnew array<unsigned char> (aui8ImageData->Length);
dtStart = DateTime::Now;
for (int ixExec = 0; ixExec < iExec; ixExec++)
{
Buffer::BlockCopy (aui8ImageData, 0, aui8Buffer, 0, aui8ImageData->Length);
}
tsDuration = DateTime::Now - dtStart;
Console::WriteLine (tsDuration + " " + ((double)aui8ImageData->Length * iExec / tsDuration.TotalSeconds / 1e6));
//========== Buffer.BlockCopy, many small 3-byte blocks ==========
// 28.0 sec for 10k * 10k * 3 * 10 exec, ~ 107 MB/s
iExec = 10;
dtStart = DateTime::Now;
for (int ixExec = 0; ixExec < iExec; ixExec++)
{
int ixCnt = 0;
for (int ixRow = 0; ixRow < iHeight; ixRow++)
{
for (int ixCol = 0; ixCol < iWidth; ixCol++)
{
Buffer::BlockCopy (aui8ImageData, ixCnt, aui8Buffer, ixCnt, iBytesPerPixel);
ixCnt += iBytesPerPixel;
}
}
}
tsDuration = DateTime::Now - dtStart;
Console::WriteLine (tsDuration + " " + ((double)aui8ImageData->Length * iExec / tsDuration.TotalSeconds / 1e6));
oBitmap->UnlockBits (oBitmapData);
oBitmap->Save ("d:\\temp\\bitmap.bmp", ImageFormat::Bmp);
}
_
関連情報:
なぜmemcpy()およびmemmove()がポインタのインクリメントよりも速いのですか?
Array.Copy vs Buffer.BlockCopy 、回答 https://stackoverflow.com/a/33865267
https://github.com/dotnet/coreclr/issues/24 "Array.Copy&Buffer.BlockCopy x2 to x3 slower <1kB"
https://github.com/dotnet/coreclr/blob/master/src/vm/comutilnative.cpp 、執筆時の718行目:Buffer.BlockCopy()
はmemmove
を使用します