2015-05-05 29 views
5

Ich lese gerade die ECMA-334 wie von einem Freund vorgeschlagen, die Programmierung für einen Lebensunterhalt macht. Ich bin auf dem Abschnitt mit unsicheren Code beschäftigt. Obwohl ich etwas verwirrt bin über das, worüber sie sprechen.Understanding Unsafe Code und seine Verwendungen

Der dem C# zugrunde liegende Garbage Collector funktioniert möglicherweise, indem Objekte im Speicher verschoben werden, aber diese Bewegung ist für die meisten C# -Entwickler unsichtbar. Für Entwickler, die im Allgemeinen mit automatischem Speicher Management zufrieden sind, aber manchmal feinkörnige Kontrolle oder das zusätzliche Bit der Leistung benötigen, bietet C# die Möglichkeit, "unsicheren" Code zu schreiben. Solch ein Code kann direkt mit Zeigertypen und Objektadressen umgehen; C# erfordert jedoch, dass der Programmierer Objekte temporär repariert verhindert, dass der Garbage Collector sie bewegt. Diese "unsichere" Code Funktion ist in der Tat eine "sichere" Funktion aus der Perspektive der beiden Entwickler und Benutzer . Unsicherer Code muss deutlich im Code mit dem Modifikator unsafe gekennzeichnet sein, so dass Entwickler möglicherweise nicht unsichere Sprachfunktionen versehentlich verwenden können, und der Compiler und die Ausführung zusammenarbeiten, um sicherzustellen, dass unsichere Code nicht maskieren kann als sicherer Code. Diese Einschränkungen beschränken die Verwendung von unsicheren Codes auf Situationen, in denen der Code vertrauenswürdig ist.

Das Beispiel

using System; 
class Test 
{ 
    static void WriteLocations(byte[] arr) 
    { 
     unsafe 
     { 
      fixed (byte* pArray = arr) 
      { 
       byte* pElem = pArray; 
       for (int i = 0; i < arr.Length; i++) 
       { 
        byte value = *pElem; 
        Console.WriteLine("arr[{0}] at 0x{1:X} is {2}", 
        i, (uint)pElem, value); 
        pElem++; 
       } 
      } 
     } 
    } 
    static void Main() 
    { 
     byte[] arr = new byte[] { 1, 2, 3, 4, 5 }; 
     WriteLocations(arr); 
     Console.ReadLine(); 
    } 
} 

zeigt ein unsicheres Block in einem Verfahren namens WriteLocations, die eine Array-Instanz Behebungen und Zeigermanipulation verwendet über die Elemente iterieren. Der Index, der Wert und die Position jedes Array-Elements werden in die Konsole geschrieben. Ein mögliches Beispiel Ausgabe lautet:

arr[0] at 0x8E0360 is 1 
arr[1] at 0x8E0361 is 2 
arr[2] at 0x8E0362 is 3 
arr[3] at 0x8E0363 is 4 
arr[4] at 0x8E0364 is 5 

aber natürlich die genauen Speicherplätze können in verschiedenen Ausführungen der Anwendung unterschiedlich sein.

Warum ist es für uns als Entwickler von Vorteil, die genauen Speicherpositionen zu kennen? Und könnte jemand dieses Ideal in einem vereinfachten Kontext erklären?

+0

Es ist fast immer nicht, und Sie sollten Ihr Bestes tun, um es zu vermeiden. –

+0

Auch das Fixieren von Objekten und das Ermitteln ihres Standorts ist manchmal für Interop notwendig, aber normalerweise brauchen Sie keinen unsicheren Code dafür –

+1

Wenn Sie diese Frage stellen müssen, brauchen Sie wahrscheinlich keinen unsicheren Code;). Unsicherer Code kann schneller sein und Sie können auf Speicher zugreifen, der von nicht verwaltetem Code verwendet wird. –

Antwort

4

Die Sprachfunktion fixed ist nicht gerade "vorteilhaft", da sie "absolut notwendig" ist.

Normalerweise ein C# Benutzer vorstellen Referenz-Typen als gleichwertig zu Einzel indirection Zeiger (zB für class Foo, dies: Foo foo = new Foo(); diese C entspricht ++:. Foo* foo = new Foo();

In Wirklichkeit Referenzen in C# sind näher an Zeiger mit doppelter Indirektion ist ein Zeiger (oder vielmehr ein Handle) auf einen Eintrag in einer massiven Objekttabelle, der dann die tatsächlichen Adressen von Objekten speichert.Der GC bereinigt nicht nur nicht verwendete Objekte, sondern auch also move objects around in memory, um eine Speicherfragmentierung zu vermeiden.

All das ist gut und gut, wenn Sie ausschließlich Objektreferenzen in C# verwenden Wenn Sie Zeiger verwenden, dann haben Sie Probleme, weil der GC bei jeden Punkt rechtzeitig laufen kann, selbst während der Tight-Loop-Ausführung, und wenn der GC läuft, ist die Ausführung des Programms eingefroren (weshalb die CLR und Java nicht sind) Geeignet für harte Echtzeitanwendungen - eine GC-Pause kann in einigen Fällen einige hundert Millisekunden dauern.

... aufgrund dieses inhärenten Verhaltens (wo ein Objekt während der Codeausführung verschoben wird) müssen Sie verhindern, dass das Objekt verschoben wird. Daher das Schlüsselwort fixed, das den GC anweist, dieses Objekt nicht zu verschieben.

Ein Beispiel:

unsafe void Foo() { 

    Byte[] safeArray = new Byte[ 50 ]; 
    safeArray[0] = 255; 
    Byte* p = &safeArray[0]; 

    Console.WriteLine("Array address: {0}", &safeArray); 
    Console.WriteLine("Pointer target: {0}", p); 
    // These will both print "0x12340000". 

    while(executeTightLoop()) { 
     Console.WriteLine(*p); 
     // valid pointer dereferencing, will output "255". 
    } 

    // Pretend at this point that GC ran right here during execution. The safeArray object has been moved elsewhere in memory. 

    Console.WriteLine("Array address: {0}", &safeArray); 
    Console.WriteLine("Pointer target: {0}", p); 
    // These two printed values will differ, demonstrating that p is invalid now. 
    Console.WriteLine(*p) 
    // the above code now prints garbage (if the memory has been reused by another allocation) or causes the program to crash (if it's in a memory page that has been released, an Access Violation) 
} 

Statt durch fixed zum safeArray Objekt anwenden, wird der Zeiger p wird immer ein gültiger Zeiger sein und nicht zu einem Absturz führen oder Müll Daten umgehen.

Randnotiz: Eine Alternative zu fixed ist die Verwendung stackalloc, aber das begrenzt die Lebensdauer des Objekts auf den Umfang Ihrer Funktion.

1

Einer der Hauptgründe, die ich fixed verwende, ist die Schnittstelle mit nativem Code. Angenommen, Sie haben eine native Funktion mit der folgenden Signatur:

double cblas_ddot(int n, double* x, int incx, double* y, int incy); 

Sie könnten eine Interop-Wrapper wie folgt schreiben:

public static extern double cblas_ddot(int n, [In] double[] x, int incx, 
             [In] double[] y, int incy); 

und C# -Code schreiben es so zu nennen:

double[] x = ... 
double[] y = ... 
cblas_dot(n, x, 1, y, 1); 

Aber nun nehme ich an, ich wollte mit einigen Daten in der Mitte meines Arrays arbeiten, beginnend bei x [2] und y [2]. Es gibt keine Möglichkeit, den Anruf zu tätigen, ohne das Array zu kopieren.

double[] x = ... 
double[] y = ... 
cblas_dot(n, x[2], 1, y[2], 1); 
      ^^^^ 
      this wouldn't compile 

In diesem Fall kommt zur Rettung. Wir können die Signatur des Interops ändern und fixed vom Aufrufer verwenden.

public unsafe static extern double cblas_ddot(int n, [In] double* x, int incx, 
               [In] double* y, int incy); 

double[] x = ... 
double[] y = ... 
fixed (double* pX = x, pY = y) 
{ 
    cblas_dot(n, pX + 2, 1, pY + 2, 1); 
} 

Ich habe auch in seltenen Fällen festgelegt, wo ich schnell Schleifen über Arrays und benötigen brauchen wurde, um die .NET-Array-Grenzen, um sicherzustellen, Überprüfung nicht passiert.

1

Im Allgemeinen sind die genauen Speicherplätze innerhalb eines "unsicheren" Blocks nicht so relevant.

Wie in Dai`s answer erläutert, müssen Sie bei der Verwendung von Garbage Collector verwaltetem Speicher sicherstellen, dass die zu bearbeitenden Daten nicht verschoben werden (mit "fixed"). Sie verwenden in der Regel die, wenn

  • Sie sind eine Performance kritische Operation viele Male in einer Schleife und rohe Byte Strukturen Manipulation ausreichend schneller.
  • Sie tun Interop und haben einige nicht-Standard Data Marshalling benötigt.

In einem einigen Fällen Sie mit Speicher arbeiten, die nicht von der Garbage Collector, einige Beispiele für solche Szenarien sind verwaltet:

  • Wenn tun Interop mit unmanaged Code, kann es sein, wird verwendet, um ein wiederholtes Marshalling von Daten hin und her zu verhindern, und stattdessen einige Arbeit in Brocken mit größerer Granularität unter Verwendung der "rohen Bytes" oder Strukturen, die diesen rohen Bytes zugeordnet sind.
  • Wenn Low-Level-IO mit großen Puffern, die Sie mit dem Betriebssystem (z. B. für Scatter/sammeln IO) freigeben müssen.
  • Beim Erstellen bestimmter Strukturen in einer Speicherabbilddatei. Ein Beispiel könnte zum Beispiel ein B + Tree mit Knoten mit Speichergröße sein, der in einer festplattenbasierten Datei gespeichert ist, die Sie in den Speicher laden möchten.