Yapılar ve sınıflar


95

Kodda 100.000 nesne oluşturmak üzereyim. Sadece 2 veya 3 mülkle küçük olanlardır. Onları genel bir listeye koyacağım ve olduklarında, onları döngüye sokup değeri kontrol aedip belki değeri güncelleyeceğim b.

Bu nesneleri sınıf olarak mı yoksa yapı olarak mı oluşturmak daha hızlı / daha iyi?

DÜZENLE

a. Özellikler değer türleridir (düşündüğüm dize dışında?)

b. (Henüz emin değiliz) bir doğrulama yöntemine sahip olabilirler

DÜZENLEME 2

Merak ediyordum: Yığın üzerindeki nesneler ve yığın çöp toplayıcı tarafından eşit olarak mı işleniyor yoksa bu farklı mı çalışıyor?


2
Sadece kamusal alanlara mı sahip olacaklar, yoksa yöntemleri de olacak mı? Türler tam sayılar gibi ilkel türler midir? Bir dizide mi yoksa List <T> gibi bir şeyde mi yer alacaklar?
JeffFerguson

14
Değiştirilebilir yapıların bir listesi? Velociraptor'a dikkat edin.
Anthony Pegram

1
@Anthony: Korkarım velociraptor şakasını kaçırıyorum: -s
Michel

5
Velociraptor şakası XKCD'den. Ancak, 'değer türleri yığına tahsis edilmiştir' yanılgısı / uygulama ayrıntısını attığınızda (uygunsa silin), o zaman Eric Lippert dikkat etmeniz gereken ...
Greg Beech

Yanıtlar:


138

O mi daha hızlı sınıf olarak veya yapı olarak bu nesneleri oluşturmak için?

Bu sorunun cevabını belirleyebilecek tek kişi sizsiniz. Her iki şekilde de deneyin, anlamlı, kullanıcı odaklı, alakalı bir performans metriğini ölçün ve ardından değişikliğin ilgili senaryolarda gerçek kullanıcılar üzerinde anlamlı bir etkisi olup olmadığını anlayacaksınız.

Yapılar daha az yığın belleği tüketir (çünkü daha küçüktürler ve daha kolay sıkıştırılırlar, "yığın üzerinde" oldukları için değil). Ancak kopyalanmaları referans kopyaya göre daha uzun sürer. Bellek kullanımı veya hız için performans ölçütlerinizin ne olduğunu bilmiyorum; burada bir değiş tokuş var ve ne olduğunu bilen kişi sensin.

Öyle mi daha iyi sınıf olarak veya yapı olarak bu nesneleri oluşturmak için?

Belki sınıf, belki yapı. Genel bir kural olarak: Eğer nesne ise:
1. Küçük
2. Mantıksal olarak değişmez bir değer
3. Birçoğu var
O zaman onu bir yapı yapmayı düşünürüm. Aksi takdirde bir referans türüne bağlı kalırım.

Bir yapının bazı alanlarını değiştirmeniz gerekiyorsa, alan doğru ayarlanmış olarak tamamen yeni bir yapı döndüren bir kurucu oluşturmak genellikle daha iyidir. Bu belki biraz daha yavaştır (ölçün!) Ama mantıksal olarak akıl yürütmesi daha kolaydır.

Öbek ve yığın üzerindeki nesneler çöp toplayıcı tarafından eşit olarak mı işleniyor?

Hayır , aynı değiller çünkü yığındaki nesneler koleksiyonun kökleridir . Çöp toplayıcının "bu şey yığında canlı mı?" Diye sormasına gerek yoktur. çünkü bu sorunun cevabı her zaman "Evet, yığında" dır. (Şimdi, buna güvenemezsin bir nesneyi canlı tutmak için çünkü yığın bir uygulama ayrıntısıdır. Jitter'in, örneğin normalde yığın değeri olacağını kaydeden optimizasyonlar sunmasına izin verilir ve sonra hiçbir zaman yığında olmaz bu yüzden GC hala hayatta olduğunu bilmiyor. Kayıtlı bir nesnenin soyundan gelenler, onu tutan kayıt tekrar okunmayacağı anda agresif bir şekilde toplanabilir.)

Ancak çöp toplayıcının , canlı olduğu bilinen herhangi bir nesneyi canlı olarak ele aldığı gibi, yığındaki nesnelere canlı muamelesi yapması gerekir. Yığın üzerindeki nesne, canlı tutulması gereken yığın tahsisli nesnelere başvurabilir, bu nedenle GC, canlı kümeyi belirlemek amacıyla yığın nesnelerini canlı yığın tahsisli nesneler gibi ele almak zorundadır. Ama açıkçası bunlar değil onlar ilk etapta öbek üzerinde değil, çünkü, yığın sıkıştırma amacıyla "canlı nesneler" olarak ele.

Anlaşıldı mı?


Eric, derleyicinin ya da seğirmenin readonlyoptimizasyonlara izin vermek için değişmezlikten (belki de zorlanırsa ) yararlandığını biliyor musun ? Bunun değişkenlik konusundaki bir seçimi etkilemesine izin vermezdim (teoride verimlilik detayları için deliyim, ancak pratikte verimliliğe yönelik ilk adımım her zaman elimden geldiğince basit bir doğruluk garantisine sahip olmaya çalışmaktır ve bu nedenle zorunda kalmam) İşlemci döngülerini ve beyin döngülerini kontroller ve uç durumlar için boşa harcayın ve uygun şekilde değiştirilebilir veya değişmez olmak buna yardımcı olur), ancak bu, değişmezliğin daha yavaş olabileceğini söylemenize karşı herhangi bir diz-ani tepkiye karşı koyacaktır.
Jon Hanna

@Jon: C # derleyicisi const verilerini optimize eder ancak salt okunur verileri değil . Jit derleyicisinin salt okunur alanlarda herhangi bir önbelleğe alma optimizasyonu gerçekleştirip gerçekleştirmediğini bilmiyorum.
Eric Lippert

Ne yazık ki, değişmezlik bilgisi bazı optimizasyonlara izin veriyor, ancak bu noktada teorik bilgimin sınırlarını aşıyor, ancak bunlar esnetmeyi çok istediğim sınırlar. :) söylemek mümkün yararlıdır arada "bu durumda geçerli şimdi testi ve nedenini öğrenmek, daha hızlı iki yöne olabilir, burada"
Jon Hanna

Okumayı öneriyoruz simple-talk.com/dotnet/.net-framework/... ve kendi makale (@Eric): blogs.msdn.com/b/ericlippert/archive/2010/09/30/... dalışı başlatmak için ayrıntılara. Etrafta birçok iyi makale var. BTW, 100 000 küçük bellek içi nesnenin işlenmesindeki fark, sınıf için bir miktar bellek ek yükü (~ 2.3 MB) sayesinde neredeyse hiç fark edilmez. Basit bir test ile kolayca kontrol edilebilir.
Nick Martyshchenko

23

Bazen structyeni () kurucuyu çağırmanıza ve alanları doğrudan atamanıza gerek kalmaz, bu da onu her zamankinden daha hızlı hale getirir.

Misal:

Value[] list = new Value[N];
for (int i = 0; i < N; i++)
{
    list[i].id = i;
    list[i].isValid = true;
}

şundan yaklaşık 2 ila 3 kat daha hızlıdır

Value[] list = new Value[N];
for (int i = 0; i < N; i++)
{
    list[i] = new Value(i, true);
}

nerede Valuebir olduğunu structiki alan (ile idve isValid).

struct Value
{
    int id;
    bool isValid;

    public Value(int i, bool isValid)
    {
        this.i = i;
        this.isValid = isValid;
    }
}

Öte yandan, öğelerin taşınması gerekir veya tüm bu kopyalamanın sizi yavaşlatacağı değer türleri seçilir. Kesin yanıtı almak için kodunuzun profilini çıkarmanız ve test etmeniz gerektiğinden şüpheleniyorum.


Açıkçası, değerleri yerel sınırların ötesinde sıraladığınızda işler çok daha hızlı hale gelir.
leppie

listBelirtilen kodun a ile çalışmayacağı göz önüne alındığında dışında bir ad kullanmanızı öneririm List<Value>.
supercat

7

Yapılar sınıflara benzer görünebilir, ancak bilmeniz gereken önemli farklılıklar vardır. Her şeyden önce, sınıflar başvuru türleridir ve yapılar değer türleridir. Yapılar kullanarak, yerleşik türler gibi davranan ve bunların faydalarından da yararlanan nesneler oluşturabilirsiniz.

Bir sınıfta Yeni işleci aradığınızda, öbek üzerinde tahsis edilecektir. Bununla birlikte, bir yapıyı başlattığınızda, yığın üzerinde oluşturulur. Bu performans kazanımları sağlayacaktır. Ayrıca, sınıflarda yaptığınız gibi bir yapı örneğine yapılan başvurularla uğraşmayacaksınız. Doğrudan struct örneğiyle çalışacaksınız. Bu nedenle, bir yapı bir yönteme aktarılırken, referans yerine değere göre aktarılır.

Daha fazla burada:

http://msdn.microsoft.com/en-us/library/aa288471(VS.71).aspx


4
Bunu MSDN'de söylediğini biliyorum, ancak MSDN tüm hikayeyi anlatmıyor. Yığın ve yığın bir uygulama ayrıntısıdır ve yapılar her zaman yığına gitmez. Bununla ilgili yeni bir blog için bakınız: blogs.msdn.com/b/ericlippert/archive/2010/09/30/…
Anthony Pegram

"... değere göre aktarılır ..." hem referanslar hem de yapılar değere göre aktarılır ('ref' kullanılmadığı sürece) - farklı olan bir değerin veya referansın aktarılıp aktarılmadığıdır, yani yapılar değer bazında geçirilir , sınıf nesneleri değere göre geçirilir ve ref işaretli parametreler başvuruya göre geçer.
Paul Ruane

10
Bu makale birkaç önemli noktada yanıltıcıdır ve MSDN ekibinden onu gözden geçirmesini veya silmesini istedim.
Eric Lippert

2
@supercat: ilk noktanızı ele almak için: daha büyük olan nokta, bir değerin veya bir değere referansın depolandığı yönetilen kodda büyük ölçüde alakasız olmasıdır . Çoğu zaman geliştiricilerin çalışma zamanının kendi adlarına akıllı depolama kararları vermesine izin veren bir bellek modeli oluşturmak için çok çalıştık. Bu ayrımlar, onları anlayamamanın, C'de olduğu gibi çarpışan sonuçları olduğu zaman çok önemlidir; C # 'da pek değil.
Eric Lippert

1
@supercat: ikinci noktaya değinmek için, hiçbir değiştirilebilir yapı çoğunlukla kötü değildir. Örneğin, void M () {S s = new S (); s.Blah (); N (s); }. Yeniden düzenleme: void DoBlah (S s) {s.Blah (); } void M (S s = new S (); DoBlah (s); N (s);}. S değişken bir yapı olduğu için bu bir hata ortaya çıkardı. Hatayı hemen gördünüz mü yoksa S değişebilir bir yapı hatayı sizden mi gizler ?
Eric Lippert

6

Yapı dizileri, bitişik bir bellek bloğunda yığın üzerinde temsil edilirken, bir nesne dizisi, öbek üzerinde başka bir yerde gerçek nesnelerin kendisiyle birlikte bitişik bir referans bloğu olarak temsil edilir, böylece hem nesneler hem de dizi referansları için bellek gerektirir. .

Bu durumda, onları bir diziye yerleştirdiğiniz için List<>(ve List<>bir diziye yedeklendiğinden), yapıları kullanmak bellek açısından daha verimli olacaktır.

(Yine de, büyük dizilerin yaşam süreleri uzunsa, işleminizin bellek yönetimi üzerinde olumsuz bir etkiye sahip olabileceği Büyük Nesne Yığını üzerinde yollarını bulacağına dikkat edin. Ayrıca, hafızanın tek husus olmadığını unutmayın.)


refBununla başa çıkmak için anahtar kelime kullanabilirsiniz .
leppie

"Yine de, büyük dizilerin, yaşam süreleri uzunsa, işleminizin bellek yönetimi üzerinde olumsuz bir etkisi olabileceği Büyük Nesne Yığını üzerinde yollarını bulacağına dikkat edin." - Neden böyle düşündüğünden emin değilim? LOH'ye tahsis edilmiş olmak, (muhtemelen) kısa ömürlü bir nesne olmadıkça ve bir Gen 2 koleksiyonunu beklemeden hafızayı hızlı bir şekilde geri almak istemedikçe, bellek yönetimi üzerinde herhangi bir olumsuz etkiye neden olmaz.
Jon Artus

@Jon Artus: LOH sıkıştırılmıyor. Uzun ömürlü herhangi bir nesne, LOH'yi öncesi ve sonrası boş hafıza alanına böler. Tahsisat için bitişik bellek gereklidir ve bu alanlar bir tahsis için yeterince büyük değilse, LOH'ye daha fazla bellek tahsis edilir (yani, LOH parçalanması elde edersiniz).
Paul Ruane

4

Değer semantiği varsa, muhtemelen bir yapı kullanmalısınız. Referans semantiği varsa, muhtemelen bir sınıf kullanmalısınız. Değer semantikleri varken bile çoğunlukla bir sınıf yaratmaya meyilli olan, ancak oradan başlayan istisnalar vardır.

İkinci düzenlemenize gelince, GC yalnızca yığınla ilgilenir, ancak yığın alanından çok daha fazla yığın alanı vardır, bu nedenle bir şeyleri yığına koymak her zaman bir kazanç değildir. Bunun yanı sıra, yapı türlerinin bir listesi ve sınıf türlerinin bir listesi her iki şekilde de yığında olacaktır, dolayısıyla bu durum bu durumda önemsizdir.

Düzenle:

Kötü terimini düşünmeye başlıyorum zararlı . Sonuçta, eğer aktif olarak ihtiyaç duyulmuyorsa, bir sınıfı değiştirilebilir yapmak kötü bir fikirdir ve değişken bir yapı kullanmayı asla göz ardı etmem. Neredeyse her zaman kötü bir fikir olduğu için çoğu zaman zayıf bir fikirdir, ancak çoğunlukla değer anlambilimiyle örtüşmez, bu nedenle verilen durumda bir yapı kullanmak mantıklı değildir.

Özel iç içe geçmiş yapılarda makul istisnalar olabilir, bu yapının tüm kullanımları bu nedenle çok sınırlı bir kapsamla sınırlıdır. Bu burada geçerli değil.

Gerçekten, bence "mutasyona uğruyor, bu yüzden kötü bir davranış", yığın ve yığın hakkında devam etmekten çok daha iyi değil (en azından, sık sık yanlış temsil edilse bile, en azından bazı performans etkisine sahip). "Bu mutasyon, o kadar büyük olasılıkla kötü bir yapı bu yüzden, değer anlambilimini sahip olarak bunu düşünmek mantıklı değil" sadece biraz farklı, ama önemlisi bence bu yüzden.


3

En iyi çözüm ölçmek, tekrar ölçmek ve sonra biraz daha ölçmektir. Yaptığınız şeyin ayrıntıları, "yapıları kullan" veya "sınıfları kullan" gibi basitleştirilmiş, kolay bir cevabı zorlaştırabilir.


Ölçü kısmına katılıyorum, ancak bence bu açık ve net bir örnekti ve belki de bu konuda bazı genel şeyler söylenebileceğini düşündüm. Ve ortaya çıktıkça, bazı insanlar yaptı.
Michel

3

Bir yapı, özünde, alanların toplamından ne fazlası ne de azıdır. .NET'te bir yapının bir nesne "gibi davranması" mümkündür ve her yapı türü için .NET örtük olarak aynı alanlara ve yöntemlere sahip bir yığın nesnesi türü tanımlar - bir yığın nesnesi olarak - bir nesne gibi davranır. . Böyle bir öbek nesnesine ("kutulu" yapı) bir referans tutan bir değişken, referans anlambilimini sergileyecektir, ancak bir yapıyı doğrudan tutan bir değişken basitçe bir değişkenlerin toplamıdır.

Bence yapı-sınıf karmaşasının çoğu, yapıların çok farklı tasarım yönergelerine sahip olması gereken çok farklı iki kullanım durumuna sahip olmasından kaynaklanıyor, ancak MS yönergeleri bunları birbirinden ayırmıyor. Bazen bir nesne gibi davranan bir şeye ihtiyaç vardır; bu durumda, MS yönergeleri oldukça mantıklıdır, ancak "16 bayt sınırı" muhtemelen daha çok 24-32 gibi olmalıdır. Ancak bazen ihtiyaç duyulan şey değişkenlerin bir araya toplanmasıdır. Bu amaçla kullanılan bir yapı, basitçe bir grup genel alandan ve muhtemelen birEquals geçersiz kılma, ToStringgeçersiz kılma veIEquatable(itsType).Equalsuygulama. Alanların toplamaları olarak kullanılan yapılar nesne değildir ve öyle görünmemelidir. Yapının bakış açısından, alanın anlamı "bu alana yazılan son şey" den daha fazla veya daha az olmamalıdır. Herhangi bir ek anlam müşteri koduna göre belirlenmelidir.

Örneğin, bir değişken toplama yapısının üyeleri varsa Minimumve Maximumyapının kendisi bunu vaat etmemelidir Minimum <= Maximum. Parametre olarak böyle bir yapıyı alan kod, ayrı Minimumve Maximumdeğerler aktarılmış gibi davranmalıdır . Bundan Minimumdaha büyük olmayan Maximumbir gereksinim, bir Minimumparametrenin ayrı olarak geçirilmiş olandan daha büyük olmaması şartı olarak görülmelidir Maximum.

Bazen dikkate alınması gereken kullanışlı bir model, aşağıdaki gibi bir ExposedHolder<T>sınıfa sahip olmaktır :

class ExposedHolder<T>
{
  public T Value;
  ExposedHolder() { }
  ExposedHolder(T val) { Value = T; }
}

Biri varsa List<ExposedHolder<someStruct>>, nerede someStructbir değişken toplayarak yapı, tek gibi şeyler yapabilir olduğunu myList[3].Value.someField += 7;, ancak vererek myList[3].Valuediğer koda o içeriğini verecek Valueo bunu değiştirmek için bir araç vererek yerine. Aksine, eğer biri a kullanılıyorsa List<someStruct>, kullanmak gerekli olacaktır var temp=myList[3]; temp.someField += 7; myList[3] = temp;. Değiştirilebilir bir sınıf türü kullanılıyorsa, içeriğinin myList[3]dış koda sunulması , tüm alanların başka bir nesneye kopyalanmasını gerektirir. Eğer biri değişmez bir sınıf türü veya "nesne-tarzı" bir yapı kullanırsa, şunun myList[3]dışında olduğu gibi yeni bir örnek oluşturmak gerekecektir.someField ve sonra bu yeni örneği listeye .

Ek bir not: Çok sayıda benzer şeyi depoluyorsanız, bunları muhtemelen iç içe geçmiş yapı dizilerinde depolamak, tercihen her dizinin boyutunu 1K ile 64K arasında tutmaya çalışmak iyi olabilir. Yapı dizileri özeldir, çünkü indeksleme içerisindeki bir yapıya doğrudan bir referans verecektir, bu nedenle kişi "a [12] .x = 5;" diyebilir. Dizi benzeri nesneler tanımlanabilmesine rağmen, C # bu tür sözdizimini dizilerle paylaşmalarına izin vermez.


1

Sınıfları kullanın.

Genel bir not olarak. Siz onları oluştururken neden b değerini güncellemiyorsunuz?


1

Bir c ++ perspektifinden, bir yapı özelliklerini bir sınıfa kıyasla değiştirmenin daha yavaş olacağını kabul ediyorum. Ancak, yığın yerine yığın üzerine ayrılan yapı nedeniyle okunmasının daha hızlı olacağını düşünüyorum. Yığından veri okumak, yığından daha fazla denetim gerektirir.


0

Eğer struct afterall ile giderseniz, o zaman dizeden kurtulun ve sabit boyutlu karakter veya bayt tampon kullanın.

Bu yeniden: performans.

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.