管理到非管理成本
在 .NET 中,有幾個地方您必須離開託管程式碼並進入非託管(即本機程式碼)領域。僅舉幾例:
- 外部 dll 函式
- COM 呼叫
總是有關於從一側跳到另一原因的成本的評論,我的問題是,是否有人測量了正在發生的確切成本,並可以解釋如何計算它。例如,也許
byte[]可以轉換為IntPtr甚至轉換為byte*.NET 並幫助 marshaller 節省一些 CPU 週期。
[我知道我並沒有真正回答關於如何衡量的問題;最好的測量方法是使用一些儀器,或者使用儀器類(參見: http: //msdn.microsoft.com/en-us/library/aa645516 (v=vs.71).aspx) 或什至像在您感興趣的任何呼叫周圍放置一些計時器一樣簡單。因此,以最粗略的形式,當我們試圖找到性能損失時,例如,在 C# 和 ATL COM 之間的呼叫中,我們只需在空函式呼叫周圍放置計時器,我們將啟動一個計時器,在 C# 和空 ATL COM 函式之間的緊密循環中執行,執行足夠多的循環,以便我們能夠在執行之間獲得合理一致的答案,然後執行在 C++ 中完全相同的東西。然後,這兩個數字之間的差異是跨越該邊界進行呼叫的成本。]
我真的沒有任何確切的數字,但我可以從以前的經驗中回答,只要你以一種有效的方式使用事物,C# 的執行成本很少(如果有的話)超出人們在 C++ 中可能預期的成本,具體取決於你正在嘗試做的事情的確切性質。
我在幾個應用程序上工作,這些應用程序通過非常高的頻率(100MHz-3GHz A/D 板)收集大量超聲波數據,並按照你的建議做某些事情(比如字節
$$ $$在託管程式碼中分配的數組,然後作為指針鎖定並作為數據的緩衝區傳遞;傳輸大量數據並對其進行處理以對各個部分進行成像)。 很久以前,我們用 C++ 程式碼與 VB6 通信,我們將 C++ 包裝在 ATL Simple COM 對像中,並在需要數據和成像時來回傳遞指針。很久以後,我們在 VS.NET 2003 中使用 C# 實踐了類似的技術。此外,我已經為這裡的一個問題編寫了一個庫,它允許大量非託管數據儲存,可以為非常大的數組和數組操作提供支持,甚至很多LINQ 類型的功能!使用數組欄位而不是大量對象。(注意:最新版本中的引用計數存在一些問題,我還沒有追查到。)
此外,我已經使用 ATL COM 與 FFTW 庫進行了一些介面,以便執行高性能 DSP 以獲得良好的效果,雖然該庫還沒有為黃金時段做好準備,但它是我為上面的連結創建的尖峰解決方案的基礎,並且峰值為我提供了我正在尋找的大部分資訊,以完成更全面的非託管記憶體分配器和快速數組處理,支持外部分配以及來自非託管堆的非託管分配,最終將取代處理目前存在於 FFTW C# 庫中。
所以,關鍵是,我認為性能損失被誇大了,尤其是考慮到我們現在擁有的處理能力。事實上,如果您只注意避免使用 C# 本身的一些缺陷(例如呼叫大量小的跨邊界函式而不是傳遞緩衝區,或者字元串的多次分配等),您可以獲得非常好的性能。但是在高速處理方面,C# 仍然可以滿足我提到的所有場景。是不是需要一點深謀遠慮,是的,有時。但是在開發速度、可維護性和可理解性方面獲得的優勢,我花在弄清楚如何獲得所需性能上的時間總是遠遠少於主要或完全使用 C++ 開發所需的時間。
我的兩個位。(哦,有一點需要注意,我特別提到 ATL COM 是因為您在使用 MFC 時所遭受的性能損失並不值得。我記得,通過 MFC COM 對象呼叫與ATL 一個,並不能滿足我們的需求。另一方面,ATL 只比直接在 C++ 中呼叫等效函式慢一點。抱歉,我不記得任何特定的數字,即使有大量的我們收集和移動的超聲波數據,我們沒有發現它是一個瓶頸。)
哦,我發現了這個:http: //msdn.microsoft.com/en-us/library/ms973839.aspx “.NET 應用程序中的性能提示和技巧”。我發現這句話很有趣:
為了加快過渡時間,盡可能使用 P/Invoke。如果需要數據編組,成本只有 31 條指令加上編組成本,否則只有 8 條。COM 互操作要貴得多,需要 65 條以上的指令。
範例部分標題:“進行粗塊呼叫”、“使用 For 循環進行字元串迭代”、“留意非同步 IO 機會”。
引用的快速記憶體庫中的一些片段:
在
MemoryArray.cspublic MemoryArray(int parElementCount, int parElementSize_bytes) { Descriptor = new MemoryArrayDescriptor ( Marshal.AllocHGlobal(parElementCount * parElementSize_bytes), parElementSize_bytes, parElementCount ); } protected override void OnDispose() { if (Descriptor.StartPointer != IntPtr.Zero) Marshal.FreeHGlobal(Descriptor.StartPointer); base.OnDispose(); } // this really should only be used for random access to the items, if you want sequential access // use the enumerator which uses pointer math via the array descriptor's TryMoveNext call. // // i haven't figured out exactly where it would go, but you could also do something like // having a member MemoryArrayItem that gets updated here rather than creating a new one each // time; that would break anything that was trying to hold on to a reference to the item because // it will no longer be immutable. // // that could be remedied by something like a call that would return a new copy of the item if it // was to be held onto. i would definitely need to see that i needed the performance boost and // that it was significant enough before i would contradict the users expectations on that one. public MemoryArrayItem this[int i] { get { return new MemoryArrayItem(this, Descriptor.GetElementPointer(i), Descriptor.ElementSize_bytes); } } // you could also do multiple dimension indexing; to do so you would have to pass in dimensions somehow in // the constructor and store them. // // there's all sorts of stuff you could do with this; take various slices, etc, do switching between // last-to-first/first-to-last/custom dimension ordering, etc, but i didn't tackle that for the example. // // if you don't need to error check here then just you could always do something like: public MemoryArrayItem this[int x, int y] { get { if (myDimensions == null) throw new ArrayTypeMismatchException("attempted to index two dimensional array without calling SetDimensions()"); if (myDimensions.Length != 2) throw new ArrayTypeMismatchException("currently set dimensions do not provide a two dimensional array. [dimension: " + myDimensions.Length + "]"); int RowSize_bytes = myDimensions[0] * Descriptor.ElementSize_bytes; return new MemoryArrayItem(this, Descriptor.StartPointer + (y * RowSize_bytes) + x * Descriptor.ElementSize_bytes, Descriptor.ElementSize_bytes); } } public void SetDimensions(int[] parDimensions) { if (parDimensions.Length <= 0) throw new Exception("unable to set array to dimension of zero."); for (int i = 0; i < parDimensions.Length; ++i) if (parDimensions[i] <= 0) throw new ArgumentOutOfRangeException("unable to set dimension at index " + i.ToString() + " to " + parDimensions[i] + "."); myDimensions = new int[parDimensions.Length]; parDimensions.CopyTo(myDimensions, 0); } private int[] myDimensions = null;從
MemoryArrayEnumerator.cspublic class MemoryArrayEnumerator : IEnumerator<MemoryArrayItem> { // handles reference counting for the main array private AutoReference<MemoryArray> myArray; private MemoryArray Array { get { return myArray; } } private IntPtr myCurrentPosition = IntPtr.Zero; public MemoryArrayEnumerator(MemoryArray parArray) { myArray = AutoReference<MemoryArray>.CreateFromExisting(parArray); } //--------------------------------------------------------------------------------------------------------------- #region IEnumerator<MemoryArrayItem> implementation //--------------------------------------------------------------------------------------------------------------- public MemoryArrayItem Current { get { if (Array.Descriptor.CheckPointer(myCurrentPosition)) return new MemoryArrayItem(myArray, myCurrentPosition, Array.Descriptor.ElementSize_bytes); else throw new IndexOutOfRangeException("Enumerator Error: Current() was out of range"); } } public void Dispose() { myArray.Dispose(); } object System.Collections.IEnumerator.Current { get { throw new NotImplementedException(); } } public bool MoveNext() { bool RetVal = true; if (myCurrentPosition == IntPtr.Zero) myCurrentPosition = Array.Descriptor.StartPointer; else RetVal = Array.Descriptor.TryMoveNext(ref myCurrentPosition); return RetVal; } public void Reset() { myCurrentPosition = IntPtr.Zero; } //--------------------------------------------------------------------------------------------------------------- #endregion IEnumerator<MemoryArrayItem> implementation //---------------------------------------------------------------------------------------------------------------
確實可以獲取託管數組的地址。
首先,您必須使用System.Runtime.InteropServices.GCHandle固定數組,以便垃圾收集器不會移動數組。只要非託管程式碼可以訪問託管數組,就必須保持分配此句柄。
byte[] the_array = ... ; GCHandle pin = GCHandle.Alloc(the_array, GCHandleType.Pinned);然後,您應該能夠使用System.Runtime.InteropServices.Marshal.UnsafeAddrOfPinnedArrayElement來獲取數組中任何元素的 IntPtr。
IntPtr p = Marshal.UnsafeAddrOfPinnedArrayElement(the_array,0);**重要提示:**固定對象會嚴重破壞 GC 操作。能夠在堆中移動對像是現代 GC 能夠(在某種程度上)跟上手動記憶體管理的原因之一。通過將對象固定在託管堆中,GC 失去了與手動記憶體管理相比的一個性能優勢:相對沒有碎片的堆。
因此,如果您打算將這些數組“在非託管方面”保留一段時間,請考慮製作數組的副本。複製記憶體速度驚人。使用這些
Marshal.Copy(*)方法從託管記憶體複製到非託管記憶體,反之亦然。