62

質問

C アプリケーションを C# に移植しています。C アプリはサードパーティの DLL から多くの関数を呼び出すため、これらの関数の P/Invoke ラッパーを C# で作成しました。これらの C 関数の一部は、C# アプリで使用する必要があるデータを割り当てるため、 を使用しIntPtrMarshal.PtrToStructureネイティブMarshal.Copyデータ (配列と構造体) をマネージド変数にコピーしました。

残念ながら、C# アプリは C バージョンよりもはるかに遅いことが判明しました。簡単なパフォーマンス分析により、前述のマーシャリング ベースのデータ コピーがボトルネックであることがわかりました。代わりにポインターを使用するように書き直して、C# コードを高速化することを検討しています。私は C# で安全でないコードとポインターを使用した経験がないため、次の質問について専門家の意見が必要です。

  1. and ingunsafeの代わりにコードとポインターを使用することの欠点は何ですか? たとえば、何らかの方法でより安全でない (しゃれが意図されている) ことはありますか? 人々はマーシャリングを好むようですが、その理由はわかりません。IntPtrMarshal
  2. P/Invoking にポインターを使用すると、マーシャリングを使用するよりも本当に高速ですか? おおよそどのくらいのスピードアップが期待できますか? これに関するベンチマーク テストは見つかりませんでした。

サンプルコード

状況をより明確にするために、小さなサンプル コードをまとめました (実際のコードはもっと複雑です)。この例が、「安全でないコードとポインター」と「IntPtr と Marshal」について話しているときの意味を示していることを願っています。

C ライブラリ (DLL)

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);
  }
}

C アプリケーション

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とを使用する C# アプリケーションMarshal

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);
  }
}
4

7 に答える 7

45

少し古いスレッドですが、最近 C# のマーシャリングで過剰なパフォーマンス テストを行いました。何日もかけてシリアルポートから大量のデータをアンマーシャリングする必要があります。メモリ リークがないことは私にとって重要でした (数百万回の呼び出しの後に最小のリークが重要になるため)。念のために(いいえ、10kbの構造体を持つべきではありません:-))

次の 3 つのアンマーシャリング戦略をテストしました (マーシャリングもテストしました)。ほぼすべてのケースで、最初のもの (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;
    }
}

2番:

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;
    }
}

三番:

/// <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;
    }
}
于 2015-04-23T23:45:42.090 に答える
14

相互運用性の考慮事項では、マーシャリングが必要な理由と時期、およびコストについて説明しています。見積もり:

  1. マーシャリングは、呼び出し元と呼び出し先がデータの同じインスタンスを操作できない場合に発生します。
  2. マーシャリングを繰り返すと、アプリケーションのパフォーマンスに悪影響を及ぼす可能性があります。

したがって、あなたの質問に答える

... マーシャリングを使用するよりも本当に高速な P/Invoking のポインターを使用する ...

まず、マネージ コードがアンマネージ メソッドの戻り値のインスタンスを操作できるかどうかを自問してください。答えが「はい」の場合、マーシャリングと関連するパフォーマンス コストは必要ありません。おおよその時間の節約はO(n)関数で、nはマーシャリングされたインスタンスのサイズです。さらに、メソッドの実行中 (「IntPtr と Marshal」の例) にマネージ ブロックとアンマネージ データ ブロックの両方を同時にメモリに保持しないと、追加のオーバーヘッドとメモリ プレッシャーがなくなります。

安全でないコードとポインターを使用することの欠点は何ですか...

欠点は、ポインタを介してメモリに直接アクセスすることに伴うリスクです。C または C++ でポインタを使用することほど安全なことはありません。必要に応じて使用し、理にかなっています。詳細はこちら

提示された例には "安全性" に関する懸念が 1 つあります。マネージ コード エラーの後で、割り当てられたアンマネージ メモリの解放は保証されません。ベスト プラクティスは、

CreateMyData(out myData1, length);

if(myData1!=IntPtr.Zero) {
    try {
        // -> use myData1
        ...
        // <-
    }
    finally {
        DestroyMyData(myData1);
    }
}
于 2018-01-10T13:59:09.687 に答える
5

この古いスレッドに私の経験を追加したかっただけです: 私たちは録音ソフトウェアでマーシャリングを使用しました - ミキサーからネイティブ バッファにリアルタイムのサウンド データを受け取り、それを byte[] にマーシャリングしました。それは本当のパフォーマンスキラーでした。タスクを完了する唯一の方法として、安全でない構造体に移動することを余儀なくされました。

大きなネイティブ構造体がなく、すべてのデータが 2 回入力されることを気にしない場合 - マーシャリングはよりエレガントで、はるかに安全なアプローチです。

于 2015-02-12T16:38:50.570 に答える
4

二つの答え、

  1. アンセーフ コードは、CLR によって管理されていないことを意味します。使用するリソースに注意する必要があります。

  2. パフォーマンスに影響を与える要因が非常に多いため、パフォーマンスをスケーリングすることはできません。しかし、確実にポインターを使用すると、はるかに高速になります。

于 2013-07-15T06:18:32.157 に答える
3

あなたのコードはサードパーティの DLL を呼び出すと述べたので、あなたのシナリオには安全でないコードの方が適していると思います。;で可変長配列を wappingstructするという特定の状況に遭遇しました。私は知っています、私はこの種の使用法が常に発生していることを知っていますが、結局のところ常にそうであるとは限りません. これに関するいくつかの質問を見てみたいと思うかもしれません。例えば:

可変サイズの配列を含む構造体を 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にゼロに設定して、管理対象側に伝えることができます。バイト配列はマネージ側で確保されるため、いずれ回収されるので気にする必要はありません。

于 2018-01-05T20:11:06.580 に答える
2

今日も同じ質問があり、具体的な測定値を探しましたが、見つかりませんでした。だから私は自分のテストを書きました。

このテストでは、10k x 10k RGB 画像のピクセル データをコピーしています。画像データは 300 MB (3*10^9 バイト) です。このデータを 10 回コピーするメソッドもあれば、高速なため 100 回コピーするメソッドもあります。使用されるコピー方法には、

  • バイトポインタによる配列アクセス
  • Marshal.Copy(): a) 1 * 300 MB、b) 1e9 * 3 バイト
  • Buffer.BlockCopy(): a) 1 * 300 MB、b) 1e9 * 3 バイト

テスト環境:
CPU: Intel Core i7-3630QM @ 2.40 GHz
OS: Win 7 Pro x64 SP1
Visual Studio 2015.3、コードは C++/CLI、対象の .net バージョンは 4.5.2、デバッグ用にコンパイル。

テスト結果:
CPU 負荷は、すべての方法で 1 コアに対して 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 と Buffer.BlockCopy、回答 https://stackoverflow.com/a/33865267
https://github.com/dotnet/coreclr/issues/2430「Array.Copy & Buffer.BlockCopy x2 から x3 への速度が 1kB 未満」
https://github.com/dotnet/coreclr/blob/master/src/vm/comutilnative.cpp、執筆時点の 718 行目: Buffer.BlockCopy()usesmemmove

于 2018-10-27T06:50:38.363 に答える