2016-08-27 4 views
3

In F #, wir einige sehr schöne Lösungen für Design-Zeit Typsicherheit haben: (! Und keine impliziten Konvertierungen beginnen) Typ-Aliasnamen und Einzelfall struct union:C# Markerstrukturen leistung

// type aliases are erased at compile time 
type Offset = int64<offset> 

// no allocations 
[<Struct>] 
type Offset = Offset of int64 

Was wäre eine Alternative für C#?

Ich habe noch nie eine praktische Verwendung von Markerstrukturen (die ein einzelnes Element), aber es sieht aus wie gesehen, wenn wir explizites Typkonvertierungen hinzufügen dann könnten wir Design-Zeit-Verhalten sehr ähnlich Typen Aliase in F # bekommen. Das heißt - IDE wird sich über Typinkongruenzen beschweren und man muss explizit Werte werfen.

Nachfolgend finden Sie einige POC-Code:

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); 
    } 
} 

Also, die IDE, was schön ist! Aber ich wollte fragen, was sind die Auswirkungen auf die Leistung und andere Nachteile, und zu vermeiden "nur messen" Kommentare haben es nur gemessen und aufgehört zu schreiben diese Frage zunächst ... Aber die Ergebnisse kamen kontraintuitiv:

[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]; 
} 

Überraschenderweise auf [email protected] x64/mit Offset den Fall lösen ist sichtbar schneller auf jede Testrunde - typische Werte sind:

Int64: 1046 
Offset: 932 
Direct: 730 

würde ich gleich oder langsames Ergebnis im Vergleich erwartet int64 nur verwenden. Also was geht hier hin? Können Sie den gleichen Unterschied reproduzieren oder einen Mangel feststellen, z. wenn ich verschiedene Dinge messe?

+0

haben Sie sich die generierte IL angesehen? – thumbmunkeys

+0

@thumbmunkeys, ja, offensichtlich für 'Offset' ruft es die op_explicit Methoden auf und hat mehr Zeilen - so macht es aus IL Perspektive mehr Arbeit. Aber ansonsten ist der Code derselbe. Wahrscheinlich einige JIT-Besonderheiten, wenn das Ergebnis reproduzierbar ist. –

+0

Die Ergebnisse sind ein bisschen anders für "echte" 64-Bit (lieber 32-Bit aus), aber im Allgemeinen würde ich nicht für die Leistung stören - eine "Struktur", die ein einzelnes primitives Mitglied wickelt, sollte wirklich das gleiche wie die Verwendung sein das Mitglied. –

Antwort

6

1.e Sobald Sie for (int i = 0; mit for (long i = 0; im Test Int64 ersetzen, wird die Leistung an den direkten Test identisch sein.

Während int Verwendung erzeugt es eine solche x86-64 Anweisungen:

inc   ecx 
cmp   ecx,0F4240h 

Während long Verwendung erzeugt es eine solche x86-64 Anweisungen:

inc   rcx 
cmp   rcx,0F4240h 

So ist der einzige Unterschied in der Verwendung von 32-Bit-Register ecx oder seine 64-Bit-Version rcx, wo später aufgrund eines CPU-Designs schneller ist.

2. Verwenden Sie long für Iterator im Offset-Test, und Sie werden ähnliche Leistung sehen.

3. Da der Code im Release-Modus optimiert ist, gibt es fast keinen Unterschied zwischen der Verwendung Int64 oder Offset aber an einem gewissen Punkt der Anweisungen sind wenig neu arrangiert.

Während Offset mit (einer Anweisung weniger):

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

Bei der Verwendung von Int64 (eine Anweisung mehr):

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

4. Der direkte Test ist die schnellste, weil es tut Vergleichen Sie die Array-Grenzen nicht mit den oben unter # 3 gezeigten Anweisungen. Diese Optimierung geschieht, wenn Sie eine Schleife wie folgt schreiben:

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

Normalerweise, wenn Ihr Index ist außerhalb der Grenzen des Arrays der IndexOutOfRangeException wirft, aber in diesem Fall Compiler weiß, dass es nicht passieren kann, so dass es unterlässt die Prüfung.

Dann, sogar mit zusätzlichen Anweisungen in anderen Tests, haben sie eine ähnliche Leistung aufgrund der CPU-Verzweigung Prädiktor, der im Voraus Anweisungen im Voraus startet, falls erforderlich, und verwirft die Ergebnisse, wenn die Bedingung fehlschlägt.

+0

Das ist so toll! Vielen Dank! :) –