2013-02-12 5 views
5

Sto lavorando a un IEqualityComparer che dovrebbe confrontare molto velocemente le matrici di tipi primitivi. Il mio piano è di ottenere i puntatori agli array e memcmp loro. Come questo:Come usare fisso con una variabile di tipo Array o 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); 
     } 
    } 

La dichiarazione fissa non mi permette al piedino sia Array o T[].

messaggi di errore ci sono:

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') 

Ora, io in realtà non importa quanto io faccio questo lavoro (non sto impegnato a questo approccio esatto). Come faccio a memcmp due T[] dove so che T è un tipo primitivo/blittabile?

Voglio davvero evitare di accendere il tipo e creare una versione di codice specializzata (e duplicata) per ogni tipo interessante. Qualsiasi tipo di soluzione di riflessione non è praticabile a causa di limiti prestazionali (e sì, ho davvero bisogno delle prestazioni qui - non si applicano avvisi di ottimizzazione prematuri in quanto è consuetudine su Stack Overflow).

+1

non è possibile chiamare 'memcpy' da C# –

+0

Inoltre, hai provato' pubblica non sicura di override bool Equals (T [] x, T [] y) dove T: struct'? – Davio

+1

perché stai cercando di ottimizzare il tuo codice? probabilmente stai creando più problemi di prestazioni tramite il marshalling verso il codice non gestito. –

risposta

5

dove so che T è un primitivo/tipo copiabili

Lo sai, il compilatore non lo sa. Il CLR richiede che tutto ciò che è in un oggetto bloccato non possa più essere spostato dal garbage collector. Per una matrice, che include i suoi elementi dell'array. Solo il tipo di T che qualifica è un tipo di valore semplice, uno che è blittable. Generics non ti dà un modo per vincolare T a un tipo blittable.

Normalmente si dichiarano gli argomenti di memcmp() come byte []. Il marshaller di pinvoke quindi fa già la cosa giusta e attaccherà gli array di byte [] prima di chiamare memcmp(). Tuttavia, ciò non funzionerà poiché non è possibile convertire facilmente un T [] in un byte []. Dovrai metterti alla prova con GCHandle. Dichiarare gli argomenti memcmp() come IntPtr invece di byte [] di conseguenza.

Il sottoinsieme di tipi che può funzionare è in pratica abbastanza piccolo da considerare semplicemente la scrittura di overload di metodi anziché un metodo generico. Che ora abilita il marshaller di pinvoke a prendersi cura del pinning, sovraccaricando di conseguenza le dichiarazioni della funzione memcmp().

+1

Hai qualche riferimento all'istruzione 'fixed' _non_ appuntando oggetti in memoria, che è [cosa] (http://msdn.microsoft.com/en-gb/library/ f58wzh21.aspx) è [supposto] (http://blogs.msdn.com/b/ericlippert/archive/2009/08/27/what-s-the-difference-between-fixed-and-fixed.aspx) a – Rawling

+0

Basta guardare l'IL generato dall'istruzione –

+1

Molto prezioso, Hans. In qualche modo sapevo che il compilatore C# usa solo ma puntatori nani ("puntatori interni") ma non ho mai capito che questo significa che l'oggetto è * non * appuntato .; Sono convinto ora che l'ultima affermazione che hai modificato è la scelta giusta per il design. Seguirò su questo. – usr

1

Sono d'accordo con i commenti di Daniel A. White dicendo che probabilmente non si traduca in un aumento di prestazioni, ma in un calo di prestazioni a causa del sovraccarico di marshalling di codice non gestito ecc

Detto questo, è dovrebbe essere in grado di utilizzare GCHandle.Alloc:

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

sto andando per confrontare questo approccio e pubblicare ciò che ho trovato. Sono preoccupato che GCHandle sia pesante. risolto non li usa. (A proposito, avrò solo PInvoke per i grandi array. Vado a confrontare piccoli array in codice gestito non sicuro, quindi non ci sono overhead di chiamata). – usr

+0

Controlla anche questo: http://techmikael.blogspot.co.uk/2009/01/fast-byte-array-comparison-in-c.html E penso che accelera le cose per gli array LARGE (anche se penso la versione che usa GC aggiungerà un sacco di overhead rispetto al solo aggiustamento degli array - ma ovviamente il fixing risulterà in un codice non sicuro –

+0

Le prestazioni di GCHandle sono davvero terribili.Non mi aspettavo che fosse così male. unica soluzione che permette di non specializzarsi su T. – usr

2

È possibile scrivere un front-end P/Invoke separato per ogni tipo di array che si desidera confrontare. So che non vuoi davvero specializzarti su T, ma penso che il sovraccarico non sia troppo grande.

Questo è un po 'un hack, perché sto definendo più metodi P/Invoke con firme diverse per la stessa funzione API, ma facendo questo posso sfruttare il supporto di marshalling P/Invoke.

(Si noti che il segno del valore restituito da memcmp è davvero significativo solo se i dati di origine sono effettivamente una matrice di byte.Se gli stai dando una serie di valori, dovresti solo confrontare il valore restituito con zero e ignorare il suo segno. L'ordinamento che essa implica non è significativo per interi)

Ad esempio, il seguente codice stampa la seguente per me (STAMPA costruire, non build di debug):.

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 

Si noti che il byte [] prova sta usando i byte quindi confronta un quarto del numero di byte rispetto al test int [] utilizzato. Il codice gestito fa lo stesso numero di confronti, quindi è comparativamente molto più lento.

Non c'è davvero un'enorme differenza tra i tempi per il confronto di matrici di interi, ma c'è una grande differenza per il confronto di matrici di byte. Questo mi suggerisce che potrebbe esserci un'ottimizzazione gestita che utilizza fissa per ottenere un puntatore a ints da una matrice di byte per confrontare 4 byte alla volta (con qualche giochino per i byte eventualmente extra alla fine che don ' t adatto a un int).

Penso anche che potresti scrivere una versione gestita multithread (utilizzando l'ottimizzazione "int *" per confrontare gli array di byte) che sarebbe MOLTO PIÙ FACILE rispetto al memcmp non gestito(), che ovviamente non è multithread (per quanto mi riguarda conoscere).

In ogni caso, ecco il mio codice di prova. Ricorda, RILASCIARE costruire, non eseguire il debug!

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

Ho testato la versione di PInvoke e nel ciclo di confronto gestito uso 'ulong *' per confrontare 64 bit alla volta. I numeri sono molto meglio del sicuro, ingenuo elemento per elemento (che non sorprende proprio come hai detto tu). – usr

+0

Grazie per questa risposta. Ha aiutato a implementare la soluzione che ho ora: per la lunghezza <8 usa un loop di un singolo elemento sicuro. Per lunghezza <512 utilizzare il loop a 64 bit non sicuro. In altri casi usa memcmp. – usr