System.Xml i klasa NameTable

Chyba każdy, kto choć trochę bawił się Xmlem na platformie .NET zauważył, że konstruktory niektórych klas z przestrzeni nazw System.Xml przyjmują tajemniczy parametr typu NameTable. Oczywiście w większości przypadków można znakomicie obejść się bez obiektów typu NameTable i wszystko będzie nadal działać poprawnie ale warto wiedzieć co się traci NIE korzystając z tych obiektów.

Intuicja programisty podpowiada, że jeśli jest "coś" co działa ale można temu "czemuś" przekazać dodatkowe informacje, które nie zmieniają sposobu działania tego "czegoś" od strony funkcjonalnej to można spodziewać się dodatkowych korzyści w wydajności. Inaczej jaki byłby sens przekazywania dodatkowych informacji? I rzeczywiście - table nazw (ang. nametables) mają na celu zwiększenie wydajności.

Zanim jednak przejdziemy do tabeli nazw (ang. nametable) przyjrzyjmy się gdzie tak naprawdę leży problem - czyli zobaczmy jak .NET Framework porównuje łańcuchy zankowe. Otóż, najpierw porównywane są referencje obiektów przechowujących łańcuchy znakowe - jeśli referencje są takie same to wiadomo, że łańcuchy też będą takie same bo to ten sam obiekt. Jeśli referencje różnią się to porównywane są długości łańcuchów - jeśli długości są różne to wiadomo, że łańcuchy będą różne. Ostatecznie jeśli powyższe warunki nie są spełnione (czyli referencje różnią się, a długości łańcuchów są takie same) następuje porównanie łańcuchów znak po znaku dopóki znaki na tych samych pozycjach nie różnią się lub zostanie osiągnięty koniec łańcucha. Oczywiście taki algorytm najwięcej porównań wykonuje dla dwóch instancji tego samego łańcucha znakowego (referencje się różnią, długości są takie same i trzeba "przejść" przez wszystkie znaki). Z powyższego opisu widać, że porównywanie łańcuchów znakowych może być powolne i może zmniejszyć wydajność przetwarzania dokumentów Xmlowych gdzie porówynwanie stringów (chociażby nazw elementów czy atrybutów) jest przecież na porządku dziennym.

Z pomocą przychodzą właśnie tabele nazw. Dzięki nim porównywanie dwóch łańcuchów znakowych sprowadza się tylko do porównywania referencji. Szybciej już chyba nie można. Jak to działa? Najpierw trzeba dodać łańcuch do tabeli wywołując odpowiednią metodę. Metoda ta sprawdza czy w tabeli jest już łańcuch, który chcemy dodać - jeśli tak zwraca referencję do istniejącego łańcucha. Jeśli łańcucha nie ma jeszcze w tabeli to łańcuch jest dodawany a  każde następne wywołanie próbujące dodać taki sam łańcuch znakowy zwróci referencję do nowo właśnie dodanego łańcucha. Jeśli wzorzec ten będzie w programie stosowany konsekwentnie (a tak się dzieje w przypadku API z przestrzeni nazw System.Xml), wszystkie każdemu unikatowemu łańcuchowi będzie odpowiadała zawsze tylko jedna referencja. W rezultacie porównywanie łańcuchów znakowych będzie rówoważne z porównywaniem referencji. Jak to wygląda w praktyce? Poniżej znajdują się dwa fragmenty kodu - pierwszy korzysta z tabeli nazw drugi nie korzysta.

1) Do XmlReadera przekazywana jest tabela nazw

NameTable nt = new NameTable();
string elementName = nt.Add("element");
XmlReaderSettings settings = new XmlReaderSettings() { NameTable = nt };




int elCount = 0;
using(XmlReader xr = XmlReader.Create("plik.xml", settings)) {
while(xr.Read()) {
if(xr.NodeType == XmlNodeType.Element &&
xr.LocalName == elementName) {
elCount++;
}
}
}

2) Do XmlReadera nie jest przekazywana tabela nazw

string elementName = "element";
int elCount = 0;
using(XmlReader xr = XmlReader.Create("plik.xml")) {
while(xr.Read()) {
if(xr.NodeType == XmlNodeType.Element &&
xr.LocalName == elementName) {
elCount++;
}
}
}

W testach kod wykorzystujący tabelę nazw okazał sie o prawie 5% szybszy. Mała rzecz a cieszy.

(Do testowania został wykonany dokument xml zawierający milion elementów o nazwie "element" będących dziećmi węzła root. Porównanie zostało wykonane na podstawie uśrednionego czasu wykonywania powyższych fragmentów w pętli o 20 powtórzeniach)