在定義任何一個ValueType之後,它都是從System.ValueType繼承過來的,默認的就繼承了Equals方法和GetHashCode方法,在使用的時候,必須主意的是最好重寫自定義ValueType的這兩個方法,因為可能帶來性能上面的嚴重問題或者是比較的不正確。
譬如定義下面這樣的一個結構體值類型:
struct TestValueType
{
public int Myint;
public float Myfloat;
}
TestValueType V1,V2;
V1.Equals(V2);
可以翻開sscli看看ValueType的equals方法的實現:
public override bool Equals (Object obj)
{
if (null==obj) {
return false;
}
RuntimeType thisType = (RuntimeType)this.GetType();
RuntimeType thatType = (RuntimeType)obj.GetType();
if (thatType!=thisType) {
return false;
}
Object thisObj = (Object)this;
Object thisResult, thatResult;
// if there are no GC references in this object we can avoid reflection
// and do a fast memcmp
if (CanCompareBits(this))
return FastEqualsCheck(thisObj, obj);
FieldInfo[] thisFields = thisType.GetFields(BindingFlags.Instance
| BindingFlags.Public | BindingFlags.NonPublic);
for (int i=0; i<thisFields.Length; i++) {
thisResult = ((RtFieldInfo)thisFields[i]).InternalGetValue(thisObj,false);
thatResult = ((RtFieldInfo)thisFields[i]).InternalGetValue(obj, false);
if (thisResult == null) {
if (thatResult != null)
return false;
}
else
if (!thisResult.Equals(thatResult)) {
return false;
}
}
return true;
}
首先得到比較兩個對象的Type,如果類型不一樣就直接返回。
然後對於能夠進行快速比較的值類型,就使用快速比較:
// if there are no GC references in this object we can avoid reflection
// and do a fast memcmp
if (CanCompareBits(this))
return FastEqualsCheck(thisObj, obj);
從上面的代碼可以看到,如果不是進行快速比較的話,要使用反射來枚舉出所有的成員變量然後比較,這樣的開銷還是比較大的。
那麼,在什麼情況下不會進行快速比較而是采用枚舉成員變量比較的方法呢?
看看CanCompareBits方法的實現:
// Return true if the valuetype does not contain pointer and is tightly packed
FCIMPL1(FC_BOOL_RET, ValueTypeHelper::CanCompareBits, Object* obj)
{
WRAPPER_CONTRACT;
STATIC_CONTRACT_SO_TOLERANT;
_ASSERTE(obj != NULL);
MethodTable* mt = obj->GetMethodTable();
FC_RETURN_BOOL(!mt->ContainsPointers() && !mt->IsNotTightlyPacked());
}
FCIMPLEND
在最後一行裡面做出的判斷:如果方法體裡面不包含Pointers,GC需要用到的,同時是Tightly Packed的時候,就會調用FastEqualsCheck進行快速比較。
FastEqualsCheck方法的實現如下:
FCIMPL2(FC_BOOL_RET, ValueTypeHelper::FastEqualsCheck, Object* obj1, Object* obj2)
{
WRAPPER_CONTRACT;
STATIC_CONTRACT_SO_TOLERANT;
_ASSERTE(obj1 != NULL);
_ASSERTE(obj2 != NULL);
_ASSERTE(!obj1->GetMethodTable()->ContainsPointers());
_ASSERTE(obj1->GetSize() == obj2->GetSize());
TypeHandle pTh = obj1->GetTypeHandle();
FC_RETURN_BOOL(memcmp(obj1->GetData(),obj2->GetData(),pTh.GetSize()) == 0);
}
FCIMPLEND
是調用了memcmp方法,來直接對內存進行一個bit一個bit的比較。這樣就可以快很多。
接下來剩下是什麼時候調用快速比較方法呢?
看看判斷是否符合快速比較的判斷:
// Note: This flag MUST be available even from an unrestored MethodTable –
// GcScanRoots in siginfo.cpp.
DWORD ContainsPointers()
{
LEAF_CONTRACT;
return(m_wFlags & enum_flag_ContainsPointers);
}
BOOL IsNotTightlyPacked()
{
LEAF_CONTRACT;
return (m_wFlags & enum_flag_NotTightlyPacked);
}
m_wFlags是一個DWORD來標識了很多在對象運行時候的信息,轉到這兩個枚舉類型的定義:
enum_flag_ContainsPointers = 0x00080000,
enum_flag_NotTightlyPacked = 0x04000000,
enum_flag_NotTightlyPacked專門在m_wFlags占了一位來標識這個對象ValueType是不是能夠進行bit位的比較。而且它只是在ContainsPointer為false的時候才起作用。因為如果包含對對象的引用的話就不能進行bit位的比較了。
而對於m_wFlags中個能否進行ValueType的bit位比較的這個bit的設置,也有定義:
if (IsValueClass() && (GetNumInstanceFieldBytes() != totalDeclaredFieldSize
|| HasOverLayedField()))
{
pMT->SetNotTightlyPacked();
}
到這裡了,會有人說,咦,這微軟不已經做的很好了麼,可以bit比較的就快速比較,不行的就枚舉成員變量來比較,為什麼要重寫呢?
真正的問題在於,CanCompareBits方法,並不能總是得到正確的結果。假設一下,在上面定義的結構體中,有兩個實例a和b,
Set a.float=-0.0;
Set b.float=+0.0;
這樣的比較,在邏輯上面是相等的,但是在jit優化之前,這兩個0用memcmp比較的結果是true,優化之後,比較的結果是false。也就是優化了以後,這兩個邏輯上面相等的變量在內存裡面的表示方法是不一樣的。
類似的道理,如果定義的struct裡面包含了重寫的equals方法,優化了之後,得到的比較結果就不一定正確了。
這ms是微軟的一個bug,當然,這裡只是妄加揣測,並沒有做嚴格的測試。Have not got much time for that.
恩,這裡是參考了http://blog.csdn.net/nineforever/archive/2008/10/12/3062289.aspx的這個假設才敢這麼說的..........當然,在本文中還是假設。
當然,還有一種情況是CLR在build Object的內存布局的時候就考慮到這種情況避免了這個問題。就是在build一個valuetype的methodtable的時候,就考慮到了這樣的一種情況,直接制定了這個bit位的值。也就是
m_wFlags對應的0x04000000這個地方的一個bit位。
來驗證這個想法:
首先找到設置這個bit位的地方,直接搜索enum_flag_NotTightlyPacked這個枚舉類型,得到的結果集合最小,一共三個結果,很容易就看到了設置這個值的地方:
void SetNotTightlyPacked()
{
LEAF_CONTRACT;
m_wFlags |= enum_flag_NotTightlyPacked;
}
然後尋找對這個方法的引用,得到在什麼情況下設置這個bit位的判斷條件:
if (IsValueClass() && (GetNumInstanceFieldBytes() != totalDeclaredFieldSize
|| HasOverLayedField()))
{
pMT->SetNotTightlyPacked();
}
e:\Projects\Rotor\sscli20\clr\src\vm\class.cpp
中間的一個判斷可以不看,和這裡討論的東西關系不大,主要就是IsValueClass()的判斷和HasOverLayedField()這兩個方法比較可疑。
首先看看第一個方法IsValueClass的實現:
inline DWORD IsValueClass()
{
LEAF_CONTRACT;
STATIC_CONTRACT_SO_TOLERANT;
return (m_VMFlags & VMFLAG_VALUETYPE);
}
VMFLAG_VALUETYPE = 0x00800000,
僅僅校驗了m_VMFlags中的VMFLAG_VALUETYPE這個位的值。那就找m_VMFlags中的VMFLAG_VALUETYPE這個值對應的bit位是在哪兒被設置的:
inline void SetValueClass()
{
LEAF_CONTRACT;
g_IBCLogger.LogEEClassCOWTableAccess(this);
FastInterlockOr(&m_VMFlags,VMFLAG_VALUETYPE);
}
然後找這個函數是在哪兒被調用的,找了半天,終於找到了這個函數被調用的地方,因為調用這個方法的地方比較多,這裡就選取在build method Table的時候的調用:
// Check to see if the class is an valuetype
// these build a value type response to later ValueType.Equals methods....
if(pParentMethodTable != NULL && ((g_pEnumClass != NULL && pParentMethodTable == g_pValueTypeClass) || pParentMethodTable == g_pEnumClass))
{
SetValueClass();
// Cutttttttted
}
這個方法存在於MethodTableBuilder::BuildMethodTableThrowing,文件地址位於
e:\Projects\Rotor\sscli20\clr\src\vm\class.cpp。這裡只是根據程序語言的定義,如果是從ValueType或者是枚舉類型繼承過來的,就判斷為ValueType。
OK了,就剩下最後一個假設,在HasOverLayedField方法中做出的判斷。看看它的實現:
BOOL HasOverLayedField()
{
LEAF_CONTRACT;
STATIC_CONTRACT_SO_TOLERANT;
return m_VMFlags & VMFLAG_HASOVERLAYEDFIELDS;
}
尋找引用,找到了對這個bit進行設置的方法:
inline void SetHasOverLayedFields()
{
LEAF_CONTRACT;
g_IBCLogger.LogEEClassCOWTableAccess(this);
FastInterlockOr(&m_VMFlags,VMFLAG_HASOVERLAYEDFIELDS);
}
在MethodTableBuilder::HandleExplicitLayout中找到了對這個方法的調用:
if(!explicitClassTrust.IsNonOverLayed())
{
SetHasOverLayedFields();
}
HandleExplicitLayout這個方法是在build一個實例的MethodTable的時候,用來處理額外build method中未考慮的情況。
這裡,使用了一個trust等級來標識是不是需要設置這個值。轉到IsNonOverLayed的定義中:
BOOL IsNonOverLayed()
{
LEAF_CONTRACT;
return m_trust >= kNonOverLayed;
}
查看kNonOverLayed的定義,以上所有的問題都得到了解釋:
E:\Rotor\sscli20\clr\src\vm\class.h
Enum TrustLevel:
What's guaranteed. What the loader does Knoe 0 no guarantees at all Type refuses to load at all. Klegal 1 guarantees no objref <-> scalar overlap and no unaligned objref T Type loads but field access won't verify kVerifiable 2 guarantees no objref <-> objref overlap and all guarantees above Type loads and field access will verify kNonOverLayed 3 guarantees no overlap at all and all guarantees above Type loads, field access verifies and Equals() may be optimized if structure is tightly packed在上面的程序中,使用的是kNonOverLayed,這裡,考慮到了Equals方法被優化了之後會出現的情況。
到這裡,上面的所有假設,經過一段漫長的查找實現過程,終於得到了證實。