Du bist nicht angemeldet.

Stilllegung des Forums
Das Forum wurde am 05.06.2023 nach über 20 Jahren stillgelegt (weitere Informationen und ein kleiner Rückblick).
Registrierungen, Anmeldungen und Postings sind nicht mehr möglich. Öffentliche Inhalte sind weiterhin zugänglich.
Das Team von spieleprogrammierer.de bedankt sich bei der Community für die vielen schönen Jahre.
Wenn du eine deutschsprachige Spieleentwickler-Community suchst, schau doch mal im Discord und auf ZFX vorbei!

Werbeanzeige

21

26.01.2015, 12:18

Virtuelle Funktionen verursachen von sich aus keine nennenswert messbaren Verluste.

Naja mit viel Runtime Polymorphism macht sich das schon bemerkbar, weil die Methoden nicht ohne weiteres vom Compiler geinlined werden können.

Dazu muss ich vllt. noch etwas zum Context sagen: Ich arbeite prinzipiell auf einem Netbook (älteres Gerät mit single-core CPU). Dadurch ist "messbare Verluste" halt relativ.

fast jedem Videospiel und in fast jeder Programmiersprache.

Die Frage ist auch wo man es anwendet. Nicolas Fleury (Ubisoft) hatte in einem Vortrag auf der CppCon ("C++ in Huge AAA Games") von einem 90/10-Verhältnis gesprochen. Kurz: 90% der Zeit werden in 10% des Codes verbracht.
Wenn ich bei diesen 10% "kritischem Code" natürlich viele virtual method calls habe und die Aufrufreihenfolge nicht optimiere, brauche ich mich nicht wundern. Und das war afaik bei mir das Problem. Ich hatte das Component-Pattern sehr stark OO-lastig implementiert, ohne Cache etc. im Hinterkopf zu behalten.
Dadurch und inspiriert von Mike Actons Vortrag ("Data-Oriented Design and C++", ebenfalls CppCon) fahre ich jetzt eine Hybrid-Lösung: "Kritische Subsysteme" (d.h. Systeme, die über viele Objekte iterieren, wie Physik- oder Animationssystem) baue ich eher nach DOD, weniger Kritische Subsysteme (z.B. Input-Handling, Kampfberechnung) dann mit stärkerem OO-Einfluss.

LG

DeKugelschieber

Community-Fossil

Beiträge: 2 641

Wohnort: Rheda-Wiedenbrück

Beruf: Software-Entwickler

  • Private Nachricht senden

22

26.01.2015, 13:01

Ich dachte du arbeitest mit dem ECS, von daher war ich verwirrt. Warum speicherst du denn noch mal Eigenschaften wenn die Komponenten diese bereits abbilden können?
Daher auch mein Ansatz, Komponente X vorhanden Ja/Nein.

Der Punkt mit dem schlecht leserlich bleibt (imo).

23

26.01.2015, 13:11

Warum speicherst du denn noch mal Eigenschaften wenn die Komponenten diese bereits abbilden können?

Meine Komponenten umfassen bisher nicht einzelne Eigenschaften sondern zusammengesetzte Aspekte, z.B. habe ich eine Physics-Komponente und dafür ein Physics-System das Bewegung, Kollision und Focusing steuert. Aber sicherlich wäre es ein interessanter Ansatz das System nochmal aufzuspalten, d.h.

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct MoveData {
    float velocity;
};

struct CollisionData {
    Rect bounding_box;
};

struct FocusData {
    ObjectID focus;
};

struct PhysicsData {
    Vector pos, face_direction;
    // optionale Daten je nach Objekt-Typ (oder halt mit unique_ptr)
    MoveData* move;
    CollisionData* coll;
    FocusData* focus;
};


Der Punkt mit dem schlecht leserlich bleibt (imo).

Warum?

C-/C++-Quelltext

1
2
3
if (data.move != nullptr) {
    // interpoliere Bewegung
}

Ist doch relativ hübsch :)

Bleibt nur der Fall: Eigenschaften ohne konkrete Daten... Ich schau mal ob mir ein sinnvolles Beispiel einfällt :D

LG

DeKugelschieber

Community-Fossil

Beiträge: 2 641

Wohnort: Rheda-Wiedenbrück

Beruf: Software-Entwickler

  • Private Nachricht senden

24

26.01.2015, 16:54

Wir reden aneinander vorbei.
Warum du sowas hier haben willst:

C-/C++-Quelltext

1
if ((chest & types::Collideable) && !(chest & types::Lighted)) {


War die Frage. Weil das stellst du mit Hilfe der Komponenten über das != nullptr doch schon dar, oder nicht?
Eigenschaften ohne konkrete Daten machen irgendwie keinen Sinn. Und sollte sie es doch, was spricht gegen eine leere Komponente die du dann nullptr setzen kannst?

25

26.01.2015, 17:13

Wir reden aneinander vorbei.

Ups xD

Weil das stellst du mit Hilfe der Komponenten über das != nullptr doch schon dar, oder nicht?

Im Moment sitzen die z.B. bewegungsspezifischen Daten (für Movable) mit in meiner Physics-Komponente, d.h. nein :D Mein System ist ziemlich einfach, so dass ich kaum extra Daten brauche. Im Endeffekt habe ich noch nichtmal Kollisionsdaten außer einem Collideable-Fag; meine Objekte bewegen sich diskret zwischen zwei Kacheln und die Kollision spielt sich pro-Kachel ab, d.h. nur die Position und das Collideable-Flag sind für die Kollision relevant. Ähnlich sieht es mit Bewegungen aus: Ausgangsposition + Blickrichtung sind allgemeine Daten (werden z.B. beim Rendern auch abgefragt) und ergeben für die Bewegung eine Zielposition. (Genauer: Die Position der Ziel-Kachel wird gespeichert --> wäre das einzige Element der MoveData Komponente; dann wird interpoliert bis das Ziel erreicht ist)

Von daher hätte ich viele leere Komponenten .. aber in einem anderen Subsystem wäre das sicher eine tolle Sache; z.B. wenn es um Skills, Inventar etc. geht .. hat ein Objekt keine Inventar-Komponente, hat es kein Inventar. Das werde ich mir auf jeden Fall so vornehmen wenn ich dann dort bin!

Und sollte sie es doch, was spricht gegen eine leere Komponente die du dann nullptr setzen kannst?

Hmm was spricht dagegen ... ich würde dann mehrere leere Strukturen haben sowie Zeiger auf diese. Und im Endeffekt sind die genauso gut wie booleans, weil ja nicht mehr Information drinne steckt (nur mehr Bits :D ) .. und wenn ich daran denke mehrere Booleans in einer Struktur zu haben, komme ich gedanklich wieder zu den Bitmasks, so dass sich der Kreis schließt :D

Bin ich ein hoffnungsloser Fall? :thumbsup:

DeKugelschieber

Community-Fossil

Beiträge: 2 641

Wohnort: Rheda-Wiedenbrück

Beruf: Software-Entwickler

  • Private Nachricht senden

26

26.01.2015, 17:20

Nö ich hatte dein Problem nur falsch verstanden ^^
Ich hatte da noch Bezug zum ECS Thread. Jetzt passt es, weitermachen :P

27

26.01.2015, 17:49

Nö ich hatte dein Problem nur falsch verstanden ^^
Ich hatte da noch Bezug zum ECS Thread. Jetzt passt es, weitermachen :P

Oki :thumbup:

@All: Ich denke ich habe meinen "Weg" nun gefunden: Für meine Physik- und Grafik-Systeme werde ich Bitmasken verwenden - die Daten unterscheiden sich nicht stark genug als dass ich einen Vorteil sehe hier mit Zeigern auf Komponenten zu arbeiten. Für mein "eigentliches" Spiel (Physik und Grafik sind ja nur die Rahmen-Systeme derer sich die anderen indirekt bedienen) werde ich den Ansatz aber verfolgen.

Danke für all eure Mühe :!:

LG Glocke :)

Beiträge: 1 223

Wohnort: Deutschland Bayern

Beruf: Schüler

  • Private Nachricht senden

28

26.01.2015, 17:58

Also ich finde, am Ende läuft alles, bis auf den Speicherverbrauch und die Performance, auf das selbe heraus. Das ein eigener Typ wegen "möglicherweise nicht vorhandenen Inlining" schneller sein könnte, ist quatsch und trifft genaus auch auf die Strings zu. Wenn du Code selber schreibst, kannst du selbst beeinflussen wie Inlining arbeitet.
Wenn du Datenorientierung verwendest, würde ich dir auf jeden Fall die initiale Lösung oder die Variante mit "union" empfehlen, wobei Zweiteres nur zur Datenformatierung bei Debuggingzwecken da wäre. Meiner Ansicht nach passt dieser Ansatz passend zur Denkweise bei Datenorientierung.

Zitat von »Evrey«

Da hast du dann Mist gemessen. Virtuelle Funktionen verursachen von sich aus keine nennenswert messbaren Verluste. Verluste erreichst du durch grottenschlechte Aufteilung der Objekte im Hauptspeicher. Sie werden immer und überall genutzt, von Compilern und Betriebssystemen, über OpenAL Soft, bis rauf zu fast jedem Videospiel und in fast jeder Programmiersprache. Im zuvor verlinkten Buch werden auch Strategien genannt, Cache-Misses stark zu reduzieren.

Das ist aber ziemlicher Quatsch. Ich würde dir dringend raten, selbst mal zu messen. Mach eine einfache Operation in einer virtuellen Funktion(bzw. Funktionszeiger) und eine direkt aufgerufen. Der Unterschied kann enorm sein. Woher diese Unterschiede kommen, hat eine ganze Reihe von Gründen. Offensichtlich können derartige Funktionen nicht mehr geinlined werden, was bereits zu einer Bremse führt. Zum einen direkt, durch die zusätzlichen auszuführenden Befehle, aber auch indirekt durch geringeres Optimierungspotential. Viele Dinge, bereits angefangen bei der Registerallokation und Alias Analysis(aber auch andere Optimierungen), leiden deutlich unter Aufrufen zu einer "beliebigen Stelle"(beliebig, aus Sicht von CPU/Compiler) im Programm.
Auf der anderen Seite gibt es auch noch direkte Folgen von indirekten Funktionsaufrufen: Die CPU kann den weiteren Verlauf kaum vorhersagen und leert die Pipeline was zu einer direkten Verzögerung führt. Und ganz besonders die v-Tables der virtuals sind auch noch schlecht für die Cache Locality: Eine zusätzlicher Zeiger an eine beliebige Stelle im Speicher. Das muss durch den Cache wandern und verdrängt andere Dinge. Außerdem gibt es die Möglichkeit eines Cachemisses zur v-table oder zur ersten Instruktion.
Eine Menge Dinge die schief gehen. Und das ist in performancekritischen Code leicht signifikant werden.

Virtuelle Aufrufe sollten nicht in Code stecken, der schnell laufen soll. Entweder spielt die Geschwindigkeit von Code an der Stelle keine große Rolle, oder der Code ist uralt.

"dynamic_cast" ist ebenfalls ein bekannter Performancefresser. Da ich allerdings nicht weiß wie das überhaupt exakt implementiert ist, kann ich wenig dazu sagen.
Mehrfachvererbung ist prinzipiell gar nicht so schlimm. Schlimmer wird es allerdings, wenn man virtuelle (Mehrfach)vererbung einsetzt.

Dieser Beitrag wurde bereits 3 mal editiert, zuletzt von »Spiele Programmierer« (26.01.2015, 18:40)


Evrey

Treue Seele

Beiträge: 245

Beruf: Weltherrscher

  • Private Nachricht senden

29

26.01.2015, 22:04

Zitat

Mach eine einfache Operation in einer virtuellen Funktion
Für etwas Stupides wie einen Getter braucht man keine Virtuals, da schießt man sich selbstverständlich ins Knie. Bei großen Funktionen fällt es weniger ins Gewicht, da dort Inlining eh' fast immer flach fällt. Die Pipeline leert sich auch, wenn die BPU einen bedingten Sprung falsch vorhersagt, was in größeren Funktionen eh' fast unvermeidlich ist. Das einzige Problem ist der potenzielle Cache-Miss, wenn man die V-Table oder die entsprechenden Funktionen lädt. Die halten sich in Schleifen allerdings ziemlich gut in Grenzen, wenn Instanzen der selben Klassen häufig auftreten, und können weiter reduziert werden, wenn der Compiler das Code-Segment clever anordnet.

Ja, Virtuals haben Einbuße, aber wie stark die ausfallen, hängt sehr stark vom Design ab, und es gibt viel mehr Stellen, an denen Cache-Misses auftreten und die Leistung herunterbrechen können, die nichts mit Virtuals zu tun haben. Zum Beispiel, wie die Instanzen im Heap verstreut sind, deren V-Tables man laden will.

dynamic_cast wird sich wahrscheinlich irgendwo aus der V-Table oder so einen Offset heraus kramen, der dann irgendwie zum Objekt-Pointer addiert wird. Keine Ahnung, bisher nie genutzt.

C-/C++-Quelltext

1
2
3
4
int main(int _argc, char** _argv) noexcept {
  asm volatile("lock cmpxchg8b %eax");
  return 0;
} // ::main
(Dieses kleine Biest vermochte einst x86-Prozessoren lahm zu legen.)

=> Und er blogt unter Hackish.Codes D:

Werbeanzeige