.Net GC新增的API

起因

在.Net 5中,在GC的公开方法中,增加了几个新的API.这几个API可能在工作用不到.这里也是学习一下.
  1. AllocateArray 在托管堆根据长度分配数组,对数组对象分配默认值
  2. AllocateUninitializedArray 在托管堆根据长度分配数组,不对对象的属性分配默认值
  3. GetAllocatedBytesForCurrentThread  线程的生存期内在托管堆上分配的总字节数
在.Net 5 GC新增的API

1. 在项目中经常使用的方式,分配数组

public void ArrayTest()
{
    People[] peoples = new People[16];
    Console.WriteLine(peoples.Length);
}

2. 在托管堆分配数组AllocateArray

public void AllocateArrayTest()
{
    //从.Net 5开始支持
    //AllocateArray 在托管堆根据长度分配数组,对数组对象分配默认值
    //第一个参数是需要分配数组的长度
    //第二个参数是否在GC中固定
    People[] arr = GC.AllocateArray<People>(16);
    Console.WriteLine(arr.Length);
}

AllocateArray源码在src/coreclr/System.Private.CoreLib/src/System/GC.cs

public static T[] AllocateArray<T>(int length, bool pinned = false) // T[] rather than T?[] to match `new T[length]` behavior
{
    GC_ALLOC_FLAGS flags = GC_ALLOC_FLAGS.GC_ALLOC_NO_FLAGS;

    if (pinned)
    {
        if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
            ThrowHelper.ThrowInvalidTypeWithPointersNotSupported(typeof(T));

        flags = GC_ALLOC_FLAGS.GC_ALLOC_PINNED_OBJECT_HEAP;
    }

    return Unsafe.As<T[]>(AllocateNewArray(typeof(T[]).TypeHandle.Value, length, flags));
}
/// <summary>
/// Allocate an array.
/// </summary>
/// <typeparam name="T">Specifies the type of the array element.</typeparam>
/// <param name="length">Specifies the length of the array.</param>
/// <param name="pinned">Specifies whether the allocated array must be pinned.</param>
/// <remarks>
/// If pinned is set to true, <typeparamref name="T"/> must not be a reference type or a type that contains object references.
/// </remarks>
public static T[] AllocateArray<T>(int length, bool pinned = false) // T[] rather than T?[] to match `new T[length]` behavior
{
    GC_ALLOC_FLAGS flags = GC_ALLOC_FLAGS.GC_ALLOC_NO_FLAGS;

    if (pinned)
    {
        if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
            ThrowHelper.ThrowInvalidTypeWithPointersNotSupported(typeof(T));

        flags = GC_ALLOC_FLAGS.GC_ALLOC_PINNED_OBJECT_HEAP;     //是否需要在托管堆上固定
    }

    //内部使用AllocateNewArray,这个API并不是c#实现
    return Unsafe.As<T[]>(AllocateNewArray(typeof(T[]).TypeHandle.Value, length, flags));
}
[MethodImpl(MethodImplOptions.InternalCall)] //表明AllocateNewArray是在CLR内部实现
internal static extern Array AllocateNewArray(IntPtr typeHandle, int length, GC_ALLOC_FLAGS flags);

AllocateArray内部调用AllocateNewArray,具体实现是在CLR中实现.后面我们去跟AllocateNewArray源码实现.

3. AllocateUninitializedArray在托管堆上分配数组

public void AllocateUninitializedArrayTest()
{
    //从.Net 5开始支持
    //AllocateUninitializedArray 在托管堆根据长度分配数组,不对对象的属性分配默认值
    //第一个参数是需要分配数组的长度
    //第二个参数是否在GC中固定
    People[] arr = GC.AllocateUninitializedArray<People>(16);
    Console.WriteLine(arr.Length);
}

AllocateUninitializedArray源码在src/coreclr/System.Private.CoreLib/src/System/GC.cs

/// <summary>
/// Allocate an array while skipping zero-initialization if possible.
/// </summary>
/// <typeparam name="T">Specifies the type of the array element.</typeparam>
/// <param name="length">Specifies the length of the array.</param>
/// <param name="pinned">Specifies whether the allocated array must be pinned.</param>
/// <remarks>
/// If pinned is set to true, <typeparamref name="T"/> must not be a reference type or a type that contains object references.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)] // forced to ensure no perf drop for small memory buffers (hot path)
public static T[] AllocateUninitializedArray<T>(int length, bool pinned = false) // T[] rather than T?[] to match `new T[length]` behavior
{
    if (!pinned)            //不固定在堆上固定对象
    {
        if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())  //如果是引用类型,直接根据类型使用直接用分组的方式
        {
            return new T[length];
        }

        // for debug builds we always want to call AllocateNewArray to detect AllocateNewArray bugs
#if !DEBUG
        // small arrays are allocated using `new[]` as that is generally faster.
        if (length < 2048 / Unsafe.SizeOf<T>())                 //数组长度较小的情况,也是使用直接的方式在堆上分配数组
        {
            return new T[length];
        }
#endif
    }
    else if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())   //如果该类型是引用类型并包含其他引用类型,会抛出异常
    {
        ThrowHelper.ThrowInvalidTypeWithPointersNotSupported(typeof(T));
    }

    // kept outside of the small arrays hot path to have inlining without big size growth
    return AllocateNewUninitializedArray(length, pinned);

    // remove the local function when https://github.com/dotnet/runtime/issues/5973 is implemented
    static T[] AllocateNewUninitializedArray(int length, bool pinned)
    {
        GC_ALLOC_FLAGS flags = GC_ALLOC_FLAGS.GC_ALLOC_ZEROING_OPTIONAL;
        if (pinned)
            flags |= GC_ALLOC_FLAGS.GC_ALLOC_PINNED_OBJECT_HEAP;

        return Unsafe.As<T[]>(AllocateNewArray(typeof(T[]).TypeHandle.Value, length, flags)); //重点还是AllocateNewArray,下面我们便去扒扒源码是如何实现的
    }
}

4. AllocateNewArray 在CLR是如何实现的

在.Net 看到MethodImplOptions.InternalCall标记的方法
    在CoreCLR中一般可以先去src/coreclr/vm/ecalllist.h查找
    在Mono中一般可以先去src/mono/mono/metadata/icall.c查找

在ecalllist.h文件中可以看到:
FCFuncElement("AllocateNewArray", GCInterface::AllocateNewArray)

根据GCInterface找不到对应cpp文件和头文件.如果用VS打开的话,可以根据解决方案中查找.

不过最后在src/coreclr/vm/comutilnative.cpp文件找到了.内部涉及的东西比较多.这里先不多源码进行解析,等后面有经验在来分析.

/*===============================AllocateNewArray===============================
**Action: Allocates a new array object. Allows passing extra flags
**Returns: The allocated array.
**Arguments: elementTypeHandle -> type of the element,
**           length -> number of elements,
**           zeroingOptional -> whether caller prefers to skip clearing the content of the array, if possible.
**Exceptions: IDS_EE_ARRAY_DIMENSIONS_EXCEEDED when size is too large. OOM if can't allocate.
==============================================================================*/
FCIMPL3(Object*, GCInterface::AllocateNewArray, void* arrayTypeHandle, INT32 length, INT32 flags)
{
    CONTRACTL {
        FCALL_CHECK;
    } CONTRACTL_END;

    OBJECTREF pRet = NULL;
    TypeHandle arrayType = TypeHandle::FromPtr(arrayTypeHandle);

    HELPER_METHOD_FRAME_BEGIN_RET_0();

    //Only the following flags are used by GC.cs, so we'll just assert it here.
    _ASSERTE((flags & ~(GC_ALLOC_ZEROING_OPTIONAL | GC_ALLOC_PINNED_OBJECT_HEAP)) == 0);

    pRet = AllocateSzArray(arrayType, length, (GC_ALLOC_FLAGS)flags);   //这里

    HELPER_METHOD_FRAME_END();

    return OBJECTREFToObject(pRet);
}
FCIMPLEND
OBJECTREF AllocateSzArray(TypeHandle arrayType, INT32 cElements, GC_ALLOC_FLAGS flags)
{
    CONTRACTL{
        THROWS;
        GC_TRIGGERS;
        MODE_COOPERATIVE; // returns an objref without pinning it => cooperative
    } CONTRACTL_END;

    MethodTable* pArrayMT = arrayType.AsMethodTable();

    return AllocateSzArray(pArrayMT, cElements, flags);
}

OBJECTREF AllocateSzArray(MethodTable* pArrayMT, INT32 cElements, GC_ALLOC_FLAGS flags)
{
    CONTRACTL{
        THROWS;
        GC_TRIGGERS;
        MODE_COOPERATIVE; // returns an objref without pinning it => cooperative
    } CONTRACTL_END;

    // IBC Log MethodTable access
    g_IBCLogger.LogMethodTableAccess(pArrayMT);
    SetTypeHandleOnThreadForAlloc(TypeHandle(pArrayMT));

    _ASSERTE(pArrayMT->CheckInstanceActivated());
    _ASSERTE(pArrayMT->GetInternalCorElementType() == ELEMENT_TYPE_SZARRAY);

    CorElementType elemType = pArrayMT->GetArrayElementType();

    // Disallow the creation of void[] (an array of System.Void)
    if (elemType == ELEMENT_TYPE_VOID)
        COMPlusThrow(kArgumentException);

    if (cElements < 0)
        COMPlusThrow(kOverflowException);

    if ((SIZE_T)cElements > MaxArrayLength())
        ThrowOutOfMemoryDimensionsExceeded();

    // Allocate the space from the GC heap
    SIZE_T componentSize = pArrayMT->GetComponentSize();
#ifdef TARGET_64BIT
    // POSITIVE_INT32 * UINT16 + SMALL_CONST
    // this cannot overflow on 64bit
    size_t totalSize = cElements * componentSize + pArrayMT->GetBaseSize();

#else
    S_SIZE_T safeTotalSize = S_SIZE_T((DWORD)cElements) * S_SIZE_T((DWORD)componentSize) + S_SIZE_T((DWORD)pArrayMT->GetBaseSize());
    if (safeTotalSize.IsOverflow())
        ThrowOutOfMemoryDimensionsExceeded();

    size_t totalSize = safeTotalSize.Value();
#endif

#ifdef FEATURE_DOUBLE_ALIGNMENT_HINT
    if ((elemType == ELEMENT_TYPE_R8) &&
        ((DWORD)cElements >= g_pConfig->GetDoubleArrayToLargeObjectHeapThreshold()))
    {
        STRESS_LOG2(LF_GC, LL_INFO10, "Allocating double MD array of size %d and length %d to large object heap\n", totalSize, cElements);
        flags |= GC_ALLOC_LARGE_OBJECT_HEAP;
    }
#endif

    if (totalSize >= g_pConfig->GetGCLOHThreshold())
        flags |= GC_ALLOC_LARGE_OBJECT_HEAP;

    if (pArrayMT->ContainsPointers())
        flags |= GC_ALLOC_CONTAINS_REF;

    ArrayBase* orArray = NULL;
    if (flags & GC_ALLOC_USER_OLD_HEAP)                 //如果还在原先堆上
    {
        orArray = (ArrayBase*)Alloc(totalSize, flags);  //在这里分配内存空间,Alloc内部是调用malloc
        orArray->SetMethodTableForUOHObject(pArrayMT);
    }
    else
    {
#ifndef FEATURE_64BIT_ALIGNMENT
        if ((DATA_ALIGNMENT < sizeof(double)) && (elemType == ELEMENT_TYPE_R8) &&
            (totalSize < g_pConfig->GetGCLOHThreshold() - MIN_OBJECT_SIZE))
        {
            // Creation of an array of doubles, not in the large object heap.
            // We want to align the doubles to 8 byte boundaries, but the GC gives us pointers aligned
            // to 4 bytes only (on 32 bit platforms). To align, we ask for 12 bytes more to fill with a
            // dummy object.
            // If the GC gives us a 8 byte aligned address, we use it for the array and place the dummy
            // object after the array, otherwise we put the dummy object first, shifting the base of
            // the array to an 8 byte aligned address.
            //
            // Note: on 64 bit platforms, the GC always returns 8 byte aligned addresses, and we don't
            // execute this code because DATA_ALIGNMENT < sizeof(double) is false.

            _ASSERTE(DATA_ALIGNMENT == sizeof(double) / 2);
            _ASSERTE((MIN_OBJECT_SIZE % sizeof(double)) == DATA_ALIGNMENT);   // used to change alignment
            _ASSERTE(pArrayMT->GetComponentSize() == sizeof(double));
            _ASSERTE(g_pObjectClass->GetBaseSize() == MIN_OBJECT_SIZE);
            _ASSERTE(totalSize < totalSize + MIN_OBJECT_SIZE);
            orArray = (ArrayBase*)Alloc(totalSize + MIN_OBJECT_SIZE, flags);  //进行内存分配

            Object* orDummyObject;
            if ((size_t)orArray % sizeof(double))
            {
                orDummyObject = orArray;
                orArray = (ArrayBase*)((size_t)orArray + MIN_OBJECT_SIZE);
            }
            else
            {
                orDummyObject = (Object*)((size_t)orArray + totalSize);
            }
            _ASSERTE(((size_t)orArray % sizeof(double)) == 0);
            orDummyObject->SetMethodTable(g_pObjectClass);
        }
        else
#endif  // FEATURE_64BIT_ALIGNMENT
        {
#ifdef FEATURE_64BIT_ALIGNMENT
            MethodTable* pElementMT = pArrayMT->GetArrayElementTypeHandle().GetMethodTable();
            if (pElementMT->RequiresAlign8() && pElementMT->IsValueType())
            {
                // This platform requires that certain fields are 8-byte aligned (and the runtime doesn't provide
                // this guarantee implicitly, e.g. on 32-bit platforms). Since it's the array payload, not the
                // header that requires alignment we need to be careful. However it just so happens that all the
                // cases we care about (single and multi-dim arrays of value types) have an even number of DWORDs
                // in their headers so the alignment requirements for the header and the payload are the same.
                _ASSERTE(((pArrayMT->GetBaseSize() - SIZEOF_OBJHEADER) & 7) == 0);
                flags |= GC_ALLOC_ALIGN8;
            }
#endif
            orArray = (ArrayBase*)Alloc(totalSize, flags);  //进行内存分配
        }
        orArray->SetMethodTable(pArrayMT);
    }

    // Initialize Object
    orArray->m_NumComponents = cElements;

    PublishObjectAndNotify(orArray, flags);
    return ObjectToOBJECTREF((Object*)orArray);
}

5. GetAllocatedBytesForCurrentThread 线程的生存期内在托管堆上分配的总字节数

public void GetAllocatedBytesForCurrentThreadTest()
{
    long currentThreadAllocateBytes = GC.GetAllocatedBytesForCurrentThread(); //线程的生存期内在托管堆上分配的总字节数
    Console.WriteLine(currentThreadAllocateBytes);
}

GetAllocatedBytesForCurrentThread源码也是在CoreCLR实现.

在src/coreclr/vm/comutilnative.cpp

/*===============================GetAllocatedBytesForCurrentThread===============================
**Action: Computes the allocated bytes so far on the current thread
**Returns: The allocated bytes so far on the current thread
**Arguments: None
**Exceptions: None
==============================================================================*/
FCIMPL0(INT64, GCInterface::GetAllocatedBytesForCurrentThread)
{
    FCALL_CONTRACT;

    INT64 currentAllocated = 0;
    Thread *pThread = GetThread();                      //获取当前线程
    gc_alloc_context* ac = pThread->GetAllocContext();  //获取当前线程的内存分配的上下文
    currentAllocated = ac->alloc_bytes + ac->alloc_bytes_uoh - (ac->alloc_limit - ac->alloc_ptr);

    return currentAllocated;
}
FCIMPLEND


秋风 2021-07-18