2016-05-09 12 views
20

Warum enthält der X86 für die folgende C# -Methode die Anweisung cmp?Warum überprüfen C# -Struct-Instanzmethoden, die Instanzmethoden in einem Struct-Feld aufrufen, zuerst ecx?

Hier ist ein vollständigeres Programm, das mit verschiedenen (Release-) Dekompilierungen als Kommentare zusammengestellt werden kann. Ich erwartete, dass die X86 für CallViaStruct sowohl in ClassDispatch und StructDispatch Typen identisch sein, aber die Version in StructDispatch (extrahiert oben) enthält eine cmp Anweisung, während die andere nicht.

Es scheint die cmp Anweisung ist ein Idiom wird verwendet, um sicherzustellen, dass eine Variable nicht null ist; Dereferenzierung eines Registers mit dem Wert 0 löst eine av aus, die in eine NullReferenceException umgewandelt wird. Aber in StructDisptach.CallViaStruct kann ich mir keinen Weg vorstellen für ecx als null, da es auf eine Struktur zeigt.

UPDATE: Die Antwort, die ich suche bin zu werden Code annehmen, umfassen, die eine NRE verursacht durch StructDisptach.CallViaStruct geworfen werden, indem es mit cmp Anweisung dereferenziert ein genullt ecx Register. Beachten Sie, dass dies mit den beiden Methoden CallViaClass durch Setzen von m_class = null und ClassDisptach.CallViaStruct problemlos möglich ist, da es keine cmp Anweisung gibt.

using System.Runtime.CompilerServices; 

namespace NativeImageTest { 

    struct Struct { 
     public void NoOp() { } 
    } 

    class Class { 
     public void NoOp() { } 
    } 

    class ClassDisptach { 

     Class m_class; 
     Struct m_struct; 

     internal ClassDisptach(Class cls) { 
      m_class = cls; 
      m_struct = new Struct(); 
     } 

     [MethodImpl(MethodImplOptions.NoInlining)] 
     public void CallViaClass() { 
      m_class.NoOp(); 
      //push  ebp 
      //mov   ebp,esp 
      //mov   eax,dword ptr [ecx+4] 
      //cmp   byte ptr [eax],al 
      //pop   ebp 
      //ret 
     } 

     [MethodImpl(MethodImplOptions.NoInlining)] 
     public void CallViaStruct() { 
      m_struct.NoOp(); 
      //push  ebp 
      //mov   ebp,esp 
      //pop   ebp 
      //ret 
     } 
    } 

    struct StructDisptach { 

     Class m_class; 
     Struct m_struct; 

     internal StructDisptach(Class cls) { 
      m_class = cls; 
      m_struct = new Struct(); 
     } 

     [MethodImpl(MethodImplOptions.NoInlining)] 
     public void CallViaClass() { 
      m_class.NoOp(); 
      //push  ebp 
      //mov   ebp,esp 
      //mov   eax,dword ptr [ecx] 
      //cmp   byte ptr [eax],al 
      //pop   ebp 
      //ret 
     } 

     [MethodImpl(MethodImplOptions.NoInlining)] 
     public void CallViaStruct() { 
      m_struct.NoOp(); 
      //push  ebp 
      //mov   ebp,esp 
      //cmp   byte ptr [ecx],al 
      //pop   ebp 
      //ret 
     } 
    } 

    class Program { 
     static void Main(string[] args) { 
      var classDispatch = new ClassDisptach(new Class()); 
      classDispatch.CallViaClass(); 
      classDispatch.CallViaStruct(); 

      var structDispatch = new StructDisptach(new Class()); 
      structDispatch.CallViaClass(); 
      structDispatch.CallViaStruct(); 
     } 
    } 
} 

UPDATE: Stellt sich heraus, es möglich ist, callvirt auf einer nicht-virtuelle Funktion zu verwenden, das eine Nebenwirkung von null hat den diesen Zeiger zu überprüfen. Während dies für die CallViaClass Callsite der Fall ist (weshalb wir den Null-Check dort sehen) StructDispatch.CallViaStruct verwendet eine call Anweisung.

.method public hidebysig instance void CallViaClass() cil managed noinlining 
{ 
    // Code size  12 (0xc) 
    .maxstack 8 
    IL_0000: ldarg.0 
    IL_0001: ldfld  class NativeImageTest.Class NativeImageTest.StructDisptach::m_class 
    IL_0006: callvirt instance void NativeImageTest.Class::NoOp() 
    IL_000b: ret 
} // end of method StructDisptach::CallViaClass 

.method public hidebysig instance void CallViaStruct() cil managed noinlining 
{ 
    // Code size  12 (0xc) 
    .maxstack 8 
    IL_0000: ldarg.0 
    IL_0001: ldflda  valuetype NativeImageTest.Struct NativeImageTest.StructDisptach::m_struct 
    IL_0006: call  instance void NativeImageTest.Struct::NoOp() 
    IL_000b: ret 
} // end of method StructDisptach::CallViaStruct 

UPDATE: Es gab einen Vorschlag, dass die cmp für den Fall Trapping werden könnte, wo ein null diese Zeiger nicht an der Aufrufstelle gefangen wurden. Wenn das der Fall wäre, würde ich erwarten, dass die cmp einmal am Anfang der Methode auftritt. Allerdings scheint es, einmal für jeden Anruf zu NoOp:

struct StructDisptach { 

    Struct m_struct; 

    [MethodImpl(MethodImplOptions.NoInlining)] 
    public void CallViaStruct() { 
     m_struct.NoOp(); 
     m_struct.NoOp(); 
     //push  ebp 
     //mov   ebp,esp 
     //cmp   byte ptr [ecx],al 
     //cmp   byte ptr [ecx],al 
     //pop   ebp 
     //ret 
    } 
} 
+1

Das Auslösen eines NRE auf diesem 'cmp' ist einfach, wenn Sie das Feld' Class' aus 'StructDisptach' entfernen und dann' unsafe {((StructDisptach *) 0) aufrufen -> CallViaStruct(); } ', aber ich schätze, dass die Verwendung eines unsicheren Kontextes betrügt;) –

+0

HaHA! Großartige Idee! Es scheint jedoch ein wenig zweifelhaft zu sein, dass das JIT die Probleme für diesen Fall auffangen würde - insbesondere angesichts des Klassenfeldes. Und selbst wenn das der Grund ist (und einen unoptimierten Fall sah), wenn ich zwei Anrufe an "NoOp" stelle, bekomme ich den Scheck zweimal. Also wäre die Frage, wofür ist die zweite? –

+0

Ja, deshalb sage ich, dass es betrügen würde - wenn ich die Antwort wüsste, würde ich es dir sagen :) Es könnte doch nur ein Versehen in der JIT sein, denn ich weiß es nicht jede bessere Erklärung für dieses 'cmp' ist hier: - \ –

Antwort

3

Kurze Antwort: Das Flattern kann nicht beweisen, dass die Struktur nicht durch einen Zeiger verwiesen wird, und muss mindestens dereferenzieren mindestens einmal bei jedem Aufruf zu NoOp() für richtiges Verhalten.


Lange Antwort: Structs sind komisch.

Der JITter ist konservativ. Wo immer es möglich ist, kann es den Code nur so optimieren, dass er absolut bestimmte korrektes Verhalten produzieren kann. "Meistens korrekt" ist nicht gut genug.

Also jetzt ist hier ein Beispielszenario, das brechen würde, wenn der JITter die Dereferenzierung wegoptimiert. Berücksichtigen Sie die folgenden Fakten:

Erstens: Denken Sie daran, dass Strukturen außerhalb von C# (und tun!) Existieren können - ein Zeiger auf ein StructDispatch könnte zum Beispiel aus nicht verwaltetem Code stammen. Wie Lucas darauf hingewiesen hat, kannst du mit Hilfe von Zeigern schummeln; aber der JITter kann nicht mit Sicherheit wissen, dass Sie nirgendwo im Code Zeiger auf StructDispatch verwenden.

Zweitens: Denken Sie daran, dass im nicht verwalteten Code, der der wichtigste Grund dafür ist, dass Strukturen überhaupt existieren, alle Wetten deaktiviert sind. Nur weil Sie gerade einen Wert aus dem Speicher gelesen haben, heißt das nicht, dass es den gleichen Wert oder sogar ein Wert sein wird, wenn Sie das nächste Mal die gleiche genaue Adresse lesen. Threading und Multiprocessing können diesen Wert im nächsten Takt buchstäblich verändern, ganz zu schweigen von Nicht-CPU-Acts wie DMA. Ein paralleler Thread könnte VirtualFree() die Seite, die diese Struktur enthält, und der JITter davor schützen. Sie haben nach Lesevorgängen aus dem Speicher gefragt, sodass Sie Lesevorgänge aus dem Speicher erhalten. Meine Vermutung ist, dass, wenn Sie in den Optimierer traten, würde es eine dieser CMP-Anweisungen entfernen, aber ich bezweifle stark, dass es beide entfernen würde.

Drittens: Ausnahmen sind auch echter Code. NullReferenceException beendet das Programm nicht unbedingt; es kann gefangen und gehandhabt werden. Das bedeutet, dass aus der JITter-Perspektive NRE mehr wie eine if-Anweisung als ein goto ist: Es ist eine Art Bedingungszweig, der bei jeder Speicher-Dereferenzierung behandelt und berücksichtigt werden muss.

Also jetzt diese Stücke zusammenfügen.

Der JITter weiß nicht - und kann nicht wissen -, dass Sie nicht mit unsicheren C# oder einer externen Quelle woanders arbeiten, um mit dem Speicher von StructDispatch zu interagieren. Es erzeugt keine separaten Implementierungen von CallViaStruct(), eine für "wahrscheinlich sicheren C# -Code" und eine für "möglicherweise riskanten externen Code"; es erzeugt immer die konservative Version für möglicherweise riskante Szenarien. Das bedeutet, dass Aufrufe von NoOp() nicht vollständig ausgeschnitten werden können, da es keine Garantie dafür gibt, dass StructDispatch nicht einer Adresse zugeordnet ist, die nicht einmal in den Speicher ausgelagert wurde.

Es weiß, dass NoOp() leer ist, und kann (der Anruf geht weg) elided werden, aber es hat zumindest zu den ldfla simulieren, indem die Speicheradresse der Struktur Stossen, weil es Code sein könnte je darauf, dass NRE angehoben wird. Speicher-Dereferenzierungen sind wie if-Anweisungen: Sie können eine Verzweigung verursachen, und eine Verzweigung kann zu einem fehlerhaften Programm führen. Microsoft kann keine Annahmen treffen und nur sagen: "Ihr Code sollte sich nicht darauf verlassen." Stellen Sie sich den wütenden Telefonanruf bei Microsoft vor, wenn ein NRE nicht in das Fehlerprotokoll eines Unternehmens geschrieben wurde, nur weil das JITter entschieden hat, dass es nicht "wichtig genug" war, um NRE auszulösen. Der JITter hat keine andere Wahl, als diese Adresse mindestens einmal zu dereferenzieren, um eine korrekte Semantik sicherzustellen.


Klassen haben keine dieser Bedenken; Es gibt keine erzwungene Gedächtnisverrücktheit mit einer Klasse. Aber Strukturen sind merkwürdiger.

+0

In Ihrem letzten Absatz, in Bezug darauf, was der Compiler annehmen kann oder nicht, hängt es davon ab, ob der Zugriff auf eine Struktur über einen Nullzeiger definiert ist oder nicht. Wenn es _undefined Behavior_ ist, können Compiler-Schreiber (und häufig) nicht nur ignorieren, was man normalerweise als "Vernunft-Checks" betrachtet, sondern auch explizit entfernen, weil der Compiler annehmen darf, dass UB nicht auftreten kann. Seht euch zum Beispiel [dieser liebe Kerl] (https://gcc.gnu.org/bugzilla/show_bug.cgi?id=30475) an, der sich über eine Überprüfung des signierten Überlaufs (UB in C) beschwert, der von GCC entfernt wurde. –

+0

Es ist ein gültiger Punkt für Sprachen wie C, die ein undefiniertes Verhalten als Teil ihrer Spezifikation haben. Aber C# soll kein undefiniertes Verhalten haben (innerhalb der Grenzen der Semantik für eine typische Programmiersprache definieren). Dies erzwingt diese Art von Problem: Sobald Sie das Verhalten für alle Vorgänge definieren müssen, können Sie keines dieser Verhaltensweisen ignorieren, wenn sie unbequem sind oder zu langsameren Code führen. C# muss diesen Speicher dereferenzieren, da die NRE erwartet wird, wohldefiniertes Verhalten für den Zugriff auf Speicher durch einen Nullzeiger, und das Entfernen der NRE würde die Spezifikation verletzen. –

+0

Ohne die Spezifikation zu überprüfen, konnte ich nicht wissen, aber Strukturen sollten immer nicht-Null sein, wenn sie nicht eingerahmt sind, also sollte eine nicht-virtuelle Funktion einer Struktur * ihr * nicht überprüfen müssen. Selbst wenn Sie mit unsicherem Code 'MyStruct * p = null;} verwenden und dann versuchen,' * p' als Variable vom Typ 'MyStruct' zurückzugeben, sollte die NRE nicht später bei '* p' auftreten . Gleiches gilt für das Unboxing. Der Versuch, ein Null-Objekt in eine Variable 'MyStruct s' zu konvertieren, sollte dann die NRE erhöhen, nicht später, wenn Mitglieder von' s' aufgerufen werden. –