未验证 提交 5a45085c 编写于 作者: A Aaron Robinson 提交者: GitHub

Remove fragile NGen logic for precomputed hashcodes (#82563)

* Remove fragile NGen logic for precomputed hashcodes

Hashing was confirmed to offer minor performance wins
but not with lazy hash code generation. Pre hashing was needed
to have a benefit on start-up but even then it was minor.
上级 ae4ba7fb
......@@ -994,9 +994,6 @@ internal static MdUtf8String GetUtf8Name(RuntimeMethodHandleInternal method)
return new MdUtf8String(_GetUtf8Name(method));
}
[MethodImpl(MethodImplOptions.InternalCall)]
internal static extern bool MatchesNameHash(RuntimeMethodHandleInternal method, uint hash);
[DebuggerStepThrough]
[DebuggerHidden]
[MethodImpl(MethodImplOptions.InternalCall)]
......@@ -1254,9 +1251,6 @@ public static RuntimeFieldHandle FromIntPtr(IntPtr value)
internal static MdUtf8String GetUtf8Name(RuntimeFieldHandleInternal field) { return new MdUtf8String(_GetUtf8Name(field)); }
[MethodImpl(MethodImplOptions.InternalCall)]
internal static extern bool MatchesNameHash(RuntimeFieldHandleInternal handle, uint hash);
[MethodImpl(MethodImplOptions.InternalCall)]
internal static extern FieldAttributes GetAttributes(RuntimeFieldHandleInternal field);
......
......@@ -154,18 +154,11 @@ internal enum CacheType
{
private readonly MdUtf8String m_name;
private readonly MemberListType m_listType;
private readonly uint m_nameHash;
public unsafe Filter(byte* pUtf8Name, int cUtf8Name, MemberListType listType)
{
m_name = new MdUtf8String(pUtf8Name, cUtf8Name);
m_listType = listType;
m_nameHash = 0;
if (RequiresStringComparison())
{
m_nameHash = m_name.HashCaseInsensitive();
}
}
public bool Match(MdUtf8String name)
......@@ -186,7 +179,6 @@ public bool Match(MdUtf8String name)
// Does the current match type require a string comparison?
// If not, we know Match will always return true and the call can be skipped
// If so, we know we can have a valid hash to check against from GetHashToMatch
public bool RequiresStringComparison()
{
return (m_listType == MemberListType.CaseSensitive) ||
......@@ -194,13 +186,6 @@ public bool RequiresStringComparison()
}
public bool CaseSensitive() => m_listType == MemberListType.CaseSensitive;
public uint GetHashToMatch()
{
Debug.Assert(RequiresStringComparison());
return m_nameHash;
}
}
private sealed class MemberInfoCache<T> where T : MemberInfo
......@@ -622,12 +607,6 @@ private unsafe RuntimeMethodInfo[] PopulateMethods(Filter filter)
{
if (filter.RequiresStringComparison())
{
if (!RuntimeMethodHandle.MatchesNameHash(methodHandle, filter.GetHashToMatch()))
{
Debug.Assert(!filter.Match(RuntimeMethodHandle.GetUtf8Name(methodHandle)));
continue;
}
if (!filter.Match(RuntimeMethodHandle.GetUtf8Name(methodHandle)))
continue;
}
......@@ -684,12 +663,6 @@ private unsafe RuntimeMethodInfo[] PopulateMethods(Filter filter)
{
if (filter.RequiresStringComparison())
{
if (!RuntimeMethodHandle.MatchesNameHash(methodHandle, filter.GetHashToMatch()))
{
Debug.Assert(!filter.Match(RuntimeMethodHandle.GetUtf8Name(methodHandle)));
continue;
}
if (!filter.Match(RuntimeMethodHandle.GetUtf8Name(methodHandle)))
continue;
}
......@@ -793,12 +766,6 @@ private RuntimeConstructorInfo[] PopulateConstructors(Filter filter)
{
if (filter.RequiresStringComparison())
{
if (!RuntimeMethodHandle.MatchesNameHash(methodHandle, filter.GetHashToMatch()))
{
Debug.Assert(!filter.Match(RuntimeMethodHandle.GetUtf8Name(methodHandle)));
continue;
}
if (!filter.Match(RuntimeMethodHandle.GetUtf8Name(methodHandle)))
continue;
}
......@@ -922,12 +889,6 @@ private unsafe void PopulateRtFields(Filter filter, RuntimeType declaringType, r
if (filter.RequiresStringComparison())
{
if (!RuntimeFieldHandle.MatchesNameHash(runtimeFieldHandle, filter.GetHashToMatch()))
{
Debug.Assert(!filter.Match(RuntimeFieldHandle.GetUtf8Name(runtimeFieldHandle)));
continue;
}
if (!filter.Match(RuntimeFieldHandle.GetUtf8Name(runtimeFieldHandle)))
continue;
}
......@@ -4196,9 +4157,6 @@ private enum DispatchWrapperType : int
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool EqualsCaseInsensitive(void* szLhs, void* szRhs, int cSz);
[LibraryImport(RuntimeHelpers.QCall, EntryPoint = "MdUtf8String_HashCaseInsensitive")]
private static partial uint HashCaseInsensitive(void* sz, int cSz);
private readonly byte* m_pStringHeap; // This is the raw UTF8 string.
private readonly int m_StringHeapByteLength;
......@@ -4249,11 +4207,6 @@ internal bool EqualsCaseInsensitive(MdUtf8String s)
}
}
internal uint HashCaseInsensitive()
{
return HashCaseInsensitive(m_pStringHeap, m_StringHeapByteLength);
}
public override string ToString()
=> Encoding.UTF8.GetString(new ReadOnlySpan<byte>(m_pStringHeap, m_StringHeapByteLength));
}
......
......@@ -223,7 +223,6 @@ FCFuncStart(gRuntimeMethodHandle)
FCFuncElement("GetMethodDef", RuntimeMethodHandle::GetMethodDef)
FCFuncElement("GetName", RuntimeMethodHandle::GetName)
FCFuncElement("_GetUtf8Name", RuntimeMethodHandle::GetUtf8Name)
FCFuncElement("MatchesNameHash", RuntimeMethodHandle::MatchesNameHash)
FCFuncElement("HasMethodInstantiation", RuntimeMethodHandle::HasMethodInstantiation)
FCFuncElement("IsGenericMethodDefinition", RuntimeMethodHandle::IsGenericMethodDefinition)
FCFuncElement("GetGenericParameterCount", RuntimeMethodHandle::GetGenericParameterCount)
......@@ -244,7 +243,6 @@ FCFuncStart(gCOMFieldHandleNewFuncs)
FCFuncElement("SetValueDirect", RuntimeFieldHandle::SetValueDirect)
FCFuncElement("GetName", RuntimeFieldHandle::GetName)
FCFuncElement("_GetUtf8Name", RuntimeFieldHandle::GetUtf8Name)
FCFuncElement("MatchesNameHash", RuntimeFieldHandle::MatchesNameHash)
FCFuncElement("GetAttributes", RuntimeFieldHandle::GetAttributes)
FCFuncElement("GetApproxDeclaringType", RuntimeFieldHandle::GetApproxDeclaringType)
FCFuncElement("GetToken", RuntimeFieldHandle::GetToken)
......
......@@ -69,7 +69,6 @@ VOID FieldDesc::Init(mdFieldDef mb, CorElementType FieldType, DWORD dwMemberAttr
_ASSERTE(fIsStatic || (!fIsRVA && !fIsThreadLocal));
_ASSERTE(fIsRVA + fIsThreadLocal <= 1);
m_requiresFullMbValue = 0;
SetMemberDef(mb);
// A TypedByRef should be treated like a regular value type.
......@@ -105,30 +104,6 @@ BOOL FieldDesc::IsByRef()
return CorTypeInfo::IsByRef_NoThrow(GetFieldType());
}
BOOL FieldDesc::MightHaveName(ULONG nameHashValue)
{
LIMITED_METHOD_CONTRACT;
// We only have space for a name hash when we are using the packed mb layout
if (m_requiresFullMbValue)
{
return TRUE;
}
ULONG thisHashValue = m_mb & enum_packedMbLayout_NameHashMask;
// A zero value might mean no hash has ever been set
// (checking this way is better than dedicating a bit to tell us)
if (thisHashValue == 0)
{
return TRUE;
}
ULONG testHashValue = nameHashValue & enum_packedMbLayout_NameHashMask;
return (thisHashValue == testHashValue);
}
#ifndef DACCESS_COMPILE //we don't require DAC to special case simple types
// Return the type of the field, as a class, but only if it's loaded.
TypeHandle FieldDesc::LookupFieldTypeHandle(ClassLoadLevel level, BOOL dropGenericArgumentLevel)
......
......@@ -45,8 +45,6 @@ class FieldDesc
unsigned m_dword1;
struct {
#endif
// Note that we may store other information in the high bits if available --
// see enum_packedMBLayout and m_requiresFullMbValue for details.
unsigned m_mb : 24;
// 8 bits...
......@@ -54,8 +52,6 @@ class FieldDesc
unsigned m_isThreadLocal : 1;
unsigned m_isRVA : 1;
unsigned m_prot : 3;
// Does this field's mb require all 24 bits
unsigned m_requiresFullMbValue : 1;
#if defined(DACCESS_COMPILE)
};
};
......@@ -93,7 +89,6 @@ public:
m_isThreadLocal = sourceField.m_isThreadLocal;
m_isRVA = sourceField.m_isRVA;
m_prot = sourceField.m_prot;
m_requiresFullMbValue = sourceField.m_requiresFullMbValue;
m_dwOffset = sourceField.m_dwOffset;
m_type = sourceField.m_type;
......@@ -131,43 +126,15 @@ public:
BOOL fIsThreadLocal,
LPCSTR pszFieldName);
enum {
enum_packedMbLayout_MbMask = 0x01FFFF,
enum_packedMbLayout_NameHashMask = 0xFE0000
};
void SetMemberDef(mdFieldDef mb)
{
WRAPPER_NO_CONTRACT;
// Check if we have to avoid using the packed mb layout
if (RidFromToken(mb) > enum_packedMbLayout_MbMask)
{
m_requiresFullMbValue = 1;
}
// Set only the portion of m_mb we are using
if (!m_requiresFullMbValue)
{
m_mb &= ~enum_packedMbLayout_MbMask;
m_mb |= RidFromToken(mb);
}
else
{
m_mb = RidFromToken(mb);
}
m_mb = RidFromToken(mb);
}
mdFieldDef GetMemberDef() const
{
LIMITED_METHOD_DAC_CONTRACT;
// Check if this FieldDesc is using the packed mb layout
if (!m_requiresFullMbValue)
{
return TokenFromRid(m_mb & enum_packedMbLayout_MbMask, mdtFieldDef);
}
return TokenFromRid(m_mb, mdtFieldDef);
}
......@@ -652,8 +619,6 @@ public:
return GetMDImport()->GetNameOfFieldDef(GetMemberDef(), pszName);
}
BOOL MightHaveName(ULONG nameHashValue);
// <TODO>@TODO: </TODO>This is slow, don't use it!
DWORD GetAttributes()
{
......
......@@ -1096,9 +1096,6 @@ MemberLoader::FindMethod(
// Retrieve the right comparison function to use.
UTF8StringCompareFuncPtr StrCompFunc = FM_GetStrCompFunc(flags);
SString targetName(SString::Utf8Literal, pszName);
ULONG targetNameHash = targetName.HashCaseInsensitive();
// Statistically it's most likely for a method to be found in non-vtable portion of this class's members, then in the
// vtable of this class's declared members, then in the inherited portion of the vtable, so we search backwards.
......@@ -1129,11 +1126,7 @@ MemberLoader::FindMethod(
}
if ((flags & FM_IgnoreName) != 0
||
(pCurDeclMD->MightHaveName(targetNameHash)
// This is done last since it is the most expensive of the IF statement.
&& StrCompFunc(pszName, pCurDeclMD->GetNameThrowing()) == 0)
)
|| StrCompFunc(pszName, pCurDeclMD->GetNameThrowing()) == 0)
{
if (CompareMethodSigWithCorrectSubstitution(pSignature, cSignature, pModule, pCurDeclMD, pDefSubst, pMT))
{
......@@ -1265,9 +1258,6 @@ MemberLoader::FindMethodByName(MethodTable * pMT, LPCUTF8 pszName, FM_Flags flag
// Retrieve the right comparison function to use.
UTF8StringCompareFuncPtr StrCompFunc = FM_GetStrCompFunc(flags);
SString targetName(SString::Utf8, pszName);
ULONG targetNameHash = targetName.HashCaseInsensitive();
// Scan all classes in the hierarchy, starting at the current class and
// moving back up towards the base.
while (pMT != NULL)
......@@ -1296,7 +1286,7 @@ MemberLoader::FindMethodByName(MethodTable * pMT, LPCUTF8 pszName, FM_Flags flag
continue;
}
if (pCurMD->MightHaveName(targetNameHash) && StrCompFunc(pszName, pCurMD->GetNameOnNonArrayClass()) == 0)
if (StrCompFunc(pszName, pCurMD->GetNameOnNonArrayClass()) == 0)
{
if (pRetMD != NULL)
{
......@@ -1494,9 +1484,6 @@ MemberLoader::FindField(MethodTable * pMT, LPCUTF8 pszName, PCCOR_SIGNATURE pSig
if (pMT->IsArray())
return NULL;
SString targetName(SString::Utf8Literal, pszName);
ULONG targetNameHash = targetName.HashCaseInsensitive();
EEClass * pClass = pMT->GetClass();
MethodTable *pParentMT = pMT->GetParentMethodTable();
......@@ -1518,11 +1505,6 @@ MemberLoader::FindField(MethodTable * pMT, LPCUTF8 pszName, PCCOR_SIGNATURE pSig
// Check is valid FieldDesc, and not some random memory
INDEBUGIMPL(pFD->GetApproxEnclosingMethodTable()->SanityCheck());
if (!pFD->MightHaveName(targetNameHash))
{
continue;
}
IfFailThrow(pInternalImport->GetNameOfFieldDef(mdField, &szMemberName));
if (StrCompFunc(szMemberName, pszName) != 0)
......
......@@ -383,31 +383,6 @@ VOID MethodDesc::GetFullMethodInfo(SString& fullMethodSigName)
#endif
//*******************************************************************************
BOOL MethodDesc::MightHaveName(ULONG nameHashValue)
{
LIMITED_METHOD_CONTRACT;
// We only have space for a name hash when we are using the packed slot layout
if (RequiresFullSlotNumber())
{
return TRUE;
}
WORD thisHashValue = m_wSlotNumber & enum_packedSlotLayout_NameHashMask;
// A zero value might mean no hash has ever been set
// (checking this way is better than dedicating a bit to tell us)
if (thisHashValue == 0)
{
return TRUE;
}
WORD testHashValue = (WORD) nameHashValue & enum_packedSlotLayout_NameHashMask;
return (thisHashValue == testHashValue);
}
//*******************************************************************************
void MethodDesc::GetSig(PCCOR_SIGNATURE *ppSig, DWORD *pcSig)
{
......
......@@ -164,8 +164,7 @@ enum MethodDescClassification
// Is the method synchronized
mdcSynchronized = 0x4000,
// Does the method's slot number require all 16 bits
mdcRequiresFullSlotNumber = 0x8000
// unused = 0x8000
};
#define METHOD_MAX_RVA 0x7FFFFFFF
......@@ -322,8 +321,6 @@ public:
LPCUTF8 GetNameThrowing();
BOOL MightHaveName(ULONG nameHashValue);
FORCEINLINE LPCUTF8 GetNameOnNonArrayClass()
{
WRAPPER_NO_CONTRACT;
......@@ -1028,36 +1025,13 @@ public:
inline WORD GetSlot()
{
LIMITED_METHOD_DAC_CONTRACT;
// Check if this MD is using the packed slot layout
if (!RequiresFullSlotNumber())
{
return (m_wSlotNumber & enum_packedSlotLayout_SlotMask);
}
return m_wSlotNumber;
}
inline VOID SetSlot(WORD wSlotNum)
{
LIMITED_METHOD_CONTRACT;
// Check if we have to avoid using the packed slot layout
if (wSlotNum > enum_packedSlotLayout_SlotMask)
{
SetRequiresFullSlotNumber();
}
// Set only the portion of m_wSlotNumber we are using
if (!RequiresFullSlotNumber())
{
m_wSlotNumber &= ~enum_packedSlotLayout_SlotMask;
m_wSlotNumber |= wSlotNum;
}
else
{
m_wSlotNumber = wSlotNum;
}
m_wSlotNumber = wSlotNum;
}
inline BOOL IsVirtualSlot()
......@@ -1073,19 +1047,6 @@ public:
PTR_MethodDesc GetDeclMethodDesc(UINT32 slotNumber);
protected:
inline void SetRequiresFullSlotNumber()
{
LIMITED_METHOD_CONTRACT;
m_wFlags |= mdcRequiresFullSlotNumber;
}
inline DWORD RequiresFullSlotNumber()
{
LIMITED_METHOD_DAC_CONTRACT;
return (m_wFlags & mdcRequiresFullSlotNumber) != 0;
}
public:
mdMethodDef GetMemberDef() const;
mdMethodDef GetMemberDef_NoLogging() const;
......@@ -1687,19 +1648,9 @@ protected:
BYTE m_bFlags2;
// The slot number of this MethodDesc in the vtable array.
// Note that we may store other information in the high bits if available --
// see enum_packedSlotLayout and mdcRequiresFullSlotNumber for details.
WORD m_wSlotNumber;
enum {
enum_packedSlotLayout_SlotMask = 0x03FF,
enum_packedSlotLayout_NameHashMask = 0xFC00
};
WORD m_wFlags;
public:
#ifdef DACCESS_COMPILE
void EnumMemoryRegions(CLRDataEnumMemoryFlags flags);
......
......@@ -151,7 +151,6 @@ static const Entry s_QCall[] =
DllImportEntry(TypeBuilder_SetConstantValue)
DllImportEntry(TypeBuilder_DefineCustomAttribute)
DllImportEntry(MdUtf8String_EqualsCaseInsensitive)
DllImportEntry(MdUtf8String_HashCaseInsensitive)
DllImportEntry(TypeName_ReleaseTypeNameParser)
DllImportEntry(TypeName_CreateTypeNameParser)
DllImportEntry(TypeName_GetNames)
......
......@@ -59,25 +59,6 @@ extern "C" BOOL QCALLTYPE MdUtf8String_EqualsCaseInsensitive(LPCUTF8 szLhs, LPCU
return fStringsEqual;
}
extern "C" ULONG QCALLTYPE MdUtf8String_HashCaseInsensitive(LPCUTF8 sz, INT32 stringNumBytes)
{
QCALL_CONTRACT;
// Important: the string in pSsz isn't null terminated so the length must be used
// when performing operations on the string.
ULONG hashValue = 0;
BEGIN_QCALL;
StackSString str(SString::Utf8, sz, stringNumBytes);
hashValue = str.HashCaseInsensitive();
END_QCALL;
return hashValue;
}
static BOOL CheckCAVisibilityFromDecoratedType(MethodTable* pCAMT, MethodDesc* pCACtor, MethodTable* pDecoratedMT, Module* pDecoratedModule)
{
CONTRACTL
......@@ -896,7 +877,7 @@ FCIMPL1(Object *, RuntimeTypeHandle::GetArgumentTypesFromFunctionPointer, Reflec
FCThrowRes(kArgumentException, W("Arg_InvalidHandle"));
FnPtrTypeDesc* fnPtr = typeHandle.AsFnPtrType();
HELPER_METHOD_FRAME_BEGIN_RET_PROTECT(gc);
{
MethodTable *pMT = CoreLibBinder::GetClass(CLASS__TYPE);
......@@ -908,7 +889,7 @@ FCIMPL1(Object *, RuntimeTypeHandle::GetArgumentTypesFromFunctionPointer, Reflec
{
TypeHandle typeHandle = fnPtr->GetRetAndArgTypes()[position];
OBJECTREF refType = typeHandle.GetManagedClassObject();
gc.retVal->SetAt(position, refType);
gc.retVal->SetAt(position, refType);
}
}
HELPER_METHOD_FRAME_END();
......@@ -933,7 +914,7 @@ FCIMPL1(FC_BOOL_RET, RuntimeTypeHandle::IsUnmanagedFunctionPointer, ReflectClass
FnPtrTypeDesc* fnPtr = typeHandle.AsFnPtrType();
unmanaged = (fnPtr->GetCallConv() & IMAGE_CEE_CS_CALLCONV_MASK) == IMAGE_CEE_CS_CALLCONV_UNMANAGED;
}
FC_RETURN_BOOL(unmanaged);
}
FCIMPLEND
......@@ -1722,14 +1703,6 @@ FCIMPL1(LPCUTF8, RuntimeMethodHandle::GetUtf8Name, MethodDesc *pMethod) {
}
FCIMPLEND
FCIMPL2(FC_BOOL_RET, RuntimeMethodHandle::MatchesNameHash, MethodDesc * pMethod, ULONG hash)
{
FCALL_CONTRACT;
FC_RETURN_BOOL(pMethod->MightHaveName(hash));
}
FCIMPLEND
FCIMPL1(StringObject*, RuntimeMethodHandle::GetName, MethodDesc *pMethod) {
CONTRACTL {
FCALL_CHECK;
......@@ -2159,7 +2132,7 @@ FCIMPL6(void, SignatureNative::GetSignature,
pMethod, declType.GetClassOrArrayInstantiation(), pMethod->LoadMethodInstantiation(), &typeContext);
else
SigTypeContext::InitTypeContext(declType, &typeContext);
MetaSig msig(pCorSig, cCorSig, pModule, &typeContext,
(callConv & IMAGE_CEE_CS_CALLCONV_MASK) == IMAGE_CEE_CS_CALLCONV_FIELD ? MetaSig::sigField : MetaSig::sigMember);
......@@ -2734,14 +2707,6 @@ FCIMPL1(LPCUTF8, RuntimeFieldHandle::GetUtf8Name, FieldDesc *pField) {
}
FCIMPLEND
FCIMPL2(FC_BOOL_RET, RuntimeFieldHandle::MatchesNameHash, FieldDesc * pField, ULONG hash)
{
FCALL_CONTRACT;
FC_RETURN_BOOL(pField->MightHaveName(hash));
}
FCIMPLEND
FCIMPL1(INT32, RuntimeFieldHandle::GetAttributes, FieldDesc *pField) {
CONTRACTL {
FCALL_CHECK;
......
......@@ -103,8 +103,6 @@ public:
extern "C" BOOL QCALLTYPE MdUtf8String_EqualsCaseInsensitive(LPCUTF8 szLhs, LPCUTF8 szRhs, INT32 stringNumBytes);
extern "C" ULONG QCALLTYPE MdUtf8String_HashCaseInsensitive(LPCUTF8 sz, INT32 stringNumBytes);
class RuntimeTypeHandle;
typedef RuntimeTypeHandle FCALLRuntimeTypeHandle;
......@@ -138,7 +136,7 @@ public:
static FCDECL1(Object *, GetArgumentTypesFromFunctionPointer, ReflectClassBaseObject *pTypeUNSAFE);
static FCDECL1(FC_BOOL_RET, IsUnmanagedFunctionPointer, ReflectClassBaseObject *pTypeUNSAFE);
static FCDECL2(FC_BOOL_RET, CanCastTo, ReflectClassBaseObject *pType, ReflectClassBaseObject *pTarget);
static FCDECL2(FC_BOOL_RET, IsInstanceOfType, ReflectClassBaseObject *pType, Object *object);
......@@ -255,7 +253,6 @@ public:
static FCDECL1(INT32, GetMethodDef, ReflectMethodObject *pMethodUNSAFE);
static FCDECL1(StringObject*, GetName, MethodDesc *pMethod);
static FCDECL1(LPCUTF8, GetUtf8Name, MethodDesc *pMethod);
static FCDECL2(FC_BOOL_RET, MatchesNameHash, MethodDesc * pMethod, ULONG hash);
static
FCDECL1(FC_BOOL_RET, HasMethodInstantiation, MethodDesc *pMethod);
......@@ -309,7 +306,6 @@ public:
static FCDECL5(void, SetValueDirect, ReflectFieldObject *pFieldUNSAFE, ReflectClassBaseObject *pFieldType, TypedByRef *pTarget, Object *valueUNSAFE, ReflectClassBaseObject *pContextType);
static FCDECL1(StringObject*, GetName, ReflectFieldObject *pFieldUNSAFE);
static FCDECL1(LPCUTF8, GetUtf8Name, FieldDesc *pField);
static FCDECL2(FC_BOOL_RET, MatchesNameHash, FieldDesc * pField, ULONG hash);
static FCDECL1(INT32, GetAttributes, FieldDesc *pField);
static FCDECL1(ReflectClassBaseObject*, GetApproxDeclaringType, FieldDesc *pField);
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册