Ist die Microsoft-Empfehlung zur Verwendung von C # -Eigenschaften auf die Spieleentwicklung anwendbar?


26

Ich verstehe, dass Sie manchmal Eigenschaften benötigen, wie:

public int[] Transitions { get; set; }

oder:

[SerializeField] private int[] m_Transitions;
public int[] Transitions { get { return m_Transitions; } set { m_Transitions = value; } }

Mein Eindruck ist jedoch, dass in der Spieleentwicklung, sofern Sie keinen Grund haben, etwas anderes zu tun, alles so schnell wie möglich sein sollte. Mein Eindruck von den Dingen, die ich gelesen habe, ist, dass Unity 3D nicht sicher ist, ob der Zugriff über Eigenschaften inline erfolgt. Daher führt die Verwendung einer Eigenschaft zu einem zusätzlichen Funktionsaufruf.

Habe ich das Recht, die Vorschläge von Visual Studio, keine öffentlichen Felder zu verwenden, ständig zu ignorieren? (Beachten Sie, dass wir nur dann verwenden, int Foo { get; private set; }wenn es von Vorteil ist, die beabsichtigte Verwendung anzuzeigen.)

Ich bin mir bewusst, dass eine vorzeitige Optimierung schlecht ist, aber wie ich bereits sagte, macht man in der Spieleentwicklung etwas nicht langsam, wenn eine ähnliche Anstrengung es schnell machen könnte.


34
Überprüfen Sie immer mit einem Profiler, bevor Sie glauben, dass etwas "langsam" ist. Mir wurde gesagt, etwas sei "langsam" und der Profiler sagte mir, es müsse 500.000.000 Mal ausgeführt werden, um eine halbe Sekunde zu verlieren. Da es niemals mehr als ein paar hundert Mal in einem Frame ausgeführt werden würde, entschied ich, dass es irrelevant war.
Almo

5
Ich habe festgestellt, dass ein bisschen Abstraktion hier und da dazu beiträgt, Optimierungen auf niedriger Ebene allgemeiner anzuwenden. Ich habe zum Beispiel zahlreiche einfache C-Projekte gesehen, in denen verschiedene Arten von verknüpften Listen verwendet wurden, die im Vergleich zu zusammenhängenden Arrays (z. B. das Backing für ArrayList) furchtbar langsam sind . Dies liegt daran, dass C keine Generika hat und diese C-Programmierer es wahrscheinlich leichter fanden, verknüpfte Listen wiederholt neu zu implementieren, als dasselbe für zu tunArrayLists den Größenänderungsalgorithmus . Wenn Sie eine gute Low-Level-Optimierung finden, kapseln Sie diese ein!
hegel5000

3
@Almo Wenden Sie dieselbe Logik auf Vorgänge an, die an 10.000 verschiedenen Orten ausgeführt werden? Weil das der Klassenmitgliedszugriff ist. Logischerweise sollten Sie die Leistungskosten mit 10.000 multiplizieren, bevor Sie entscheiden, welche Optimierungsklasse keine Rolle spielt. Und 0,5 ms sind eine bessere Faustregel, wenn die Leistung Ihres Spiels beeinträchtigt wird, nicht eine halbe Sekunde. Ihr Punkt bleibt bestehen, obwohl ich nicht der Meinung bin, dass die richtige Aktion so klar ist, wie Sie es sich vorstellen.
Piojo

5
Du hast 2 Pferde. Fahre mit ihnen.
Mast

@piojo weiß ich nicht. Einige Gedanken müssen immer in diese Dinge gehen. In meinem Fall war es für Schleifen im UI-Code. Eine UI-Instanz, wenige Schleifen, wenige Iterationen. Für das Protokoll habe ich Ihren Kommentar positiv bewertet. :)
Almo

Antworten:


44

Mein Eindruck ist jedoch, dass in der Spieleentwicklung, sofern Sie keinen Grund haben, etwas anderes zu tun, alles so schnell wie möglich sein sollte.

Nicht unbedingt. Genau wie in Anwendungssoftware gibt es Code in einem Spiel, der leistungskritisch ist, und Code, der es nicht ist.

Wenn der Code mehrere tausend Mal pro Frame ausgeführt wird, ist eine solche Optimierung auf niedriger Ebene möglicherweise sinnvoll. (Obwohl Sie vielleicht zuerst prüfen möchten, ob es tatsächlich notwendig ist, dies so oft aufzurufen. Der schnellste Code ist Code, den Sie nicht ausführen.)

Wenn der Code alle paar Sekunden ausgeführt wird, ist die Lesbarkeit und Wartbarkeit weitaus wichtiger als die Leistung.

Ich habe gelesen, dass Unity 3D nicht sicher ist, ob der Inline-Zugriff über Eigenschaften erfolgt. Die Verwendung einer Eigenschaft führt daher zu einem zusätzlichen Funktionsaufruf.

Mit trivialen Eigenschaften im Stil von können int Foo { get; private set; }Sie ziemlich sicher sein, dass der Compiler den Property Wrapper wegoptimiert. Aber:

  • Wenn Sie tatsächlich eine Implementierung haben, können Sie nicht mehr so ​​sicher sein. Je komplexer diese Implementierung ist, desto unwahrscheinlicher ist es, dass der Compiler sie einbinden kann.
  • Eigenschaften können die Komplexität verbergen. Dies ist ein zweischneidiges Schwert. Einerseits macht es Ihren Code lesbarer und änderbarer. Andererseits könnte ein anderer Entwickler, der auf eine Eigenschaft Ihrer Klasse zugreift, denken, dass er nur auf eine einzelne Variable zugreift und nicht weiß, wie viel Code Sie tatsächlich hinter dieser Eigenschaft versteckt haben. Wenn eine Eigenschaft rechnerisch teuer zu werden beginnt, dann neige ich dazu , es in eine explizite Refactoring GetFoo()/ SetFoo(value)Methode: implizieren , dass es viel mehr ist hinter den Kulissen passiert. (oder wenn ich noch sicherer sein muss, dass andere den Hinweis bekommen: CalculateFoo()/ SwitchFoo(value))

Der Zugriff auf ein Feld in einem nicht schreibgeschützten öffentlichen Feld des Strukturtyps ist schnell, auch wenn sich dieses Feld in einer Struktur befindet, die sich in einer Struktur usw. befindet, vorausgesetzt, das Objekt der obersten Ebene ist entweder eine nicht schreibgeschützte Klasse Feld oder eine nicht schreibgeschützte freistehende Variable. Strukturen können sehr groß sein, ohne die Leistung zu beeinträchtigen, wenn diese Bedingungen zutreffen. Das Aufbrechen dieses Musters führt zu einer Verlangsamung proportional zur Strukturgröße.
Supercat

5
"dann neige ich dazu, es in eine explizite GetFoo () / SetFoo (value) - Methode umzugestalten:" - Guter Punkt, ich neige dazu, schwere Operationen aus Eigenschaften in Methoden zu verschieben.
Mark Rogers

Gibt es eine Möglichkeit zu bestätigen, dass der Unity 3D-Compiler die von Ihnen erwähnte Optimierung durchführt (in IL2CPP- und Mono-Builds für Android)?
Piojo

9
@piojo Wie bei den meisten Fragen zur Leistung ist die einfachste Antwort "Mach es einfach". Sie haben den Code bereit - testen Sie den Unterschied zwischen einem Feld und einer Eigenschaft. Die Implementierungsdetails sind nur dann zu untersuchen, wenn Sie tatsächlich einen wichtigen Unterschied zwischen den beiden Ansätzen messen können. Wenn Sie alles so schnell wie möglich schreiben, begrenzen Sie meiner Erfahrung nach die verfügbaren Optimierungen auf hoher Ebene und versuchen nur, die Teile auf niedriger Ebene zu rasen ("Ich sehe keinen Wald vor lauter Bäumen"). Wenn UpdateSie beispielsweise Möglichkeiten zum vollständigen Überspringen finden, sparen Sie mehr Zeit als durch Updateschnelles Arbeiten.
Luaan

9

Verwenden Sie im Zweifelsfall bewährte Methoden.

Sie haben Zweifel.


Das war die einfache Antwort. Die Realität ist natürlich komplexer.

Erstens gibt es den Mythos, dass Spieleprogrammierung Ultra-Super-Hochleistungsprogrammierung ist und alles so schnell wie möglich sein muss. Ich klassifiziere Spielprogrammierung als leistungsbewusstes Programmieren . Der Entwickler muss sich der Leistungsbeschränkungen bewusst sein und innerhalb dieser arbeiten.

Zweitens gibt es den Mythos, dass jedes einzelne Ding so schnell wie möglich ist, was ein Programm schnell laufen lässt. In Wirklichkeit macht das Erkennen und Optimieren von Engpässen ein Programm schnell, und das ist viel einfacher / billiger / schneller, wenn der Code einfach zu warten und zu ändern ist. Extrem optimierter Code ist schwer zu pflegen und zu modifizieren. Es folgt dem extrem optimierte Programme zu schreiben , um, müssen Sie extreme Code - Optimierung nutzen so oft wie nötig und so selten wie möglich.

Drittens besteht der Vorteil von Eigenschaften und Zugriffsmethoden darin, dass sie robust gegenüber Änderungen sind und somit die Pflege und Änderung von Code erleichtern. Dies ist etwas, was Sie zum Schreiben schneller Programme benötigen. Möchten Sie eine Ausnahme auslösen, wenn jemand Ihren Wert auf null setzen möchte? Verwenden Sie einen Setter. Möchten Sie eine Benachrichtigung senden, wenn sich der Wert ändert? Verwenden Sie einen Setter. Möchten Sie den neuen Wert protokollieren? Verwenden Sie einen Setter. Möchten Sie eine verzögerte Initialisierung durchführen? Verwenden Sie einen Getter.

Und viertens folge ich in diesem Fall nicht den Best Practices von Microsoft. Wenn ich eine Eigenschaft mit trivialen und öffentlichen Gettern und Setzern benötige , verwende ich einfach ein Feld und ändere es einfach in eine Eigenschaft, wenn es erforderlich ist. Ich brauche jedoch sehr selten eine solche triviale Eigenschaft - es ist weitaus üblicher, dass ich nach Randbedingungen suchen oder den Setter privat machen möchte.


2

Es ist sehr einfach für die .net-Laufzeit, einfache Eigenschaften zu integrieren, und häufig auch. Dieses Inlining wird jedoch normalerweise von Profilern deaktiviert. Daher ist es leicht, sich irreführen zu lassen und zu glauben, dass das Ändern einer öffentlichen Eigenschaft in ein öffentliches Feld die Software beschleunigt.

In Code, der von einem anderen Code aufgerufen wird, der beim erneuten Kompilieren des Codes nicht erneut kompiliert wird, gibt es gute Gründe für die Verwendung von Eigenschaften, da der Wechsel von einem Feld zu einer Eigenschaft eine bahnbrechende Änderung darstellt. Aber für die meisten Codes, die nicht in der Lage sind, einen Haltepunkt für get / set festzulegen, habe ich im Vergleich zu einem öffentlichen Feld noch nie Vorteile bei der Verwendung einer einfachen Eigenschaft gesehen.

Der Hauptgrund, warum ich in meinen 10 Jahren als professioneller C # -Programmierer Eigenschaften verwendet habe, bestand darin, andere Programmierer davon abzuhalten, mir mitzuteilen, dass ich den Kodierungsstandard nicht einhalte. Dies war ein guter Kompromiss, da es kaum einen guten Grund gab, eine Immobilie nicht zu nutzen.


2

Strukturen und nackte Felder erleichtern die Interoperabilität mit einigen nicht verwalteten APIs. Oft werden Sie feststellen, dass die Low-Level-API auf die Werte über Verweise zugreifen möchte, was der Leistung zuträglich ist (da wir eine unnötige Kopie vermeiden). Die Verwendung von Eigenschaften ist ein Hindernis dafür, und häufig werden von den Wrapperbibliotheken Kopien erstellt, um die Verwendung zu vereinfachen und manchmal um die Sicherheit zu gewährleisten.

Aus diesem Grund erzielen Sie häufig eine bessere Leistung mit Vektor- und Matrixtypen, die keine Eigenschaften, aber nackte Felder aufweisen.


Best Practices werden nicht im Vakuum erstellt. Trotz eines gewissen Frachtkults gibt es im Allgemeinen Best Practices aus gutem Grund.

In diesem Fall haben wir ein paar:

  • Mit einer Eigenschaft können Sie die Implementierung ändern, ohne den Client-Code zu ändern. (Auf einer Binär-Ebene ist es möglich, ein Feld in eine Eigenschaft zu ändern, ohne den Client-Code auf einer Quell-Ebene zu ändern. Es wird jedoch nach der Änderung zu etwas anderem kompiliert.) . Das bedeutet, dass durch die Verwendung einer Eigenschaft von Anfang an der Code, der auf Ihre verweist, nicht neu kompiliert werden muss, nur um zu ändern, was die Eigenschaft intern tut.

  • Wenn nicht alle möglichen Werte der Felder Ihres Typs gültige Zustände sind, möchten Sie sie nicht dem Client-Code aussetzen, der sie modifiziert. Wenn also einige Wertekombinationen ungültig sind, möchten Sie die Felder privat (oder intern) halten.

Ich habe Client-Code gesagt. Das bedeutet Code, der in Ihren aufruft. Wenn Sie keine Bibliothek erstellen (oder sogar eine Bibliothek erstellen, aber intern statt öffentlich verwenden), können Sie in der Regel mit dieser und einer guten Disziplin davonkommen. In dieser Situation sollten Sie am besten Eigenschaften verwenden, um zu verhindern, dass Sie sich in den Fuß schießen. Darüber hinaus ist es viel einfacher, über Code nachzudenken, wenn Sie alle Stellen sehen, an denen sich ein Feld in einer einzigen Datei ändern kann, anstatt sich darum kümmern zu müssen, ob es an einer anderen Stelle geändert wird oder nicht. In der Tat sind Eigenschaften auch gut, um Haltepunkte zu setzen, wenn Sie herausfinden, was schief gelaufen ist.


Ja, es ist wertvoll zu sehen, was in der Industrie getan wird. Haben Sie jedoch eine Motivation, sich gegen die Best Practices zu wenden? Oder widersetzen Sie sich nur den bewährten Methoden - machen Sie es schwieriger, über den Code nachzudenken - nur weil es jemand anderes getan hat? Übrigens: "Andere tun es", so beginnt ein Frachtkult.


Also ... Läuft dein Spiel langsam? Sie sollten lieber Zeit investieren, um den Engpass herauszufinden und diesen zu beheben, anstatt zu spekulieren, woran es liegen könnte. Sie können sicher sein, dass der Compiler viele Optimierungen vornimmt. Aus diesem Grund suchen Sie wahrscheinlich das falsche Problem.

Auf der anderen Seite sollten Sie sich zunächst überlegen, welche Algorithmen und Datenstrukturen verwendet werden sollen, anstatt sich um kleinere Details wie Felder oder Eigenschaften zu kümmern.


Verdienen Sie etwas, indem Sie gegen Best Practices verstoßen?

Nimmt Unity für die Details Ihres Falls (Unity und Mono für Android) Werte als Referenz? Wenn dies nicht der Fall ist, werden die Werte trotzdem kopiert, und es wird kein Leistungsgewinn erzielt.

Wenn dies der Fall ist, übergeben Sie diese Daten an eine API, die ref. Ist es sinnvoll, das Feld öffentlich zu machen, oder kann der Typ die API direkt aufrufen?

Ja, natürlich gibt es Optimierungen, die Sie mithilfe von Strukturen mit nackten Feldern vornehmen können. Beispielsweise greifen Sie mit Zeigern darauf zu Span<T> oder ähnlichem darauf zu. Sie haben auch einen kompakten Speicher, sodass sie einfach zu serialisieren sind, um über das Netzwerk gesendet oder dauerhaft gespeichert zu werden (und ja, das sind Kopien).

Haben Sie nun die richtigen Algorithmen und Strukturen ausgewählt, und wenn sich herausstellt, dass sie ein Engpass sind, entscheiden Sie, wie Sie das Problem am besten beheben können. Dies können Strukturen mit nackten Feldern sein oder nicht. Sie können sich darüber Gedanken machen, wenn es passiert. In der Zwischenzeit können Sie sich um wichtigere Dinge kümmern, z. B. ein gutes oder unterhaltsames Spiel zu spielen.


1

... Mein Eindruck ist, dass in der Spieleentwicklung, sofern Sie keinen Grund haben, etwas anderes zu tun, alles so schnell wie möglich sein sollte.

:

Ich bin mir bewusst, dass eine vorzeitige Optimierung schlecht ist, aber wie ich bereits sagte, macht man in der Spieleentwicklung etwas nicht langsam, wenn eine ähnliche Anstrengung es schnell machen könnte.

Sie haben Recht zu wissen, dass vorzeitige Optimierung schlecht ist, aber das ist nur die halbe Wahrheit (siehe das vollständige Zitat unten). Auch in der Spieleentwicklung nicht alles so schnell wie möglich sein.

Lass es zuerst funktionieren. Nur dann optimieren Sie die Bits, die es benötigen. Das heißt nicht, dass der erste Entwurf chaotisch oder unnötig ineffizient sein kann, aber dass die Optimierung in dieser Phase keine Priorität hat.

" Das eigentliche Problem ist, dass Programmierer viel zu viel Zeit damit verbracht haben, sich um die Effizienz an den falschen Orten und zu den falschen Zeiten zu sorgen . Eine vorzeitige Optimierung ist die Wurzel allen Übels (oder zumindest des größten Teils davon) in der Programmierung." Donald Knuth, 1974.


1

Für grundlegende Dinge wie diese wird der C # -Optimierer es trotzdem richtig machen. Wenn Sie sich Sorgen machen (und wenn Sie sich mehr als 1% Ihres Codes Sorgen machen, machen Sie es falsch ), wurde ein Attribut für Sie erstellt. Das Attribut [MethodImpl(MethodImplOptions.AggressiveInlining)]erhöht die Wahrscheinlichkeit, dass eine Methode eingefügt wird, erheblich (Eigenschafts-Getter und Setter sind Methoden).


0

Wenn Sie Elemente vom Typ "Struktur" als Eigenschaften anstatt als Felder anzeigen, erhalten Sie häufig eine schlechte Leistung und Semantik, insbesondere in Fällen, in denen Strukturen tatsächlich als Strukturen und nicht als "eigenartige Objekte" behandelt werden.

Eine Struktur in .NET ist eine Sammlung von Feldern, die mit einem Klebeband versehen sind und für einige Zwecke als Einheit behandelt werden können. Mit .NET Framework können Strukturen ähnlich wie Klassenobjekte verwendet werden. In manchen Fällen kann dies praktisch sein.

Ein Großteil der Ratschläge zu Strukturen basiert auf der Vorstellung, dass Programmierer Strukturen als Objekte und nicht als zusammengeklebte Feldsammlungen verwenden möchten. Andererseits gibt es viele Fälle, in denen es nützlich sein kann, etwas zu haben, das sich wie ein zusammengeklebter Feldstapel verhält. Wenn es das ist, was man braucht, wird der .NET-Ratschlag sowohl für Programmierer als auch für Compiler unnötige Arbeit bedeuten, was zu einer schlechteren Leistung und Semantik führt als die direkte Verwendung von Strukturen.

Die wichtigsten Prinzipien, die bei der Verwendung von Strukturen zu beachten sind, sind:

  1. Übergeben oder geben Sie keine Strukturen als Wert zurück oder verursachen Sie auf andere Weise, dass sie unnötig kopiert werden.

  2. Wenn Prinzip 1 eingehalten wird, funktionieren große Strukturen genauso gut wie kleine.

Wenn Alphablobeine Struktur mit 26 öffentlichen intFeldern mit dem Namen az und ein Eigenschafts-Getter, abder die Summe seiner aund bFelder zurückgibt , angegeben ist Alphablob[] arr; List<Alphablob> list;, muss der Code int foo = arr[0].ab + list[0].ab;Felder aund bvon lesen arr[0], muss aber alle 26 Felder von lesen, list[0]auch wenn dies der Fall ist ignoriere alle bis auf zwei. Wenn man eine generische listeähnliche Auflistung haben möchte, die mit einer Struktur wie dieser effizient arbeiten kann alphaBlob, sollte man den indizierten Getter durch eine Methode ersetzen:

delegate ActByRef<T1,T2>(ref T1 p1, ref T2 p2);
actOnItem<TParam>(int index, ActByRef<T, TParam> proc, ref TParam param);

das würde dann aufrufen proc(ref backingArray[index], ref param);. Bei einer solchen Sammlung, wenn man ersetzt sum = myCollection[0].ab;mit

int result;
myCollection.actOnItem<int>(0, 
  ref (ref alphaBlob item, ref int dest)=>dest = item,
  ref result);

Dies würde die Notwendigkeit vermeiden, Teile der zu kopieren alphaBlob, die nicht im Property Getter verwendet werden sollen. Da der übergebene Delegierte auf nichts anderes zugreift als auf seineref Parameter zugreift, kann der Compiler einen statischen Delegaten übergeben.

Leider definiert .NET Framework nicht die Art von Delegaten, die erforderlich sind, damit diese Funktion ordnungsgemäß funktioniert, und die Syntax führt letztendlich zu einem Durcheinander. Auf der anderen Seite kann durch diesen Ansatz vermieden werden, dass Strukturen unnötig kopiert werden, was es wiederum praktisch macht, Aktionen an Ort und Stelle für große Strukturen durchzuführen, die in Arrays gespeichert sind. Dies ist die effizienteste Art, auf Speicher zuzugreifen.


Hallo, hast du deine Antwort auf die falsche Seite gepostet?
Piojo

@pojo: Vielleicht hätte ich klarer machen sollen, dass der Zweck der Antwort darin besteht, eine Situation zu identifizieren, in der die Verwendung von Eigenschaften anstelle von Feldern schlecht ist, und zu ermitteln, wie die Leistung und die semantischen Vorteile von Feldern erzielt werden können, während der Zugriff immer noch gekapselt wird.
Supercat

@piojo: Macht meine Bearbeitung die Dinge klarer?
Supercat

msgstr "muss alle 26 Felder der Liste [0] lesen, obwohl alle bis auf zwei ignoriert werden." Was? Bist du sicher? Haben Sie die MSIL überprüft? C # übergibt Klassentypen fast immer als Referenz.
pjc50

@ pjc50: Das Lesen einer Eigenschaft ergibt ein Ergebnis nach Wert. Wenn sowohl der indizierte Getter für listals auch der Eigenschafts-Getter für abinliniert sind, kann ein JITter möglicherweise erkennen, dass nur Member aund Elemente bbenötigt werden, aber mir ist keine JITter-Version bekannt, die tatsächlich solche Aktionen ausführt.
Supercat
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.