Bir döngü sırasında TextBox.Text'e eklemek neden her yinelemede daha fazla bellek kaplıyor?


82

Kısa Soru

180.000 kez çalışan bir döngüm var. Her yinelemenin sonunda, sonuçların gerçek zamanlı olarak güncellenen bir TextBox'a eklenmesi beklenir.

Kullanımı MyTextBox.Text += someValue, uygulamanın büyük miktarda bellek tüketmesine neden oluyor ve birkaç bin kayıttan sonra kullanılabilir belleği tükeniyor.

TextBox.Text180.000 defaya metin eklemenin daha verimli bir yolu var mı ?

Düzenleme Bu özel durumun sonucunu gerçekten umursamıyorum, ancak bunun neden bir bellek domuzu gibi göründüğünü ve bir TextBox'a metin eklemenin daha verimli bir yolu olup olmadığını bilmek istiyorum.


Uzun (Orijinal) Soru

Bir CSV dosyasındaki kimlik numaralarının listesini okuyan ve her biri için bir PDF raporu oluşturan küçük bir uygulamam var. Her bir pdf dosyası oluşturulduktan sonra, ResultsTextBox.Textişlenen ve başarıyla işlendiği raporun Kimlik Numarası eklenir. Süreç bir arka plan iş parçacığı üzerinde çalışır, bu nedenle ResultsTextBox öğeler işlenirken gerçek zamanlı olarak güncellenir

Şu anda uygulamayı 180.000 kimlik numarasıyla çalıştırıyorum, ancak uygulamanın kapladığı bellek zaman geçtikçe katlanarak artıyor. Yaklaşık 90K ile başlar, ancak yaklaşık 3000 kayıtta yaklaşık 250MB yer kaplar ve 4000 kayıtta uygulama yaklaşık 500 MB bellek kaplar.

Eğer Results TextBox'a yapılan güncellemeyi açıklarsam, bellek yaklaşık 90K'da nispeten sabit kalır, bu yüzden yazmanın ResultsText.Text += someValuehafızayı yemesine neden olduğunu varsayabilirim .

Sorum şu, bu neden? Bellek tüketmeyen bir TextBox.Text'e veri eklemenin daha iyi bir yolu nedir?

Kodum şöyle görünüyor:

try
{
    report.SetParameterValue("Id", id);

    report.ExportToDisk(ExportFormatType.PortableDocFormat,
        string.Format(@"{0}\{1}.pdf", new object[] { outputLocation, id}));

    // ResultsText.Text += string.Format("Exported {0}\r\n", id);
}
catch (Exception ex)
{
    ErrorsText.Text += string.Format("Failed to export {0}: {1}\r\n", 
        new object[] { id, ex.Message });
}

Ayrıca, uygulamanın tek seferlik bir şey olduğunu ve tüm raporları oluşturmanın birkaç saat (veya günler :)) almasının önemli olmadığını belirtmek gerekir. Benim asıl endişem, sistem bellek sınırına ulaşırsa çalışmayı durduracak olmasıdır.

Bu şeyi çalıştırmak için yorumlanan Sonuçları TextBox'ı güncelleyen satırı bırakmaktan memnunum, ancak TextBox.Textgelecekteki projelere veri eklemenin daha verimli bir bellek yolu olup olmadığını bilmek istiyorum .


7
StringBuilderMetni eklemek için a kullanmayı deneyebilir ve tamamlandığında StringBuilderdeğeri metin kutusuna atayabilirsiniz .
klavye

1
Herhangi bir şeyi değiştirip değiştirmeyeceğini bilmiyorum ama ya yeni Id'leri ekleyen bir StringBuilder'a sahip olsaydınız ve dize oluşturucunun yeni değeri ile güncellenen bir özellik kullanırsanız ve bunu metin kutunuza bağlarsanız. Emlak.
BigL

2
String.Format'ı çağırırken neden bir nesne dizisini başlatıyorsunuz? Bir dizi oluşturmaktan kaçınabilmeniz için 2 parametre alan aşırı yükler vardır. Artı aşırı param kullandığınızda, dizi sizin için perde arkasında oluşturulur.
ChaosPandion

1
string concat'in verimsiz olması gerekmez. Dizeleri birden çok çalışma biriminde birleştiriyorsanız ve sonuçları her çalışma birimi arasında görüntülüyorsanız, StringBuilder'dan daha verimli olacaktır. StringBuilder, yalnızca bir döngü aracılığıyla bir dizi oluşturduğunuzda ve ardından sonucu yalnızca döngünün sonunda yazdığınızda gerçekten daha etkilidir.
James Michael Hare

3
Bunun oldukça etkileyici bir makine olduğunu söyleyecektim :-)
James Michael Hare

Yanıtlar:


119

Bellek kullanımının bu kadar büyük olmasının nedeninin, metin kutularının kullanıcının metni geri alabilmesi / yeniden yapabilmesi için bir yığın tutması olduğundan şüpheleniyorum. Sizin durumunuzda bu özellik gerekli görünmüyor, bu yüzden IsUndoEnabledyanlış ayarlamayı deneyin .


1
MSDN bağlantısından: "Bellek sızıntısı Uygulamanızda, koddan değeri çok sık ayarladığınız için artan bir bellek varsa, metin bloğunun geri alma yığını bellek" sızıntısı "olabilir. Bu özelliği kullanarak devre dışı bırakabilirsiniz. ve bellek sızıntısına giden yolu temizliyor. "
dalga

33
Çoğu zaman, kullanıcılar ve geliştiriciler metin kutusunun standart metin kutuları gibi çalışmasını beklerler (yani, geri alma / yineleme özelliği ile). OP'nin gereksinimleri gibi uç durumlarda, bir engel oluşturabilir. İnsanların çoğu kullanıyorsa, varsayılan olmalıdır. Neden bir uç vakanın standart işlevselliği dahil olmaya zorlamasını beklersiniz?
keyboardP

1
Alternatif olarak, UndoLimitgerçekçi bir değere de ayarlayabilirsiniz . Varsayılan -1, sınırsız bir yığını gösterir. Sıfır (0) ayrıca geri almayı da devre dışı bırakır.
myermian

14

TextBox.AppendText(someValue)Bunun yerine kullanın TextBox.Text += someValue. TextBox.Text değil, TextBox'ta olduğu için gözden kaçırmak kolaydır. StringBuilder gibi, bu da her bir şey eklediğinizde metnin tamamının kopyalarını oluşturmaktan kaçınacaktır.

Bunun IsUndoEnabled, keyboardP'nin cevabındaki bayrağa kıyasla nasıl olduğunu görmek ilginç olurdu .


Windows formları söz konusu olduğunda, bu en iyi çözümdür, çünkü Windows formlarında
TextBox yoktur.IsUndoEnabled

Win formlarında bir bool CanUndomülkünüz var
imlokesh

9

Doğrudan metin özelliğine eklemeyin. Ekleme için bir StringBuilder kullanın, ardından bittiğinde, .text'i stringbuilder'dan bitmiş dizeye ayarlayın.


2
Döngünün bir arka plan iş parçacığında çalıştığını ve sonuçların gerçek zamanlı olarak güncellendiğinden bahsetmeyi unuttum
Rachel

5

Bir metin kutusu kullanmak yerine aşağıdakileri yapardım:

  1. Bir metin dosyası açın ve her ihtimale karşı hataları bir günlük dosyasına aktarın.
  2. Olası büyük dizelerin kopyalanmasını önlemek için hataları temsil eden bir liste kutusu denetimi kullanın.

4

Şahsen ben her zaman string.Concat* kullanıyorum. Yıllar önce Stack Overflow'da, yaygın olarak kullanılan yöntemleri karşılaştıran profilleme istatistiklerine sahip bir soruyu okuduğumu hatırlıyorum ve bu soruyu (görünüşe göre) string.Concatkazandı.

Yine de, Bulabileceğim en iyisi bu referans soru ve bu özel String.FormatvsStringBuilder bahseder soru String.Formatbir kullanımlarını StringBuilderiçten. Bu, hafızanızın başka bir yerde olup olmadığını merak etmeme neden oluyor.

** James'in yorumuna dayanarak, web tabanlı geliştirmeye odaklandığım için asla yoğun dizgi biçimlendirme yapmadığımı belirtmeliyim. *


Katılıyorum, bazen insanlar "her zaman X'i kullanın çünkü X en iyisidir", bu genellikle aşırı basitleştirmedir. String.Concat (), string.Format () ve StringBuilder arasında çok fazla incelik vardır. Benim temel kuralım her birini amaçladığı şeyi kullanmaktır (kulağa aptalca geliyor, biliyorum, ama doğrudur). Dizeleri birleştirirken (ve ardından sonucu hemen kullanarak) concat kullanıyorum, önemsiz olmayan dize biçimlendirmesi (dolgu, sayısal biçimler, vb.) Gerçekleştirirken Biçim'i ve bir döngü sırasında dizeleri oluşturmak için StringBuilder'ı kullanıyorum. Döngünün sonunda kullanılmalıdır.
James Michael Hare

@JamesMichaelHare, bu bana mantıklı geliyor; burada string.Format/ StringBuilderkullanımının daha uygun olduğunu mu söylüyorsunuz ?
jwheron

Oh hayır, sadece concat'in genellikle basit telli concat'ler için en iyisi olduğuna dair genel fikrinize katılıyorum. "Pratik kurallar" ile ilgili sorun, BCL değiştiğinde .NET sürümünden sürüme geçebilmeleridir, dolayısıyla mantıksal olarak doğru yapıya bağlı kalmanın daha sürdürülebilir olması ve genellikle görevleri için daha iyi performans göstermesidir. Aslında burada üçünü karşılaştırdığım daha eski bir blog yazım
James Michael Hare

Gerektiği gibi not edildi - sadece emin olmak istedim - ve "her zaman" kelimesini kullanmamı nitelendirmek için yanıt düzenlendi.
jwheron

3

Belki TextBox'ı yeniden düşünün? Dize Öğelerini tutan bir ListBox muhtemelen daha iyi performans gösterecektir.

Ancak asıl sorun gereksinimler gibi görünüyor: 180.000 öğenin gösterilmesi bir (insan) kullanıcıya yönelik olamaz, "Gerçek Zamanlı" da değiştirilemez.

Tercih edilen yol, verilerin bir örneğini veya bir ilerleme göstergesini göstermektir.

Zayıf Kullanıcıya dökmek istediğinizde, toplu dize güncellemeleri. Hiçbir kullanıcı saniyede 2 veya 3'ten fazla değişiklik fark edemez. Yani saniyede 100 üretirseniz, 50'li gruplar yapın.


Teşekkürler Henk. Bu tek seferlik bir şeydi, bu yüzden onu yazarken tembel davrandım. Durumun ne olduğunu bilmek için bir tür görsel çıktı istedim ve metin seçme yetenekleri ve bir ScrollBar istedim. Sanırım bir ScrollViewer / Label kullanmış olabilirim, ancak TextBoxes yerleşik ScrollBarrs'a sahip. Sorunlara neden olacağını beklemiyordum :)
Rachel

2

Bazı yanıtlar buna ima etti, ancak hiç kimse bunu açıkça belirtmedi ki bu şaşırtıcı. Dizeler değişmezdir, yani bir Dize oluşturulduktan sonra değiştirilemez. Bu nedenle, mevcut bir Dize ile her bitiştirdiğinizde, yeni bir Dize Nesnesinin oluşturulması gerekir. Bu Dize Nesnesi ile ilişkili belleğin de açıkça yaratılması gerekir, bu da Dizeleriniz büyüdükçe daha pahalı hale gelebilir. Üniversitede, bir keresinde, Huffman kodlama sıkıştırması yapan bir Java programında Dizeleri bitiştirme amatör hatasını yaptım. Çok büyük miktarlarda metni birleştirirken, String birleştirme, burada bazılarının da bahsettiği gibi, StringBuilder'ı kullanabildiğiniz zaman size gerçekten zarar verebilir.


2

StringBuilder'ı önerildiği gibi kullanın. Nihai dize boyutunu tahmin etmeye çalışın ve ardından StringBuilder'ı başlatırken bu sayıyı kullanın. StringBuilder sb = new StringBuilder (estSize);

TextBox'ı güncellerken sadece atamayı kullanın, örneğin: textbox.text = sb.ToString ();

Yukarıdaki gibi çapraz iş parçacığı işlemlerini izleyin. Ancak BeginInvoke kullanın. UI güncellenirken arka plan iş parçacığını engellemeye gerek yoktur.


1

A) Giriş: daha önce bahsedilmiş, kullanın StringBuilder

B) Nokta: çok sık güncelleme yapmayın, yani

DateTime dtLastUpdate = DateTime.MinValue;

while (condition)
{
    DoSomeWork();
    if (DateTime.Now - dtLastUpdate > TimeSpan.FromSeconds(2))
    {
        _form.Invoke(() => {textBox.Text = myStringBuilder.ToString()});
        dtLastUpdate = DateTime.Now;
    }
}

C) Bu tek seferlik bir işse, 2 Gb sınırında kalmak için x64 mimarisini kullanın.


1

StringBuilderin ViewModeldize yeniden bağlantılarını engelleyecek ve onu bağlayacaktır MyTextBox.Text. Bu senaryo, performansı birçok kez artıracak ve bellek kullanımını azaltacaktır.


0

Belirtilmeyen bir şey, işlemi arka plan iş parçacığında gerçekleştiriyor olsanız bile, UI öğesinin güncellemesinin ana iş parçacığının kendisinde (yine de WinForms'da) gerçekleşmesidir.

Metin kutunuzu güncellerken, şuna benzeyen herhangi bir kodunuz var mı?

if(textbox.dispatcher.checkAccess()){
    textbox.text += "whatever";
}else{
    textbox.dispatcher.invoke(...);
}

Öyleyse, arka plan operasyonunuz UI Güncellemesi tarafından kesinlikle darboğaz altındadır.

Arka plan işleminizin yukarıda belirtildiği gibi StringBuilder'ı kullanmasını öneririm, ancak metin kutusunu her döngüde güncellemek yerine, sizin için performansı artırıp artırmadığını görmek için düzenli aralıklarla güncellemeyi deneyin.

DÜZENLEME NOT: WPF kullanmamış.


0

Hafızanın katlanarak büyüdüğünü söylüyorsunuz. Hayır, ikinci dereceden bir büyüme , yani üstel bir büyüme kadar dramatik olmayan bir polinom büyümesidir.

Aşağıdaki sayıda öğeyi içeren dizeler oluşturuyorsunuz:

1 + 2 + 3 + 4 + 5 ... + n = (n^2 + n) /2.

İle , yani n = 180,000toplam bellek ayırma elde edersiniz.16,200,090,000 items16.2 billion items ! Bu bellek bir kerede tahsis edilmeyecektir, ancak GC (çöp toplayıcı) için çok fazla temizleme işi vardır!

Ayrıca, bir önceki dizenin (büyüyen) yeni dizeye 179.999 kez kopyalanması gerektiğini unutmayın. Toplam kopyalanan bayt sayısı,n^2 !

Diğerlerinin önerdiği gibi, bunun yerine bir ListBox kullanın. Burada, büyük bir dizge oluşturmadan yeni dizeler ekleyebilirsiniz. BirStringBuildAra sonuçları da görüntülemek istediğiniz için yardımcı olmuyor.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.