簡體   English   中英

C#性能 - 使用不安全的指針而不是IntPtr和Marshal

[英]C# performance - Using unsafe pointers instead of IntPtr and Marshal

我正在將C應用程序移植到C#中。 C應用程序從第三方DLL調用許多函數,所以我在C#中為這些函數編寫了P / Invoke包裝器。 其中一些C函數分配我必須在C#app中使用的數據,因此我使用IntPtrMarshal.PtrToStructureMarshal.Copy將本機數據(數組和結構)復制到托管變量中。

不幸的是,C#app被證明比C版慢得多。 快速的性能分析表明,上述基於編組的數據復制是瓶頸。 我正在考慮通過重寫它以使用指針來加速C#代碼。 由於我沒有C#中不安全的代碼和指針的經驗,我需要有關以下問題的專家意見:

  1. 使用unsafe代碼和指針而不是IntPtrMarshal ing有什么缺點? 例如,它是否以任何方式更不安全(雙關語)? 人們似乎更喜歡編組,但我不知道為什么。
  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();
}

C#應用程序,它使用IntPtrMarshal

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

C#應用程序,它使用unsafe代碼和指針

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結構:-))

我測試了以下三種解組策略(我也測試了編組)。 在幾乎所有情況下,第一個(MarshalMatters)的表現優於其他兩個。 Marshal.Copy總是最慢的,其他兩個在比賽中大多非常接近。

使用不安全的代碼可能會帶來嚴重的安全風險。

第一:

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

第三:

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

互操作性中的注意事項解釋了為什么以及何時需要編組以及以何種成本進行編組。 引用:

  1. 當調用者和被調用者無法對同一數據實例進行操作時,就會發生編組。
  2. 重復編組可能會對您的應用程序的性能產生負面影響。

因此,如果你回答你的問題

...使用P / Invoking的指針比使用編組更快...

如果托管代碼能夠在非托管方法返回值實例上運行,請首先問問自己一個問題。 如果答案是肯定的,則不需要Marshaling和相關的性能成本。 節省的大致時間是O(n)函數,其中編組實例的大小為n 此外,在方法持續時間內(在“IntPtr和Marshal”示例中)不同時將托管和非托管數據塊同時保存在內存中消除了額外的開銷和內存壓力。

使用不安全的代碼和指針有什么缺點......

缺點是與通過指針直接訪問存儲器相關的風險。 與在C或C ++中使用指針相比,沒有什么比這更安全了。 如果需要使用它並且有意義。 更多細節在這里

所提供的示例存在一個“安全”問題:在托管代碼錯誤之后無法保證釋放分配的非托管內存。 最好的做法是

CreateMyData(out myData1, length);

if(myData1!=IntPtr.Zero) {
    try {
        // -> use myData1
        ...
        // <-
    }
    finally {
        DestroyMyData(myData1);
    }
}

對於還在讀書的人

我不認為我在任何答案中看到的東西, - 不安全的代碼確實存在安全風險。 這不是一個巨大的風險,它將是一個非常具有挑戰性的東西。 但是,如果像我一樣,您在兼容PCI的組織中工作,則由於此原因,策略不允許使用不安全的代碼。

托管代碼通常非常安全,因為CLR負責內存位置和分配,阻止您訪問或寫入任何您不應該使用的內存。

當您使用unsafe關鍵字並使用'/ unsafe'編譯並使用指針時,您可以繞過這些檢查並創建某人使用您的應用程序獲得對其運行的計算機的某種程度的未授權訪問的可能性。 使用類似緩沖區溢出攻擊的東西,您的代碼可能會被欺騙,將指令寫入內存區域,然后可能被程序計數器訪問(即代碼注入),或者只是使機器崩潰。

許多年前,SQL服務器實際上已經成為TDS數據包中傳遞的惡意代碼的犧牲品,而TDS數據包遠遠超過預期。 讀取數據包的方法沒有檢查長度,並繼續將內容寫入保留的地址空間。 額外的長度和內容都經過精心設計,以便將整個程序寫入內存 - 在下一個方法的地址。 然后,攻擊者在具有最高訪問級別的上下文中由SQL服務器執行自己的代碼。 它甚至不需要破解加密,因為傳輸層堆棧中的漏洞低於此點。

兩個答案,

  1. 不安全的代碼意味着它不受CLR管理。 您需要處理它使用的資源。

  2. 您無法擴展性能,因為影響它的因素很多。 但絕對使用指針會快得多。

只是想把我的經驗添加到這個舊線程中:我們在錄音軟件中使用Marshaling - 我們從混音器接收實時聲音數據到本機緩沖區並將其封送到byte []。 那是真正的性能殺手。 我們被迫轉向不安全的結構作為完成任務的唯一方法。

如果您沒有大型原生結構,並且不介意所有數據都填充兩次 - Marshaling更優雅,更安全。

因為您聲明您的代碼調用第三方DLL,我認為不安全的代碼更適合您的情況。 您遇到了struct等待變長數組的特定情況; 我知道,我知道這種用法一直都在發生,但畢竟並非總是如此。 您可能想看看有關此問題的一些問題,例如:

如何將包含可變大小數組的結構封送到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 [], int)聲明了CreateMyDataAlt(byte[], ref int) CreateMyDataAlt(BYTE [], int) 一些數據復制和指針檢查變得不必要,也就是說,代碼可以更簡單,並且可能運行得更快。

那么,修改有什么不同呢? 現在,字節數組直接編組,而不會在struct進行扭曲並傳遞給非托管端。 您不在非托管代碼中分配內存,而只是向其填充數據(省略實現細節); 在通話之后,所需的數據被提供給管理方。 如果要表示數據未填充且不應使用,則只需將length設置為零即可告知管理方。 因為字節數組是在托管端分配的,所以有時會收集它,你不需要處理它。

我今天有同樣的問題,我正在尋找一些具體的測量值,但我找不到任何。 所以我寫了自己的測試。

測試是復制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
操作系統:Win 7 Pro x64 SP1
Visual Studio 2015.3,代碼為C ++ / CLI,目標.net版本為4.5.2,編譯為Debug。

檢測結果:
在所有方法中,1個內核的CPU負載為100%(相當於12.5%的總CPU負載)。
速度和執行時間的比較:

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/2430“Array.Copy&Buffer.BlockCopy x2 to x3 slow <1kB”
https://github.com/dotnet/coreclr/blob/master/src/vm/comutilnative.cpp ,撰寫本文時第718行: Buffer.BlockCopy()使用memmove

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM