2012-12-14 97 views
9

由以下類型字段訪問與多個字段

class SimpleClass     struct SimpleStruct 
{         { 
    public int Value0;     public int Value0; 
}         } 

class ComplexClass     struct ComplexStruct 
{         { 
    public int Value0;     public int Value0; 
    public int Value1;     public int Value1; 
    public int Value2;     public int Value2; 
    public int Value3;     public int Value3; 
    public int Value4;     public int Value4; 
    public int Value5;     public int Value5; 
    public int Value6;     public int Value6; 
    public int Value7;     public int Value7; 
    public int Value8;     public int Value8; 
    public int Value9;     public int Value9; 
    public int Value10;     public int Value10; 
    public int Value11;     public int Value11; 
}         } 

產生我的計算機上執行以下有趣的結果代替以下簡短但完整的示例程序

const long iterations = 1000000000; 

T[] array = new T[1 << 20]; 
for (int i = 0; i < array.Length; i++) 
{ 
    array[i] = new T(); 
} 

Stopwatch sw = Stopwatch.StartNew(); 
for (int i = 0; i < iterations; i++) 
{ 
    array[i % array.Length].Value0 = i; 
} 

Console.WriteLine("{0,-15} {1} {2:n0} iterations/s", 
    typeof(T).Name, sw.Elapsed, iterations * 1000d/sw.ElapsedMilliseconds); 

T用於類型更慢(Windows 7 .NET 4.5 32-bit)

 
SimpleClass  00:00:10.4471717 95,721,260 iterations/s 
ComplexClass  00:00:37.8199150 26,441,736 iterations/s 
SimpleStruct  00:00:12.3075100 81,254,571 iterations/s 
ComplexStruct 00:00:32.6140182 30,661,679 iterations/s 

問題1:爲什麼ComplexClassSimpleClass慢得多?經過的時間似乎隨着班級中的字段數量線性增加。寫一個有很多字段的類的第一個字段應該與僅寫入一個字段的一個類的第一個字段不同,不是嗎?

問題2:爲什麼ComplexStruct慢於SimpleStruct?查看IL代碼可以看出,i是直接寫入數組,而不是寫入ComplexStruct的本地實例,然後被複制到數組中。所以不應該複製更多的字段造成開銷。

獎勵問題:爲什麼ComplexStruct快於ComplexClass


編輯:更新測試結果具有更小的陣列,T[] array = new T[1 << 8];

 
SimpleClass  00:00:13.5091446 74,024,724 iterations/s 
ComplexClass  00:00:13.2505217 75,471,698 iterations/s 
SimpleStruct  00:00:14.8397693 67,389,986 iterations/s 
ComplexStruct 00:00:13.4821834 74,172,971 iterations/s 

所以幾乎SimpleClassComplexClass,只有SimpleStructComplexStruct之間的微小差異之間沒有什麼區別。但是,SimpleClassSimpleStruct的性能顯着下降。


編輯:現在與T[] array = new T[1 << 16];

 
SimpleClass  00:00:09.7477715 102,595,670 iterations/s 
ComplexClass  00:00:10.1279081 98,745,927 iterations/s 
SimpleStruct  00:00:12.1539631 82,284,210 iterations/s 
ComplexStruct 00:00:10.5914174 94,419,790 iterations/s 

結果爲1<<15就像1<<8,併爲1<<17結果是一樣1<<20

+0

我很有興趣聽到有確定知識答案的人。我認爲有一件事情會導致複雜版本變得越來越慢,這是因爲必須從內存移動到CPU緩存的數據量增加。 – hatchet

+0

我同意Carson63000,簡單結構和複雜結構之間的區別幾乎肯定是由於複雜類型的緩存優勢較少造成的。至於結構與類,結構是一個值類型,而類是一個引用類型,所以有一個額外的間接進行類。 –

+0

另一個有趣的問題是爲什麼SimpleStruct不比SimpleClass更快?我本來預計這是最快的。 – hatchet

回答

7

對問題1的可能答案:

您的CPU一次將內存讀入其緩存中。

對於較大的數據類型,可以將更少的對象放入每個緩存頁面。即使你只寫了一個32位的值,你仍然需要CPU緩存中的頁面。對於較小的對象,在下次需要從主內存讀取之前,可以通過更多的循環。

2

我沒有文件證明它,但我想這可能是一個地方問題。由於內存方面的複雜類更寬,內核訪問遠端內存區域,堆或堆棧需要更長的時間。儘管如此,客觀上,我不得不說,你的措施之間的差異聽起來真的很高,因爲這個問題是系統的錯誤。

關於類和結構之間的差異,我也不能記錄這一點,但這可能是因爲,與以前相同的原理,堆棧比堆區域緩存更多,導致緩存未命中更少。

您是否運行過主動優化的程序?

編輯:我已完成上ComplexStruct一個小的測試,並與LayoutKind.Explicit作爲參數使用的StructLayoutAttribute,然後加入0 FieldOffsetAttribute作爲參數傳遞給該結構的每個字段。時間大幅縮短,我認爲它們與SimpleStruct的大致相同。我在調試模式下運行它,調試器沒有優化。雖然結構保留了它的字段,但它在內存中的大小被削減了,時間也被削減了。

+0

我測試了發佈版本,沒有附加調試器。 – dtb

+0

兩個結構都太大而無法堆棧。 – evanmcdonnal

+1

@Trisped堆棧是1MB,如果我正確理解他的代碼,則數組的大小爲2^20,這是MB中的字節數。 SimpleClass對象的數組是堆棧大小的4倍。它不能存儲在堆棧中。結構太大而無法堆棧,這意味着如果您嘗試將其放到那裏,將會導致堆棧溢出。 – evanmcdonnal

2

回答1ComplexClass是慢然後SimpleClass因爲CPU的緩存大小是固定的,從而更少ComplexClass對象適合在所述高速緩存處的時間。基本上,由於從內存中獲取所需的時間,您看到了增加。如果你進入緩存並降低RAM的速度,這可能會更清楚(extream)。

答案2:與答案1相同。

Bonus:結構數組是一個連續的結構塊,僅由數組指針引用。類的數組是一個連續的對類實例的引用塊,由數組指針引用。由於課堂是在堆上創建的(基本上有空間的地方),它們不是在一個連續的和有序的塊中。雖然這對優化空間非常有用,但它對CPU緩存不利。因此,當迭代數組(按順序)時,會有更多的CPU緩存未命中,並有大量指向大型類的指針,那麼將會有一個按順序迭代一個結構數組。

爲什麼SimpleStruct是那麼慢SimpleClass:據我瞭解有開銷的結構給量(約76個某處叮咬有人告訴我)。我不確定這是什麼或爲什麼在那裏,但我希望如果你使用本地代碼(C++編譯)來運行這個相同的測試,你會發現SimpleStruct陣列性能更好。這只是一個猜測。


無論哪種方式,這看起來很有趣。我今晚會嘗試一下。我會發布我的結果。是否有可能獲得完整的代碼?

+0

我期待着在這方面看到更多的結果。問題中的代碼是我所有的,每種類型都重複4次。 – dtb

+0

我跑了測試,也看到了SimpleStruct的結果比SimpleClass稍微慢一點。我也從垃圾回收器中分配了內存數據。 SimpleStruct每個元素消耗4個字節,所以沒有一個結構數組的開銷。SimpleClass每個元素消耗16個字節(在64位系統上),這可能是8個字節的數組引用+ 4個字節的對象的int值+ 4個字節的對象頭。 – hatchet

1

我修改了一下你的基準來消除模數,這可能是大部分時間消耗的原因,而你似乎在比較字段訪問時間,而不是整數模數算術。

const long iterations = 1000; 
    GC.Collect(); 
    GC.WaitForPendingFinalizers(); 
    //long sMem = GC.GetTotalMemory(true); 
    ComplexStruct[] array = new ComplexStruct[1 << 20]; 
    for (int i = 0; i < array.Length; i++) { 
     array[i] = new ComplexStruct(); 
    } 
    //long eMem = GC.GetTotalMemory(true); 
    //Console.WriteLine("memDiff=" + (eMem - sMem)); 
    //Console.WriteLine("mem/elem=" + ((eMem - sMem)/array.Length)); 
    Stopwatch sw = Stopwatch.StartNew(); 
    for (int k = 0; k < iterations; k++) { 
     for (int i = 0; i < array.Length; i++) { 
      array[i].Value0 = i; 
     } 
    } 
    Console.WriteLine("{0,-15} {1} {2:n0} iterations/s", 
     typeof(ComplexStruct).Name, sw.Elapsed, (iterations * array.Length) * 1000d/sw.ElapsedMilliseconds); 

(替換每個測試的類型)。我得到這些結果(以百萬內部循環分配/秒):

SimpleClass 357.1 
SimpleStruct 411.5 
ComplexClass 132.9 
ComplexStruct 159.1 

這些數字是接近我會盡量類VS結構版本的預期。我認爲複雜版本的較慢時間可以通過較大對象/結構的CPU緩存效果來解釋。使用註釋掉的內存測量代碼顯示結構體版本消耗較少的整體內存。我注意到內存測量代碼影響了struct和class版本的相對時間後,我添加了GC.Collect。

+0

我的代碼是我試圖優化的一個較大程序的一個片段。模量是那裏的一個重要部分。但是,感謝您嘗試這一點 - 它再次表明地方性很重要。 – dtb