string işlemleri ve performans

Bize gelen "yüksek CPU kullanımı" ve "yüksek hafıza kullanımı" sorunlarının çoğunun birkaç tane ortak nedeni olduğunu gördüm. Bu yazımda bunlardan birinin üzerinde duracağım.

.NET uygulamalarında "string" işlemleri

"string" (dizgi) veri tipi, özellikle web uygulamalarında en çok kullandığımız veri tiplerinin başında geliyor. Web uygulamalarında muhtemelen, diğer tüm yazılım türlerinden daha çok "string" işlemi yapıyoruzdur. "string" işlemi derken kastımız, kelimelerin birleştirilip cümleler yaratılmasıdır. Bunu yapmanın en basit ve hızlı (yazılım geliştirme açısından hızlı, çalışma zamanındaki durumundan aşağıda bahsedeceğiz) yolu "+=" birleştirme operatörüdür.

Basit bir örnek vermek gerekirse:

             string tableStr = "<table>";
            for (int i = 0; i < 1000; i++)
            {
                tableStr += "<tr><td>" + i.ToString() + "</td></tr>";
            }
            tableStr += "</table>";
            Response.Write(tableStr);

Yukarıdaki kod parçası, toplam 1000 satırlık bir tablo yaratacaktır. Tam olarak bu şekilde olmasa da, pek çok uygulamamızda buna benzer işler yapmışızdır. Kodlaması kolay ve çok masum görünüyor, öyle değil mi? Ama değil! Neden masum olmadığını anlamak için, .NET'in "string" işlemlerini ve hafıza yönetimini nasıl yaptığına bakmak gerekecektir.

"string" veri tipi

String veri tipi, aslında sabit ebatlı bir veri tipidir. Yani, biz bu tipte bir değişken yarattığımızda, onun büyüklüğü kadar hafıza alanı ayrılır. Örneğin:

             string tableStr = "<table>";

satırı ile, hafızadan toplam 7 karakter için gerekli alan ayrılacaktır. Sonraki satırda tanımlanan "i" isimli değişken, "tableStr"den hemen sonraki hafıza alanında yaratılacaktır; yani 8. alan dolmuş olacaktır. Döngünün içine girilip, aşağıdaki satıra sıra geldiğinde, elimizdeki 7 karakterlik alan yetersiz kalacaktır. İşte sorun tam olarak burada ortaya çıkmaktadır. Bu soruna çözüm olarak, .NET, tableStr nesnesinin bir kopyasını yaratır. Ancak bu sefer sonuna eklenecek "string"i ekler ve hafızada gerekli alanı bu toplam üzerinden ayırır. Döngü bir daha döndüğünde aynı işlemi tekrarlar. Böylece, yukarıdaki gibi bir örnekte, hafızada bulunan ama kullanılmayan toplam 1000 tane "string" nesnesi bulunur:

 1. döngü: "<table><tr><td>0</td></tr>"
2. döngü: "<table><tr><td>0</td></tr><tr><td>1</td></tr>"
3. döngü: "<table><tr><td>0</td></tr><tr><td>1</td></tr><tr><td>2</td></tr>"
4. döngü: "<table><tr><td>0</td></tr><tr><td>1</td></tr><tr><td>2</td></tr><tr><td>3</td></tr>"
...

Yukarıda da görebileceğiniz gibi, GC devreye girene kadar hafıza da yer kaplayacak olan ciddi miktarda "string" nesnemiz yaratılmış olacaktır. Durumu göz önüne aldığımızda aslında .NET'in yaptığı çok da mantıksız bir çözüm değildir; ve az sayıda birleştirme işlerinde çok da işe yarayacak bir yöntemdir. Ancak bu örnektekine benzer durumlarda hem CPU hem de hafıza kullanımını ciddi oranda artıracaktır.

ÇÖZÜM

Bu kadar bariz ve büyük bir sorun varken, tahmin edebileceğiniz gibi, .NET buna bir çözüm de sunmaktadır: "StringBuilder" nesnesi. Yukarıdaki kodu yeniden yazarsak:

             StringBuilder sb = new StringBuilder("<table>");
            for (int i = 0; i < 1000; i++)
            {
                sb.Append("<tr><td>" + i.ToString() + "</td></tr>");
            }
            sb.Append("</table>");
            Response.Write(sb.ToString());

StringBuilder nesnesi, esnek ebatlıdır. Dolayısıyla, her "append" işleminde yeni bir kopya yaratılmaz. Aynı nesnenin ebatı büyütülür (bu iş tam olarak böyle değildir elbette ama detayları şu anda konumuz olmadığından girmeyeceğim).

Aşağıdaki makalede yukarıdakine benzer basit bir örnek kod verilmiş ve aradaki farkın ne kadar korkunç olabileceği anlatılmıştır:

https://support.microsoft.com/kb/306822

Yine aşağıdaki linkte StringBuilder nesnesi ile ilgili daha detaylı bilgi bulabilirsiniz:

https://msdn.microsoft.com/en-us/library/system.text.stringbuilder.aspx

CENK ISCAN