2016-08-27 65 views
3

在F#中,我們有設計時類型安全幾個非常不錯的解決方案:(!和隱式轉換下手)類型別名和單例結構工會:C#標記結構性能

​​

什麼是C#的替代方案?

我從來沒有見過的標記結構的實際使用(包含單個元素),但看起來如果我們增加明確類型轉換,則我們可以得到非常相似,鍵入F#別名設計時行爲。那就是 - IDE會抱怨類型不匹配,並且必須顯式地賦值。

下面是一些POC代碼:

public struct Offset { 
    private readonly long _value; 
    private Offset(long value) { 
     _value = value; 
    } 

    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static explicit operator Offset(long value) { 
     return new Offset(value); 
    } 

    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static explicit operator long(Offset offset) { 
     return offset._value; 
    } 
} 

public interface IIndex<T> { 
    Offset OffsetOf(T value); 
    T AtOffset(Offset offset); 
} 

public class SmapleUsage 
{ 
    public void Test(IIndex<long> idx) 
    { 
     // without explicit cast we have nice red squiggles 
     var valueAt = idx.AtOffset((Offset)123); 
     long offset = (long)idx.OffsetOf(42L); 
    } 
} 

所以,IDE的事情是太好了!但我會問什麼是性能影響和其他缺點,並避免「只是測量」評論剛剛測量它,並最初停止寫這個問題......但結果出來反直覺:

[Test] 
public void OffsetTests() { 
    var array = Enumerable.Range(0, 1024).ToArray(); 
    var sw = new Stopwatch(); 

    for (int rounds = 0; rounds < 10; rounds++) { 
     sw.Restart(); 
     long sum = 0; 
     for (int rp = 0; rp < 1000000; rp++) { 
      for (int i = 0; i < array.Length; i++) { 
       sum += GetAtIndex(array, i); 
      } 
     } 
     sw.Stop(); 
     if (sum < 0) throw new Exception(); // use sum after loop 
     Console.WriteLine($"Index: {sw.ElapsedMilliseconds}"); 

     sw.Restart(); 
     sum = 0; 
     for (int rp = 0; rp < 1000000; rp++) { 
      for (int i = 0; i < array.Length; i++) { 
       sum += GetAtOffset(array, (Offset)i); 
      } 
     } 
     if (sum < 0) throw new Exception(); // use sum after loop 
     sw.Stop(); 
     Console.WriteLine($"Offset: {sw.ElapsedMilliseconds}"); 

     sw.Restart(); 
     sum = 0; 
     for (int rp = 0; rp < 1000000; rp++) { 
      for (int i = 0; i < array.Length; i++) { 
       sum += array[i]; 
      } 
     } 
     if (sum < 0) throw new Exception(); // use sum after loop 
     sw.Stop(); 
     Console.WriteLine($"Direct: {sw.ElapsedMilliseconds}"); 
    } 
} 

[MethodImpl(MethodImplOptions.AggressiveInlining)] 
private int GetAtIndex(int[] array, long index) { 
    return array[index]; 
} 

[MethodImpl(MethodImplOptions.AggressiveInlining)] 
private int GetAtOffset(int[] array, Offset offset) { 
    return array[(long)offset]; 
} 

出人意料的是,在[email protected] X64 /與Offset釋放的情況下是明顯快於每輪測試 - 典型值爲:

Int64: 1046 
Offset: 932 
Direct: 730 

等於或慢的結果相比,只是用int64我期望的那樣。所以這裏是怎麼回事?您是否可以重現相同的差異或找出一些不足之處,例如如果我測量不同的東西?

+0

你看過生成的IL嗎? – thumbmunkeys

+0

@thumbmunkeys,是的,很明顯,對於Offset,它調用op_explicit方法,並且有更多的行 - 所以從IL的角度來看,它的工作更多。但除此之外,代碼是相同的。如果結果是可重現的,可能會有一些JIT特性。 –

+0

結果是「真」的64位位不同(身高:32位關閉),但一般來說,我不會打擾性能 - 一個'struct'被包裝的單個原始構件確實應該與使用該成員。 –

回答

6

1.在Int64測試中將for (int i = 0;替換爲for (long i = 0;後,性能將與Direct測試相同。

在使用int它生成這樣的x86-64指令:

inc   ecx 
cmp   ecx,0F4240h 

在使用long它生成這樣的x86-64指令:

inc   rcx 
cmp   rcx,0F4240h 

因此,在使用32位寄存器中的唯一的區別ecx或其64位版本rcx,由於CPU設計,後者更快。

2.在偏移測試中使用long作爲迭代器,您會看到類似的性能。

3.由於代碼在釋放模式優化的,存在使用Int64Offset之間幾乎沒有差異,但是在某個點,所述指令點點重新排列。

在使用偏移(一個指令以內):

movsxd  rdx,eax 
movsxd  r8,r14d 
cmp   rdx,r8 
jae   <address> 

當使用的Int64(一個指令更多):

movsxd  rdx,r14d 
movsxd  r8,eax 
cmp   r8,rdx 
jae   <address> 
movsxd  rdx,eax 

4.直接測試是最快的,因爲它沒有不要使用上面#3所示的指令進行數組邊界檢查。當你寫這樣的循環這種優化情況:

for (var i=0; i<array.Length; i++) { ... array[i] ... } 

通常情況下,如果你的指數是它拋出IndexOutOfRangeException數組的範圍之外,但在這種情況下,編譯器知道,這是不可能發生的,所以省略支票。

之後,甚至有在其他測試中額外的指令,它們具有類似的性能,因爲CPU的分支預測器,如果需要,它開始提前運行的指令,如果條件不滿足丟棄的結果。

+0

這太棒了!謝謝! :) –