2013-02-12 9 views
5

Ich arbeite an einer IEqualityComparer, die Arrays von primitiven Typen wirklich schnell vergleichen soll. Mein Plan ist, Zeiger auf die Arrays und memcmp ihnen zu erhalten. Wie folgt aus:Wie fixed mit einer Variablen vom Typ Array oder T []?

public unsafe override bool Equals(T[] x, T[] y) 
    { 
     if (ReferenceEquals(x, y)) return true; 
     if (x == null || y == null) return false; 
     if (x.Length != y.Length) return false; 

     var xArray = (Array)x; 
     var yArray = (Array)y; 
     fixed (void* xPtr = xArray) //compiler error 1 
     fixed (T* yPtr = y) //compiler error 2 
     { 
      return memcmp(xPtr, yPtr, x.Length * this.elementSize); 
     } 
    } 

Die feste Aussage erlaubt mich nicht, entweder Array oder T[] zu.

Es Fehlermeldungen sind:

1. Cannot implicitly convert type 'System.Array' to 'void*' 
2. Cannot take the address of, get the size of, or declare a pointer to a managed type ('T') 

Nun, ich weiß nicht wirklich egal, wie ich diese Arbeit zu machen (ich bin nicht auf diesen genauen Ansatz verpflichtet). Wie kann ich memcmp zwei T[], wo ich weiß, dass T ist ein primitiver/blitable Typ?

Ich möchte wirklich vermeiden, den Typ einzuschalten und eine spezialisierte (und duplizierte) Codeversion für jeden interessanten Typ zu erstellen. Jede Art von Reflexionslösung ist aufgrund von Leistungseinschränkungen nicht durchführbar (und ja, ich brauche die Leistung hier wirklich - es gibt keine vorzeitigen Optimierungswarnungen, wie es bei Stack Overflow üblich ist).

+1

Sie nicht nennen kann 'memcpy' von C# –

+0

Darüber hinaus haben Sie versucht,' öffentlich unsichere Überschreibung bool Equals (T [] x, T [] y) where T: struct'? – Davio

+1

Warum versuchen Sie, Ihren Code zu optimieren? Sie erstellen wahrscheinlich mehr Leistungsprobleme durch Marshalling auf nicht verwalteten Code. –

Antwort

5

, wo ich weiß, dass T ein primitiver/blitfähig Typ ist

Sie wissen es, der Compiler weiß das nicht. Die CLR fordert, dass alles in einem angehefteten Objekt nicht mehr durch den Garbage Collector verschoben werden kann. Für ein Array, das seine Array-Elemente enthält. Nur eine Art von T, die sich qualifiziert, ist ein einfacher Wertetyp, der blitable ist. Generics gibt dir keine Möglichkeit, T auf einen blitbaren Typ zu beschränken.

Normalerweise würden Sie die Argumente von memcmp() als Byte [] deklarieren. Der pinvoke Marshaller macht dann schon das Richtige und wird die byte [] Arrays vor dem Aufruf von memcmp() pinnen. Dies wird jedoch auch nicht funktionieren, da Sie nicht einfach ein T [] in ein Byte [] konvertieren können. Du musst dich mit GCHandle festlegen. Deklarieren Sie die memcmp() - Argumente als IntPtr anstelle von Byte [] entsprechend.

Die Teilmenge der Typen, die funktionieren kann, ist in der Praxis klein genug, um einfach Methodenüberladungen anstatt einer generischen Methode zu schreiben. Nun kann der pinvoke-Marshaller das Pinning übernehmen und die Memcmp() - Funktionsdeklarationen entsprechend überladen.

+1

Haben Sie irgendeinen Hinweis auf die 'fixed' Anweisung _not_ pinning Objekte im Speicher, was [was] ist (http://msdn.microsoft.com/en-gb/library/ f58wzh21.aspx) es ist [angeblich] (http://blogs.msdn.com/b/ericlippert/archive/2009/08/27/what-s-the-difference-between-fixed-and-fixed.aspx) zu tun? – Rawling

+0

Schauen Sie sich einfach die IL von der Aussage generiert –

+1

Sehr wertvoll, Hans. Ich wusste irgendwie, dass der C# -Compiler nur ma verwendet naged Pointer ("innere Zeiger"), aber ich habe nie bemerkt, dass dies bedeutet, dass das Objekt * nicht * angeheftet ist. Ich bin überzeugt, dass die letzte Aussage, die Sie bearbeitet haben, die richtige Wahl ist. Ich werde das weiter verfolgen. – usr

1

ich mit Daniel A. White zustimmen Kommentare, die Sie sagen, dass es wahrscheinlich nicht in einer Leistungssteigerung führt aber zu einem Leistungseinbruch aufgrund des Aufwands von usw. auf nicht verwalteten Code Marshalling

gesagt haben, dass Sie sollte GCHandle.Alloc nutzen kann:

public unsafe bool Equals(T[] x, T[] y) 
{ 
    if (ReferenceEquals(x, y)) return true; 
    if (x == null || y == null) return false; 
    if (x.Length != y.Length) return false; 

    GCHandle handleOfX = default(GCHandle); 
    GCHandle handleOfY = default(GCHandle); 
    handleOfX = GCHandle.Alloc(x, GCHandleType.Pinned); 
    handleOfY = GCHandle.Alloc(y, GCHandleType.Pinned); 
    try 
    { 
     return memcmp(handleOfX.AddrOfPinnedObject(), 
         handleOfY.AddrOfPinnedObject(), 
         x.Length * this.elementSize); 
    } 
    finally 
    { 
     if(handleOfX != default(GCHandle)) handleOfX.Free(); 
     if(handleOfY != default(GCHandle)) handleOfY.Free(); 
    } 
} 
+2

Ich gehe um diesen Ansatz zu bewerten und zu posten, was ich gefunden habe. Ich bin besorgt, dass GCHandle's schwer sind. behoben verwendet sie nicht. (Btw, ich werde nur PInvoke für große Arrays. Ich werde kleine Arrays in unsicheren verwalteten Code vergleichen, so gibt es keinen Anruf Overhead überhaupt). – usr

+0

Überprüfen Sie auch dies: http://techmikael.blogspot.co.uk/2009/01/fast-byte-array-comparison-in-c.html Und ich denke, es wird Dinge für große Arrays beschleunigen (obwohl ich denke die Version, die GC verwendet, wird eine Menge Overhead im Vergleich zu nur das Fixieren der Arrays hinzufügen - aber natürlich wird die Fixierung in unsicheren Code führen. –

+0

GCHandle Leistung ist wirklich schrecklich. Ich habe nicht erwartet, dass es so schlimm ist. Dennoch ist es die einzige Lösung, die es nicht erlaubt, sich auf T. zu spezialisieren. – usr

2

Sie können ein separates P/Invoke-Front-End für jede Art von Array schreiben, das Sie vergleichen möchten. Ich weiß, dass du dich nicht wirklich auf T spezialisieren willst, aber der Overhead ist nicht so groß, denke ich.

Dies ist ein bisschen ein Hack, weil ich mehrere P/Invoke-Methoden mit verschiedenen Signaturen für die gleiche API-Funktion definieren, aber dadurch kann ich die P/Invoke Marshalling-Unterstützung nutzen.

(Beachten Sie, dass das Vorzeichen des Rückgabewerts von memcmp nur dann sinnvoll ist, wenn die Quelldaten tatsächlich ein Array von Bytes sind.Wenn Sie ihm ein Array von Ints geben, sollten Sie nur den Rückgabewert mit Null vergleichen und sein Vorzeichen ignorieren. Die Reihenfolge, die es bedeutet, ist nicht sinnvoll für ints)

Zum Beispiel der folgende Code gibt mir die folgende (RELEASE zu bauen, nicht Debug-Build).

MemCmp with ints took 00:00:08.0768666 
ManagedMemCmp with ints took 00:00:10.3750453 
MemCmp with bytes took 00:00:01.8740001 
ManagedMemCmp with bytes took 00:00:09.2885763 

Beachten Sie, dass die byte [] Test verwendet Bytes, vergleicht also ein Viertel der Anzahl der Bytes, die der int [] Test verwendet. Der verwaltete Code nimmt die gleiche Anzahl von Vergleichen vor, ist also vergleichsweise viel langsamer.

Es gibt nicht wirklich einen großen Unterschied zwischen den Zeiten für den Vergleich von Arrays von Ints, aber es gibt einen großen Unterschied für den Vergleich von Arrays von Bytes. Dies deutet darauf hin, dass es eine verwaltete Optimierung geben könnte, die fixed verwendet, um einen Zeiger auf Ints von einem Byte-Array zu erhalten, um 4 Bytes gleichzeitig zu vergleichen (mit etwas fummeln für die möglicherweise zusätzlichen Bytes am Ende, dass don ' t passen in einen int).

Ich denke auch, dass Sie eine multithreaded verwaltete Version (mit der "Int *" -Optimierung zum Vergleich von Byte-Arrays) schreiben könnte, die viel schneller als die nicht verwaltete Memcmp() wäre, die natürlich nicht Multithread ist (soweit ich kennt).

Wie auch immer, hier ist mein Testcode. Denken Sie daran, RELEASE bauen, nicht debuggen!

using System; 
using System.Diagnostics; 
using System.Runtime.InteropServices; 


namespace Demo 
{ 
    public static class Program 
    { 
     private static void Main(string[] args) 
     { 
      int[] a1 = new int[1000000]; 
      int[] a2 = new int[1000000]; 

      for (int i = 0; i < a1.Length-1; ++i) 
      { 
       a1[i] = i; 
       a2[i] = i; 
      } 

      a1[a1.Length-1] = 1; 
      a2[a1.Length-1] = 2; 

      byte[] b1 = new byte[1000000]; 
      byte[] b2 = new byte[1000000]; 

      for (int i = 0; i < b1.Length-1; ++i) 
      { 
       b1[i] = (byte)i; 
       b2[i] = (byte)i; 
      } 

      b1[a1.Length-1] = 1; 
      b2[a1.Length-1] = 2; 

      Stopwatch sw = Stopwatch.StartNew(); 
      testWithMemCmp(a1, a2); 
      sw.Stop(); 
      Console.WriteLine("MemCmp with ints took " + sw.Elapsed); 

      sw.Restart(); 
      testWithManagedMemCmp(a1, a2); 
      sw.Stop(); 
      Console.WriteLine("ManagedMemCmp with ints took " + sw.Elapsed); 

      sw.Restart(); 
      testWithMemCmp(b1, b2); 
      sw.Stop(); 
      Console.WriteLine("MemCmp with bytes took " + sw.Elapsed); 

      sw.Restart(); 
      testWithManagedMemCmp(b1, b2); 
      sw.Stop(); 
      Console.WriteLine("ManagedMemCmp with bytes took " + sw.Elapsed); 
     } 

     private static void testWithMemCmp(int[] a1, int[] a2) 
     { 
      for (int j = 0; j < COUNT; ++j) 
      { 
       MemCmp(a1, a2); 
      } 
     } 

     private static void testWithMemCmp(byte[] a1, byte[] a2) 
     { 
      for (int j = 0; j < COUNT; ++j) 
      { 
       MemCmp(a1, a2); 
      } 
     } 

     private static void testWithManagedMemCmp(int[] a1, int[] a2) 
     { 
      for (int j = 0; j < COUNT; ++j) 
      { 
       ManagedMemCmp(a1, a2); 
      } 
     } 

     private static void testWithManagedMemCmp(byte[] a1, byte[] a2) 
     { 
      for (int j = 0; j < COUNT; ++j) 
      { 
       ManagedMemCmp(a1, a2); 
      } 
     } 

     public static bool ManagedMemCmp(int[] a1, int[] a2) 
     { 
      if (a1 == null || a2 == null || a1.Length != a2.Length) 
      { 
       throw new InvalidOperationException("Arrays are null or different lengths."); 
      } 

      for (int i = 0; i < a1.Length; ++i) 
      { 
       if (a1[i] != a2[i]) 
       { 
        return false; 
       } 
      } 

      return true; 
     } 

     public static bool ManagedMemCmp(byte[] a1, byte[] a2) 
     { 
      if (a1 == null || a2 == null || a1.Length != a2.Length) 
      { 
       throw new InvalidOperationException("Arrays are null or different lengths."); 
      } 

      for (int i = 0; i < a1.Length; ++i) 
      { 
       if (a1[i] != a2[i]) 
       { 
        return false; 
       } 
      } 


      return true; 
     } 

     public static bool MemCmp(byte[] a1, byte[] a2) 
     { 
      if (a1 == null || a2 == null || a1.Length != a2.Length) 
      { 
       throw new InvalidOperationException("Arrays are null or different lengths."); 
      } 

      return memcmp(a1, a2, new UIntPtr((uint)a1.Length)) == 0; 
     } 

     public static bool MemCmp(int[] a1, int[] a2) 
     { 
      if (a1 == null || a2 == null || a1.Length != a2.Length) 
      { 
       throw new InvalidOperationException("Arrays are null or different lengths."); 
      } 

      return memcmp(a1, a2, new UIntPtr((uint)(a1.Length * sizeof(int)))) == 0; 
     } 

     [DllImport("msvcrt.dll")] 
     private static extern int memcmp(byte[] a1, byte[] a2, UIntPtr count); 

     [DllImport("msvcrt.dll")] 
     private static extern int memcmp(int[] a1, int[] a2, UIntPtr count); 

     private const int COUNT = 10000; 
    } 
} 
+0

Ich habe die PInvoke-Version getestet und in der verwalteten Vergleichsschleife verwende ich ulong * ', um 64 Bits gleichzeitig zu vergleichen. Die Zahlen sind viel besser als die sichere, naive Element-für-Element-Schleife (was nicht überraschend ist, genau wie du gesagt hast). – usr

+0

Vielen Dank für diese Antwort. Es half bei der Implementierung der Lösung, die ich jetzt habe: Für die Länge <8 verwenden Sie eine sichere Einzelelement-Schleife. Verwenden Sie für die Länge <512 die unsichere 64-Bit-Schleife. In anderen Fällen verwenden Sie memcmp. – usr