Jump to content
Unity Insider Forum

Effizienz von Linq queries


Kokujou

Recommended Posts

Hallo ihr lieben. Mir geht seit langem eine Frage im Kopf herum.

Wie wir wissen geben Linq queries ja IEnumerable zurück. Die sind ja ohne einen call nach ToList (u.A.) nutzlos.
Wenn ich nun zwei Linq queries auf demselben Enumerable laufen lasse, werden die dann sozusagen "kombiniert" falls möglich?

Ein Beispiel - eine select where query.

objectCollection.Select(x=>x.property).Where(x=>x=="someValue");

Wenn die daraus so etwas machen würden, wären Linq queries vermutlich ziemlich ineffizient:

var selectList = new List<T>();
foreach(var item in objectcollection)
{
	selectList.Add(item.property);  
}
  
var whereList = new List<T>();
foreach(var item in list)
{
	  if(item == "someValue")
  		whereList.Add(item);
}

aber wenn die queries einzeln evaluiert werden würde das natürlich dabei rauskommen. effizienter wäre natürlich

var results = new List<T>();
foreach(var item in objectCollection)
{
	if(item.property == "someValue") results.Add(item.property);
}  

Also was passiert da eigentlich genau im Hintergrund? Und sollte man auf Linq queries verzichten? Oder sind sie im Gegenteil sogar durch irgendwelche algorithmischen Super-Tricks viel effizienter als normale Enumerationen?

Ich kenne z.B. auch Leute, die gesagt haben dass Linq queries bei großen Sammlungen zu wirklich massiven Performance-Einbrüchen geführt haben.

Link zu diesem Kommentar
Auf anderen Seiten teilen

vor 4 Stunden schrieb Kokujou:

Die sind ja ohne einen call nach ToList (u.A.) nutzlos.

Dasja Quark. Du kannst da rüberiterieren, z.B. mit einer foreach-Schleife.

vor 4 Stunden schrieb Kokujou:

Und sollte man auf Linq queries verzichten?

Du solltest auf gar nix verzichten, wenn du kein Problem erkennst. Linq kann (muss aber nicht) die Lesbarkeit deines Codes drastisch erhöhen. Das kann mehr Wert sein als kleine Performance-Einbußen, weil du so Bugs leichter verhindern oder fixen kannst. Dein Code sollte immer so sein, dass du Profrmance-Probleme beheben kannst, ohne alles neu machen zu müssen. Wenn du also irgendwo ein Linq-Statement hast, und tatsächlich bemerkst, dass es die Performance spürbar (!) beeinflusst, dann solltest du das Statement einfach durch eine Schleife ersetzen können und sonst nichts mehr machen müssen, damit der Code wieder genau dasselbe tut wie vorher. Ich hab das auch irgendwo mal gehört, dass Linq manchmal langsamer sein kann als eine "handgemachte" Schleife. Aber wie gesagt... das ist erst ein Problem, wenn es ein Problem ist.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Manche Linq-Queries haben tatsächlich Memoryproblems, aber solange man das nicht konstant also nonstop aufruft ist es eigentlich kein Problem.

Irgendwo in Unity Forum oder Blog hab ich auch gelesen, dass man es z.B. nicht in einem Update-Funktion benutzen soll.

Diese Probleme gibt es aber nicht nur in Linq. Foreach, strings (anfügen in Updates) usw, dass alles kann, aber muss nicht zu Performance Probleme führen

vor 5 Stunden schrieb Kokujou:

Also was passiert da eigentlich genau im Hintergrund?

Bei foreach in deinem Beispiel passiert ungefähr folgendes:

using (IrgendeinType.Enumerator enumerator = this.objectCollection.GetEnumerator())
{
    while (enumerator.MoveNext())
    {
        IrgendeinType s = (IrgendeinType)enumerator.Current;

        if(s.property == "someValue") results.Add(s.property);
    }
}

In der While-Schleife wird ein neues Objekt (Class) jedes mal erzeugt während man bei ForLoop eigentlich nur auf ein Element zugreift. Allerdings muss das nicht immer der Fall sein. Manchmal soll es auch ein Struct sein. Ich weiß nicht mehr was der Grund war. Irgendwie solle es bei Unity anders sein, als wenn man nur pure C# benutzt.

Ich hatte z.B. mit Linq Probleme, als ich erste Mal mein Netzwerk System programmiert habe und in Unity angewendet habe. Beim nächste mal hab ich es weggelassen und hatte danach keine Memory Probleme mehr. Ich konnte statt 80 Network Objects auf über 2k bringen, was auf jeden Fall reicht.

Link zu diesem Kommentar
Auf anderen Seiten teilen

das ist schade...

ich hab mir nämlich unfertig vorgestellt, dass Funktionen mit Rückgabewert IEnumerable doch yield benutzen.

Anders gesagt du kannst das erste Element eines Array bereits abrufen bevor das gesamte Array konstruiert ist. Und wenn man das durchpipet könnte das doch automatisiert vielleicht so aussehen:

SELECT x.property -> return first Element -> fits where clause? return, else wait for next element

dann würde die Enumeration quasi-parallel abgearbeitet und das würde etwa meinem zweiten Beispiel entsprechen. Oder hab ich da einen Denkfehler?

Link zu diesem Kommentar
Auf anderen Seiten teilen

yield benutzen sollte wunderbar gehen. Aber um Parallelisierung musst du dich da selber kümmern, und du darfst nicht vergessen, dass das in Unity entweder nur mit Jobs geht, oder immer nur wenn du nichts mit Unity-Objekten machst, das geht nämlich nur im Main Thread. Oder meinst du keine richtige Parallelisierung?

Link zu diesem Kommentar
Auf anderen Seiten teilen

Ich meinte keine richtige parallelisierung. yield ist ja auch nicht parallel, sondern nur quasi-parallel. Die Anweisung wird unterbrochen und später Fortgesetzt.

Also vielleicht nochmal etwas deutlicher am Beispiel Select-Where

Select x.Property -> 1. Item objectCollection[0].property -> weitergeben an Where-Filter -> erfüllt? zurückgeben, sonst warten -> Select 2. Item objectcollection[1].property -> ...

ich hatte mir eigentlich insgeheim gehofft dass Linq sowas drauf hat dass es alle gleichzeitig ausführbaren queries auch gleichzeitig ausführt. Select und Where ist ja da sehr infach zu komprimieren. Und im Prinzip ist es überall dasselbe. Darum hätte mich sehr interessiert was da tatsächlich abläuft.

 

Link zu diesem Kommentar
Auf anderen Seiten teilen

Hallo

 

Wie bereits geschrieben wurde, würde ich mir um Optimierungen wirklich absolut null Gedanken machen. Bring das zum Laufen, was du willst. Dann schaust du, ob es vernünftig läuft. Wenn das nicht der Fall sein sollte, schmeißt du den Profiler an und schaust, wo der Schuh drückt. Habe gerade erst gelesen, dass man Linq eigentlich nicht nutzen sollte, aber ob das bei dir kritisch ist, weißt du eigentlich erst am Ende. Immer schön ein Problem nach dem nächsten. Ich habe immer schon Schwierigkeiten, die Dinge umzusetzen, die ich mir vorgenommen habe. Wie es läuft, schaue ich mir am Ende an. Wenn ich ehrlich bin, musste ich aus Performancegründen noch nie ernsthaft was anpassen. 

 

Christoph 

Link zu diesem Kommentar
Auf anderen Seiten teilen

naja es ist schon wichtig besonders wenns um umfassende KI und Dinge geht die auf regelmäßiger Basis z.B. in der Update-Routine durchgeführt werden.

 

vor 11 Stunden schrieb MaZy:

Meinst du sowas wie ne Queue? Dass da etwas gemacht wird und wenn abgeschlossen ist das Nächste? Sowas hab ich mit Coroutines auch gemacht.

Ja sowas in etwa :) ich dachte mir damit könnte man doch quasi sämtliche LINQ ausdrücke viel schneller machen. Wenn durch das yielden die Anweisung sowieso nach dem ersten Item unterbrochen wird kann man auch gleich mit der zweiten Query weitermachen. dachte ich mir zumindest. Die Frage ist nur ob das wirklich so funktioniert wie ich mir das vorstelle oder ob es da Restriktionen gibt.

Ich denke mir nämlich so... wenn es wirklich auf diese Art möglich und sogar praktisch ist, dann hätten die Leute von MIcrosoft das doch längst so gemacht und nicht so ein heftiges Performance Leck gebaut.

Oder dann könnte dohc zumindest mal jemand ein Nuget Package gepublisht haben... ein massives performance-Improvement von LINQ queries? da würden die Leute vor Freude Purzelbäume schlagen.

Link zu diesem Kommentar
Auf anderen Seiten teilen

vor 29 Minuten schrieb Kokujou:

naja es ist schon wichtig besonders wenns um umfassende KI und Dinge geht die auf regelmäßiger Basis z.B. in der Update-Routine durchgeführt werden.

Der entscheidende Punkt ist robuster Code. Wenn dein Code kein Spaghettimonster ist, dann kannst du eventuelle Performanceprobleme durch einzelne Codestellen einfach fixen. Was @chrische5 und ich meinen ist, dass du dir deshalb um die Performance keine Sorgen machen sollst, bevor sie negativ auffällt.

Link zu diesem Kommentar
Auf anderen Seiten teilen

das seh ich ja irgendwo ein aber man muss doch nicht absichtlich potenziell unperformanten Code schreiben.

Beispiel: Früher hab ich eine Schach KI geschrieben die darauf aufbaut dass sie große Mengen an Zuständen generiert und diese durchsucht und bewertet -> viel LINQ. Dasselbe bei meinem Hanafuda spiel. Wenn Linq für Performance-Einbrüche sorgt ist das schon relevant beim parallelen durchsuchen von tausenden Zustandsbäumen ;) 

Aber gut zurück zum Thema:
kennt denn irgendjemand rein zufällig irgendeine externe Bibliothek, Nuget Package oder sonstewas dass quasi eine performante erweiterung von LINQ anbietet?

Link zu diesem Kommentar
Auf anderen Seiten teilen

vor 50 Minuten schrieb Kokujou:

das seh ich ja irgendwo ein aber man muss doch nicht absichtlich potenziell unperformanten Code schreiben.

Wenn er besser lesbar, robuster, wartbar oder sonst irgendwie qualitativ besser ist und halt gar nicht feststeht, ob er Performanceprobleme verursacht: Dann doch!

vor 51 Minuten schrieb Kokujou:

kennt denn irgendjemand rein zufällig irgendeine externe Bibliothek, Nuget Package oder sonstewas dass quasi eine performante erweiterung von LINQ anbietet?

Du darfst davon ausgehen, dass die .NET-Entwickler nicht einfach nur zu faul waren, Linq performant zu machen. Einfach "Fast Linq" wirst du eher kaum finden.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Hallo

 

Auch wenn ich sicher nicht annähernd die Erfahrund von @Sascha habe, so denke ich eben auch, dass code zuallererst lesbar, wartbar und modular sein sollte. Ob das performance kostet, spielt ja nur dann eine rolle, wenn diese auch vermisst wird. Es muss bei weitem nicht alles auf Schnelligkeit optimiert werden. 

 

Christoph

Link zu diesem Kommentar
Auf anderen Seiten teilen

Ich stimme zu, dass code immer lesbar, wartbar und modular sein sollte.
Aber ich finde auch das sollte ganz sicher nicht auf Kosten von Performance geschehen. Denn am Ende geht es beim Programmieren um den Nutzer.

Und wenn ich als Nutzer vom Entwickler höre "Sorry für das Delay, aber wir wollen schönen Code haben" dann denke ich mir als Nutzer der keine Ahnung von dem ganzen Zeug hat und haben will "Danke, ich wechsel den Anbieter"

 

Link zu diesem Kommentar
Auf anderen Seiten teilen

Hallo

 

Na klar, aber das ist doch das Ende des Prozesses. Wenn du merkst, dass etwas nicht so schnell läuft, wie du es willst, musst du natürlich justieren. Wenn dein 2D spiel aber mit 230 FPS läuft und du nach der Optimierung 270 FPS hast, scheint mir das sinnlos. 

Also zuerst sauber arbeiten und dann schauen, was angepasst werden. Lesbarer Code sollte natürlich nicht dazu führen, dass das Programm unbenutzbar ist. Das will doch niemand.

 

Christoph

Link zu diesem Kommentar
Auf anderen Seiten teilen

vor 8 Stunden schrieb Kokujou:

Ja sowas in etwa :) ich dachte mir damit könnte man doch quasi sämtliche LINQ ausdrücke viel schneller machen. Wenn durch das yielden die Anweisung sowieso nach dem ersten Item unterbrochen wird kann man auch gleich mit der zweiten Query weitermachen.

Verstehe nicht warum du dazu unbedingt LINQ verwenden willst. Ich hab z.B. zwei verschiedene Klassen erstellt, die im Prinzip das machen was du willst (oder ich hab es noch nicht ganz verstanden). Da ist kein LINQ dabei.

CoroutineAction.Action(mono, 0f, StartAction, FinishedAction, 1f);

void StartAction() => Debug.Log("Action invoked");

void FinishedAction()
{
	Debug.Log("Finished, call another");
	CoroutineAction.Action(mono, 0f, () => Debug.Log("Action 2 invoked"));
}


Die Klasse dazu. Ist nicht ganz sauber, aber so geht es auch.

// Klasse;
	public static class CoroutineAction
	{
		public static void Action(MonoBehaviour mono, float invokeDelaySeconds, System.Action action, System.Action onFinished = null, float invokeFinishedDelaySeconds = 0)
		{
			mono.StartCoroutine(Start(invokeDelaySeconds, action, onFinished, invokeFinishedDelaySeconds));
		}

		static IEnumerator Start(float invokeDelaySeconds, System.Action action, System.Action onFinished = null, float invokeFinishedDelaySeconds = 0)
		{
			if (invokeDelaySeconds > 0)
				yield return new WaitForSeconds(invokeDelaySeconds);

			action.Invoke();

			if(invokeFinishedDelaySeconds > 0)
				yield return new WaitForSeconds(invokeFinishedDelaySeconds);

			onFinished?.Invoke();
		}
	}

 

Meinst du sowas?

Hab auch eine extremere Version, was noch nicht ganz fertig ist, aber modular. Das heißt, wer das verwendet, kann später eigene Funktionen integrieren ohne das Core Script anzufassen. Beispiel, wenn man "Move" Funktion braucht um ein Transform zu bewegen, dann kann man das integrieren. Hier dazu Video (hab Move sogar kurz vorm Video eingebaut):

https://streamable.com/2o3hbs

Link zu diesem Kommentar
Auf anderen Seiten teilen

nein das ist nicht was ich wollte...

mir geht es um den Rückgabewert. Ich möchte Linq funktionen effizienter gestalten indem ich sie quasiparallel auswerten lasse. Der Sinn des yield-patterns ist doch, dass man die Ausführung der Funktion unterbricht und mit dem weiteren Programm fortfährt.

Linq scheint es einfach stupide so zu machen, dass die Erweiterungsmethoden nacheinander ausgeführt werden. Heißt: Jedes mal eine Neue Liste und jedes Mal die Alte durchiterieren. Das ist sehr ineffizient.

Ideal wäre es aber, wenn du Linq-Query 2 shcon starten könntest während die Sammlung aus Linq Query 1 noch im Aufbau ist. So könnte man quasi unendlich viele queries in einer einzigen Iteration abfertigen lassen. Das würde die Effizient von Linq queries enorm steigern (falls das überhaupt möglich ist).

Ich denke immer noch das klingt alles zu schön um wahr zu sein und dass ich irgendwo einen denkfehler habe.

Link zu diesem Kommentar
Auf anderen Seiten teilen

vor 1 Stunde schrieb chrische5:

Hallo

 

Na klar, aber das ist doch das Ende des Prozesses. Wenn du merkst, dass etwas nicht so schnell läuft, wie du es willst, musst du natürlich justieren. Wenn dein 2D spiel aber mit 230 FPS läuft und du nach der Optimierung 270 FPS hast, scheint mir das sinnlos. 

Also zuerst sauber arbeiten und dann schauen, was angepasst werden. Lesbarer Code sollte natürlich nicht dazu führen, dass das Programm unbenutzbar ist. Das will doch niemand.

 

Christoph

wenn es um kleinigkeiten geht stimme ich dir ja auch zu. Aber wenn du aus Lust und Laune irgendwelche ineffizienten Codeblöcke aneinander papst und meinst "Hauptsache sie sind gut lesbar." dann kannst du dich später stundenlang mit irgnedwelchen Laufzeitanalysen befassen. Ich mache mir gerne gleich um die Performance gedanken und setze es um.
Grund zum Nachdenken gibt es für mich nur wenn A) die bessere Performance die Lesbarkeit massiv beeindruckt (was man eigentlich auch durch gutes Coding in den Griff kriegen kann), oder B. die lesbarere Schreibweise die Performance beeinträchtigen würde. Dann muss man sich die Frage stellen "ist es mir das Wert."

 

Link zu diesem Kommentar
Auf anderen Seiten teilen

Archiviert

Dieses Thema ist jetzt archiviert und für weitere Antworten gesperrt.

×
×
  • Neu erstellen...