管理不受管理的开销

时间:2011-08-16 05:46:20

标签: .net performance unmanaged

在.NET中,有几个地方必须离开托管代码并进入非托管的a.k.a.本机代码领域。仅举几例:

  • extern dll functions
  • COM调用

总是有关于从一侧跳到另一侧的开销的评论,我的问题是,如果有人测量了正在发生的确切开销,并且可以解释如何计算它。例如,可以在.NET中将byte[]转换为IntPtr或甚至转换为byte*,并帮助编组程序节省一些CPU周期。

2 个答案:

答案 0 :(得分:6)

确实可以获取托管数组的地址。

首先,您必须使用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(*)方法从托管内存复制到非托管内存,反之亦然。

答案 1 :(得分:5)

[我看到我没有真正回答关于你将如何衡量的问题;测量的最佳方法就是使用一些仪器,或者使用仪器类(参见:http://msdn.microsoft.com/en-us/library/aa645516(v=vs.71).aspx),或者甚至使用一些简单的东西来放置一些定时器来围绕你感兴趣的任何调用。所以,在最粗糙的表单,当我们试图找到性能命中时,例如,在C#和ATL COM之间的调用中,我们只是将定时器放在空函数调用周围,我们将启动一个定时器,在C#和C#之间的紧密循环中运行空的ATL COM函数,做足够的循环,我们能够在运行之间得到合理一致的答案,然后在C ++中做同样的事情。然后,这两个数字之间的差异是跨越该边界进行调用的开销。]

我真的没有任何硬数字,但我可以回答以前的经验,只要你以有效的方式使用东西,C#执行的开销很小,如果有的话,超出了人们对C ++的期望,取决于你想要做的事情的确切性质。

我研究了几种通过非常高的频率(100MHz-3GHz A / D板)收集大量超声波数据的应用程序,并按照你的建议做了一些事情(比如托管代码中分配的byte []数组和然后锁定为指针并作为数据的缓冲区传递;传输大量数据并处理它以对各个部分进行成像。)

当我们将C ++代码与VB6进行通信时,我们将C ++包装在ATL Simple COM对象中,并在需要数据和映像时来回传递指针。我们在VS.NET 2003中使用C#实现了类似的技术。此外,我在这里编写了一个问题库,允许大量非托管数据存储,可以为非常大的数组和数组操作提供支持,甚至还有很多LINQ类功能! Using array fields instead of massive number of objects。 (注意:最新版本中的引用计数存在一些问题,我还没有跟踪过。)

另外,我已经使用ATL COM与FFTW库进行了一些接口,以便执行高性能DSP以达到良好的效果,虽然该库还没有为黄金时段做好准备,但它是我为其创建的尖峰解决方案的基础上面的链接,spike给了我大部分信息,我正在寻找完成我更全面的非托管内存分配器和快速阵列处理,支持从非托管堆的外部分配和非托管分配,最终将替换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条指令。

示例部分标题:“Make Chunky Calls”,“Use for Forops for String Iteration”,“Be on the Lookout for Asynchronous IO Opportunities”。


引用的快速内存库中的一些片段:

MemoryArray.cs

中的

    public 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.cs

public 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
    //---------------------------------------------------------------------------------------------------------------