== oder String.Equals



  • Hallo!

    Ich bin gerade dabei die Geschwindigkeit meiner XML Library TXML aufzubügeln. Da ich um einen bestimmten Knoten zu finden den gesamten XML String selbst parse bin ich natürlich an der schnellsten Vergleichsmöglichkeit bei Strings interessiert. Nun habe ich auf versch. Seiten gelesen, dass die Methode String.Equals() schneller sein soll als op_Equality(), die vom == Operator verwendet wird. Also habe ich nun alle meine String Vergleiche von == auf String.Equals umgeschrieben. Das komische hierbei ist, dass die Methode, in der ich den Xml String parse und in der alle Vergleiche stattfinden mit String.Equals() langsamer läuft als mit dem Operator ==. Kann sich das jemand erklären?
    Hier ist die Problemmethode:

    private ReaderReturnValue GetNodeContent(string masterNode, int mnIndex, string subNode, int snIndex)
    		{
    			int mi = 1;		// masternode iterator
    			int si = 1;		// subnode iterator
    			int mnLength = masterNode.Length;
    			int snLength = subNode.Length;
    			int textLength = 0;	// the length of the text that is to be returned
    			int i = 0;			// the for iterator
    			int snBegin = 0;			// the beginning pos of the subnode
    			bool foundMn = false;
    			bool foundSn = false;
    			bool done = false;	// indicates if we have found the nodes and the text length
    
    			if(masterNode == "")		// if there is no masterNode specified set foundMn to true
    				foundMn = true;
    
    			for(i = 0; i < XmlText.Length; i++)
    			{
    				if(XmlText[i] == '<')			// are we currently inside a node?
    				{	
    
    					if(foundMn == false)	// do we have to search for our masternode?
    					{
    						if(mnLength + 1 <= XmlText.Length - i - 1)			// only continue if we haven't reached the end yet - aka if there are enough chars left to read
    						{
    							if(XmlText.Substring(i + 1, mnLength + 1).Equals(masterNode + '>'))	// have we found our masternode?
    							{
    								if(mi == mnIndex)
    									foundMn = true;
    								else
    									mi++;
    							}
    						}
    					}
    
    					if(foundMn == true && foundSn == false)	// do we have to search for our subnode?
    					{
    						if(snLength + 1 <= XmlText.Length - i - 1)		// only continue if we haven't reached the end yet - aka if there are enough chars left to read
    						{
    							if(XmlText.Substring(i + 1, snLength + 1).Equals(subNode + '>'))			// have we found our subnode?
    							{
    								if(si == snIndex)
    								{
    									foundSn = true;
    									snBegin = i + snLength + 2;			// set the beginning of the subnode
    								}
    								else
    									si++;
    							}
    						}
    					}
    
    					if(mnLength + 1 <= XmlText.Length - i - 1)			// only continue if we haven't reached the end yet - aka if there are enough chars left to read
    					{
    						if(foundMn == true)					// have we already found our masternode?
    						{
    							if(XmlText.Substring(i + 1, mnLength + 2).Equals('/' + masterNode + '>'))	// have we reached the end of the masternode?
    								break;							// then we'll end this loop
    						}
    					}
    
    					if(foundSn == true)					// have we already found our subnode?
    					{
    						if(mnLength + 1 <= XmlText.Length - i - 1)			// only continue if we haven't reached the end yet - aka if there are enough chars left to read
    						{
    							if(XmlText.Substring(i + 1, snLength + 2).Equals('/' + subNode + '>'))	// have we found the end of our subnode?
    							{
    								done = true;
    								break;
    							}
    							else
    								textLength++;
    						}
    					}
    
    				}			// if(XmlText[i] == '<')
    				else 
    				{
    					if(foundSn == true)							// have we found all our nodes?
    						textLength++;
    				}
    
    			}				// for...
    
    			// create the new ReaderReturnValue
    			ReaderReturnValue returnValue = new ReaderReturnValue();
    
    			if(done == true)										// have we found everything we need?
    			{
    				// fill returnValue with the node content and set foundNode to true
    				returnValue.text = XmlText.Substring(snBegin, textLength - snLength - 2);
    				returnValue.foundNode = true;
    			}	
    			else
    				returnValue.foundNode = false;
    
    			// return returnValue
    			return returnValue;
    		}
    

    Zuvor waren alle .Equals() == Operatoren.
    In dem jetzigen Zustand braucht die Methode durchschnittl. 100 Millisekunden länger als zuvor. Wenn ich String.Concat() Aufrufe, die bei jedem Durchlauf der Schleife vorkommen mit einer bereits vorgefertigten Variable ersetzte (z.B.: string snClosing = '/' + subNode + '>') wird die gesamte Methode nochmal um 400 Millisekunden langsamer, obwohl sie eigentlich schneller werden sollte.



  • Ich hab mir deinen Code jetzt nicht angeschaut. Ich sag dir nur, dass du mit sowas nie einen bemerkenswerten Geschwindigkeitsgewinn machen wirst. Und hässlicher ist es mit Equals definitiv auch.
    Ohne deinen Code angeschaut zu haben, ist mir dennoch etwas extrem störendes aufgefallen: Der Vergleich auf "== true" oder "== false" ist extrem hässlich. Ob es aber schneller wird, wenn du einfach nur if( foundMn ) schreibst, weiß ich nicht. 😉



  • Stimmt, if(foundMn == true) sieht wirklich etwas hässlich aus. Geschwindigkeitsmässig macht es aber keinen Unterschied ob das "== true" da ist oder nicht.
    Was mich allerdings wundert ist, dass die Methode mehr Zeit benötigt wenn ich anstatt

    if(XmlText.Substring(i + 1, mnLength + 2).Equals('/' + masterNode + '>'))
    

    am Anbfang eine String Variable "mnClosing = '/' + masterNode + '>'" definiere und diese dann in der if-Abfrage verwende:

    if(XmlText.Substring(i + 1, mnLength + 2).Equals(mnClosing))    // das ist langsamer als Concat()
    

    Normalerweise sollte doch die zweite Ausführung die bessere sein, da der GC nicht bei jedem Durchgang einen neuen String zusammensetzen muss.



  • tommazzo schrieb:

    Das komische hierbei ist, dass die Methode, in der ich den Xml String parse und in der alle Vergleiche stattfinden mit String.Equals() langsamer läuft als mit dem Operator ==.

    MSDN schrieb:

    This operator is implemented using the Equals method, ...

    Link

    Insofern wirds irgendwie schwer, dass Equals wirklich langsamer sein sollte 🙄



  • Das ist sehr komisch. Laut versch. Internetseiten (hier ist mal eine soll String.Equals() schneller sein als der == Operator. Laut dieser Seite ruft der == Operator die Methode String.op_Equality() auf. Wenn man eine Assembly mit ildasm auseinandernimmt kann man das auch sehen:

    IL_00d8:  call       bool [mscorlib]System.String::op_Equality(string,
                                                                     string)
    

    Warum diese Methode letztendlich langsamer ist als String.Equals() verstehe ich nicht.



  • Das ist der Operator in IL Code.

    .method public hidebysig specialname static 
            bool  op_Equality(string a,
                              string b) cil managed
    {
      // Codegröße       8 (0x8)
      .maxstack  8
      IL_0000:  ldarg.0
      IL_0001:  ldarg.1
      IL_0002:  call       bool System.String::Equals(string,
                                                      string)
      IL_0007:  ret
    } // end of method String::op_Equality
    


  • Anscheinend ist String.Equals() doch schneller als op_Equality()!
    Eine Misinterpretation einer Profileraussage hat dazu geführt das es anfangs nicht so aussah, aber nach zahlreichen Tests bin ich mir sicher: String.Equals() ist schneller. 🙂
    Danke für eure Hilfe!



  • Dieser Operator ist unter Verwendung der Equals-Methode implementiert. Dies bedeutet, dass die Operanden auf eine Kombination von Verweis- und Wertgleichheit überprüft werden. Beim Vergleich wird die Groß- und Kleinschreibung berücksichtigt.



  • Jetzt macht er seinen ganzen Code hässlich, weil er sich manchmal das eine einbildet, manchmal das andere... obwohl beides das selbe macht. 🙄
    Junge, optimiere woanders.



  • Optimizer schrieb:

    Jetzt macht er seinen ganzen Code hässlich, weil er sich manchmal das eine einbildet, manchmal das andere... obwohl beides das selbe macht. 🙄
    Junge, optimiere woanders.

    Anscheinend macht beides zwar das selbe, eines macht es halt nur langsamer als das andere. Nach mehreren Geschwindigkeitstests habe ich nach den Optimierungen, die aber noch etwas weitläufiger waren als nur == mit Equals() zu ersetzen, eine Geschwindigkeitserhöhung von 20-25% feststellen können. Wenn du aber irgendwelche Vorschläge hast, wo ich noch etwas optimieren könnte, dann nur heraus damit.



  • tommazzo schrieb:

    Anscheinend macht beides zwar das selbe, eines macht es halt nur langsamer als das andere. Nach mehreren Geschwindigkeitstests habe ich nach den Optimierungen, die aber noch etwas weitläufiger waren als nur == mit Equals() zu ersetzen, eine Geschwindigkeitserhöhung von 20-25% feststellen können. Wenn du aber irgendwelche Vorschläge hast, wo ich noch etwas optimieren könnte, dann nur heraus damit.

    Leb ruhig weiter in deiner naiven Traumwelt, in der deine wahrscheinlich sehr fragwürdigen Messungen ergeben,
    dass absolut das gleich unterschiedlich schnell sein soll. Solche Ergebniss hängen von viel zu vielen Kriterien ab,
    als du generell sagen könntest dieses oder jenes ist schneller.

    Versuch lieber mal vernünftig strukturierten Code zu schreiben, bevor du zu optimieren beginnst.
    Ich würde an deiner Stelle zum Beispiel mal eher versuchen ein wenig Regex zu verwenden ...



  • ... schrieb:

    Versuch lieber mal vernünftig strukturierten Code zu schreiben, bevor du zu optimieren beginnst.

    1. Was ist an meinem Code so unstrukturiert?
    und
    2. Mir ist schon klar, dass es bei der Geschwindigkeit auf verschiedene Faktoren ankommt. Ich spreche hier auch nur über eine Testserie bei der ich bestimmte Knoten hintereinander ausgelesen habe - und bei der gab es eben eine kleine Geschwindigkeitszunhame. Ich weiss auch, dass diese kleine Zunahme bei den meisten Operationen egal ist, wenn aber viele Operationen hintereinander ausgeführt werden, kann sie schon ins Gewicht fallen.

    P.S.: Meine Hauptoptimierung in dem Codestück, das ich gepostet habe läuft mittlerweile (und darauf beziehen sich meine letzten Testergebnisse) nicht nur auf String.Equals(), sondern eher auf GC Optimierungen hinaus:

    string mnEnd = masterNode + '>';
    for(i = 0; i < XmlText.Length; i++) {
    				if(XmlText[i] == '<') {			
    
    					if(!foundMn) {	
    						if(mnLength + 1 <= XmlText.Length - i - 1) {			
    							if(XmlText.Substring(i + 1, mnLength + 1).Equals(mnEnd))    /* anstatt den String mnEnd = masterNode + '>' bei jedem Durchlauf neu
    zusammenzusetzen wird der String nur einmal am Anfang zusammengesetzt */
    


  • tommazzo schrieb:

    ... schrieb:

    Versuch lieber mal vernünftig strukturierten Code zu schreiben, bevor du zu optimieren beginnst.

    1. Was ist an meinem Code so unstrukturiert?

    Soll das ein schlechter Witz sein? Ich sehe hier eine fette Funktion, 200 Zeilen lang, 200 Spalten breit. Geschachtelte ifs und Schleifen ohne Ende.
    Du hast dir die Chance auf Optimierung schon total verbaut. Kein Schwein kann deinen Code noch optimieren. Dazu müsste er sich erstmal durch diesen Wulst quälen, ihn verstehen und den Überblick bewahren, um noch Schwachstellen in deinem Algorithmus zu finden. Das sieht man ja schon an deinen Kommentaren. Nach jeder Zeile steht ein Kommentar, anstatt dass die Zeile für sich selbst spricht. Aber das braucht man hier auch so, sonst behält man den Überblick nicht mehr.

    Selbst wenn Equals() schneller wäre, was es nicht ist, wäre der Geschwindigkeitszuwachs ein Witz im Vergleich zu dem, was man am Algorithmus vielleicht drehen könnte. Aber dieser fette, hässliche Code-Batzen, jetzt auch noch mit dem Equals(), ist so unübersichtlich, dass man hier keine Verbesserungen mehr finden kann.
    Optimierung fängt damit an, dass du den Code leserlich machst (oder von Anfang an so schreibst). Dass du viele kleine Funktionen hast, jede nur 10 Zeilen lang und 80 Spalten breit. Jede macht nur eine kleine Teilaufgabe vom Ganzen. Und auf einmal siehst du, dass du unnötigerweise diesen Teilschritt viel zu oft machst. Du könntest eine Schleife viel früher abbrechen. Oder den Wert merken. Oder einen völlig anderen Algorithmus finden. Das geht aber nur, wenn man die Übersicht hat. Vielleicht glaubst du, den Überblick zu haben, aber um was zu verbessern, braucht man nochmal nen kleinen Bonus. Das hilft ja auch beim denken. Du kannst nur den einen kleinen Teilschritt anschauen und überlegen, ob er wirklich nötig ist.
    Und du erhältst ein viel detaillierteres Profil vom Profiler.



  • Diese Riesenmethode auf mehrere kleine aufzuteilen ist schon eine gute Idee, nur frage ich mich ob es in diesem Fall viel bringt ohne gleich den ganzen Algorithmus (der ja eigentlich problemlos funktioniert) neu zuschreiben.



  • tommazzo schrieb:

    Diese Riesenmethode auf mehrere kleine aufzuteilen ist schon eine gute Idee, nur frage ich mich ob es in diesem Fall viel bringt ohne gleich den ganzen Algorithmus (der ja eigentlich problemlos funktioniert) neu zuschreiben.

    Ich hab zwar deine Funktion kaum angesehen aber bei mir klingelt da schon ein Wort: "parsen" in deinem ersten Beitrag. Das schreit nach einer ganzen Klasse und nicht nur nach einer Funktion. XML kann man schön rekursiv bearbeiten, sich ne kleine Gramatik zusammenstellen und die dann abfahren usw....



  • Online schrieb:

    Ich hab zwar deine Funktion kaum angesehen aber bei mir klingelt da schon ein Wort: "parsen" in deinem ersten Beitrag. Das schreit nach einer ganzen Klasse und nicht nur nach einer Funktion. XML kann man schön rekursiv bearbeiten, sich ne kleine Gramatik zusammenstellen und die dann abfahren usw....

    Eigentlich ist die Funktion Teil einer ganzen Reader-Klasse.

    Ich habe jetzt aber beschlossen den ganzen Algorithmus in den Müll zu werfen und ihn von Anfang an neu zuschreiben. Dazu habe ich das Knuth-Morris-Pratt Verfahren ins Auge gefasst. Damit sollte das Auffinden eines Knotens schneller laufen. 🙂



  • tommazzo schrieb:

    [...]
    Eigentlich ist die Funktion Teil einer ganzen Reader-Klasse.
    [...]

    Nichtdesto trotz kann man da noch eine Klasse rumbauen 😉



  • Hier ist meine neue statische Reader Klasse, in der es allerdings noch ein Problem gibt: Wenn ich einen Knoten auslese, bekomme ich oft nicht nur den Inhalt sondern, nach dem Anfang des Knotens den Rest der gesamten Datei zurückgeliefert.

    Vielleicht fällt jemandem etwas dazu ein, hier ist der Code:

    internal class NodeHandler
    	{
    		#region Knuth-Morris-Pratt algorithm
    
    		/// <summary>
    		/// generates a KMP search table for the specified search-string
    		/// </summary>
    		/// <param name="searchStr">the string for which the table should be generated</param>
    		private static int[] GetKMP_Table(ref string searchStr) {
    			int i, j;
    			int[] next = new int[searchStr.Length + 1];
    
    			next[i = 0] = j = -1;
    
    			do {
    				if(j < 0 || searchStr[i] == searchStr[j])
    					next[++i] = ++j;
    				else
    					j = next[j];
    			} while(i < searchStr.Length - 1);
    
    			return next;
    		}
    
    		/// <summary>
    		/// uses the Knuth-Morris-Pratt algorithm to find the next occurance of the search string
    		/// </summary>
    		/// <param name="xmlText">our xml text</param>
    		/// <param name="searchStr">the string we're looking for</param>
    		/// <param name="next">the KMP jump table for the search string</param>
    		/// <param name="startPos">the starting position</param>
    		/// <param name="endPos">the max search position</param>
    		/// <returns>the index of the first char of the found search string or -1 if the search string has not been found in xmlText</returns>
    		private static int GetNextPos(ref string xmlText, ref string searchStr, int[] next, int startPos, int endPos) {
    			int textLength = endPos - startPos + 1;
    			int j = 0;
    
    			// Knuth-Morris-Pratt algorithm
    			for(int i = startPos; i < textLength; i++) {
    
    				// endless loop executing as long as chars match
    				for(;;) {
    					if(xmlText[i] == searchStr[j]) {
    						j++;
    						if(j == searchStr.Length)
    							return i - searchStr.Length + 1;
    						break;
    					}
    					else {
    						if(j == 0)
    							break;
    						else
    							j = next[j];
    					}
    				}
    			}
    
    			return -1;
    		}
    
    		#endregion
    
    		#region FindNode
    
    		/// <summary>
    		/// finds the specified node in xmlText
    		/// </summary>
    		/// <param name="nBegin">out: the beginning of the found node (will be -1 if node was not found)</param>
    		/// <param name="nEnd">out: the end of the found node (will be endPos if node was not found)</param>
    		/// <param name="startPos">the starting position</param>
    		/// <param name="endPos">the max search position</param>
    		/// <param name="includeNodes">specified whether the nodes themselved will be included in nBeginning and nEnd</param>
    		private static void FindNode(out int nBegin, out int nEnd, ref string xmlText, ref string node, int index, int startPos, int endPos, bool includeNodes) {
    			string nodeBegin = '<' + node + '>';
    			int[] next;
    			int foundCount = 0;
    			nBegin = 0;
    			nEnd = 0;
    
    			// let's find nodeBegin
    			next = GetKMP_Table(ref nodeBegin);
    			while(foundCount < index) {
    				nBegin = GetNextPos(ref xmlText, ref nodeBegin, next, nBegin, endPos);
    				if(nBegin != -1)
    					foundCount++;
    				else {
    					nEnd = endPos;
    					return;
    				}
    				nBegin += nodeBegin.Length;
    			}
    
    			// let's find nodeEnd
    			string nodeEnd = "</" + node + '>';
    			next = GetKMP_Table(ref nodeEnd);
    
    			nEnd = GetNextPos(ref xmlText, ref nodeEnd, next, nBegin, endPos);
    			if(nEnd == -1) {
    				nEnd = endPos;
    				return;
    			}
    
    			// check if the nodes themselves have to be included in the results and add them if necessarry
    			if(includeNodes) {
    				nBegin -= nodeBegin.Length;
    				nEnd += nodeEnd.Length;
    			}
    		}
    
    		#endregion
    
    		internal static TXmlReader.ReaderReturnValue GetNodeContent(ref string xmlText, ref string masterNode, int mnIndex, ref string subNode, int snIndex) {
    			int mnBegin, mnEnd, snBegin, snEnd;
    			TXmlReader.ReaderReturnValue returnValue = new TXML.TXmlReader.ReaderReturnValue();
    
    			// let's find our masterNode
    			if(masterNode.Length > 0) {
    				FindNode(out mnBegin, out mnEnd, ref xmlText, ref masterNode, mnIndex, 0, xmlText.Length, false);
    				if(mnBegin == -1) {
    					returnValue.foundNode = false;
    					return returnValue;
    				}
    			}
    			else {
    				mnBegin = 0;
    				mnEnd = xmlText.Length;
    			}
    
    			// let's find our subNode
    			FindNode(out snBegin, out snEnd, ref xmlText, ref subNode, snIndex, mnBegin, mnEnd, false);
    			if(snBegin == -1) {
    				returnValue.foundNode = false;
    				return returnValue;
    			}
    
    			// get and return the substring
    			returnValue.foundNode = true;
    			returnValue.text = xmlText.Substring(snBegin, snEnd - snBegin);
    			return returnValue;
    		}
    	}
    

    P.S.: Um den Inhalt eines Knotens auszulesen wird die Methode TXmlReader.ReaderReturnValue GetNodeContent() (ganz unten) aufgerufen.
    ReaderReturnValue ist eine Klasse mit zwei Membern:
    bool foundNode gibt an ob der Knoten gefunden wurde
    string text gibt den Inhalt des gefundenen Knotens wieder



  • Hallo Meister...

    es bringt nichts, einen Referenztypen wie string per ref zu übergeben. by value wird die Referenz von 4 Byte kopiert byref wird die adresse von 4 byte kopiert und du zahlst zusätzlich für die Zugriffe, wenn du Pech hast. Du solltest langsam mit diesen eingebildeten Optimierungen (dazu gehört auch Equals()) aufhören und wirklich konkret über den Algorithmus nachdenken. Online hat dir ja auch schon Ansatzpunkte genannt.
    Hör bitte auf, deinen Code hässlich zu machen. Du behinderst dich nur selbst.



  • Wenn du mal einen Blick auf den obigen Code wirfst, dann wirst du sehen, dass das eine ganz neue Klasse ist, die kein Bisschen des alten Codes enthält. Ich habe sehr wohl über den Algorithmus nachgedacht und einen völlig neuen gefunden: ich verwende jetzt einen Algorithmus der Marke "Knuth-Morris-Pratt". Irgendwie stimmt aber etwas noch nicht in dem Code. Ich kann meistens nicht den Endteil eines Knotens (</meinKnoten>) finden.

    P.S.: Strings werden bei Methoden aber ohne das ref Keyword als Value-Types übergeben, obwohl sie eigentlich Reference-Types sind.


Anmelden zum Antworten