Bitişik olmayan diziler performans gösteriyor mu?


12

C # 'da, bir kullanıcı bir List<byte>bayt oluşturduğunda ve bayt eklediğinde, alanın bitmesi ve daha fazla alan ayırması gerekir. Önceki dizinin boyutunu iki katına çıkarır (veya başka bir çarpanı), baytları kopyalar ve başvuruyu eski diziye atar. Listenin katlanarak büyüdüğünü biliyorum, çünkü her tahsis pahalı ve bu da O(log n)tahsislerle sınırlanıyor , burada 10her seferinde fazladan öğe eklemek O(n)tahsislerle sonuçlanacaktır .

Ancak büyük dizi boyutları için, belki de dizinin neredeyse yarısı kadar çok fazla boşa alan olabilir. Belleği azaltmak için , listede 4MB'den daha az varsa destek deposu olarak NonContiguousArrayListkullanılan benzer bir sınıf yazdım List<byte>, daha sonra NonContiguousArrayListboyut olarak büyüdükçe ek 4MB bayt dizileri tahsis ederdi .

List<byte>Bu dizilerin aksine bitişik değildir, bu nedenle verilerin kopyalanması yoktur, sadece ek bir 4M tahsisi vardır. Bir öğe arandığında, öğeyi içeren dizinin dizinini almak için dizin 4M'ye, ardından dizinin içindeki dizini almak için 4M modulo'ya bölünür.

Bu yaklaşımla ilgili sorunlara dikkat çekebilir misiniz? İşte listem:

  • Bitişik olmayan dizilerde önbellek yeri yoktur ve bu da kötü performans sağlar. Ancak 4M blok boyutunda iyi önbellekleme için yeterli yer olacak gibi görünüyor.
  • Bir öğeye erişmek o kadar basit değil, fazladan bir dolaylama seviyesi var. Bu optimize edilebilir mi? Önbellek sorunlarına neden olur mu?
  • 4M sınırına ulaşıldıktan sonra doğrusal büyüme olduğundan, normalde olduğundan çok daha fazla ayırmaya sahip olabilirsiniz (örneğin, 1GB bellek için maksimum 250 ayırma). 4M'den sonra fazladan bellek kopyalanmaz, ancak fazladan ayırmaların büyük bellek parçalarını kopyalamaktan daha pahalı olup olmadığından emin değilim.

8
Teoriyi tükettiniz (önbelleği dikkate aldınız, asimptotik karmaşıklığı tartıştık), geriye kalan tek şey parametreleri (burada, alt liste başına 4M öğe) takmak ve belki de mikro-optimize etmektir. Şimdi karşılaştırma zamanı, çünkü donanımı ve uygulamayı düzeltmeden performansı daha fazla tartışmak için çok az veri var.

3
Tek bir koleksiyonda 4 milyondan fazla öğe ile çalışıyorsanız, konteyner mikro optimizasyonunun performans endişelerinizin en azını beklediğini umuyorum.
Telastyn

2
Açıkladığınız, kaydedilmemiş bağlantılı bir listeye benzer (çok büyük düğümlerle). Önbellek yerinin olmadığı iddianız biraz yanlış. Bir dizinin yalnızca çok fazlası tek bir önbellek satırına sığar; 64 bayt diyelim. Yani her 64 bayt bir önbellek özlüyor olacaksınız. Şimdi, düğümleri tam olarak 64 baytlık büyüklükte (çöp toplama için nesne başlığı dahil) olan, kaydedilmemiş bir bağlantı listesini düşünün. Hala her 64 baytta bir önbellek kaçırırsınız ve düğümlerin bellekte bitişik olmaması bile fark etmez.
Doval

@Doval 4M parçaları bir dizinin kendisinde saklandığından, gerçekte kaydedilmemiş bir bağlantı listesi değildir, bu nedenle herhangi bir öğeye erişim O (1) 'dir, B (blok boyutu) B'dir (n / B).

2
@ user2313838 1000MB bellek ve 350MB dizi varsa, diziyi büyütmek için gereken bellek 1050MB, mevcut olandan daha büyük olurdu, ana sorun budur, etkin sınırınız toplam alanınızın 1 / 3'üdür. TrimExcessyalnızca liste zaten oluşturulduğunda yardımcı olur ve yine de kopya için yeterli alan gerektirir.
noisecapella

Yanıtlar:


5

Bahsettiğiniz ölçeklerde, endişeler bahsettiğiniz ölçeklerden tamamen farklıdır.

Önbellek yeri

  • İlgili iki kavram vardır:
    1. Konum, son ziyaret edilen aynı önbellek satırındaki verilerin (uzamsal konum) yeniden kullanılması (geçici konum)
    2. Otomatik önbellek önceden getirme (akış).
  • Bahsettiğiniz ölçeklerde (4 MB'lık yığınlarla yüz MB'lardan gigabayta), iki faktörün veri öğesi erişim deseninizle bellek düzeninden daha fazla ilgisi vardır.
  • Benim (clueless) tahminim, istatistiksel olarak dev bir bitişik bellek ayırmadan çok fazla performans farkı olmayabilir. Kazanç yok, kayıp yok.

Veri öğesi erişim düzeni

  • Bu makalede, bellek erişim kalıplarının performansı nasıl etkileyeceği görsel olarak gösterilmektedir.
  • Kısacası, algoritmanızın bellek bant genişliği tarafından zaten tıkanmışsa, performansı artırmanın tek yolunun zaten önbelleğe yüklenen verilerle daha yararlı işler yapmak olduğunu unutmayın.
  • Başka bir deyişle, olsa bile içinde YourList[k]ve YourList[k+1]ardışık olma olasılığı yüksektir (bir değil olmanın dört milyon fırsatı), gerçeği değil yardım performansı tamamen rasgele veya örneğin büyük öngörülemeyen adımlarla listenizi erişecek olursawhile { index += random.Next(1024); DoStuff(YourList[index]); }

GC sistemi ile etkileşim

  • Bana göre, en çok odaklanmanız gereken yer burası.
  • En azından tasarımınızın aşağıdakilerle nasıl etkileşime gireceğini anlayın:
  • Bu konularda bilgili değilim, bu yüzden başkalarını katkıda bulunmaya bırakacağım.

Adres ofset hesaplamaları

  • Tipik C # kodu zaten çok fazla adres ofseti hesaplaması yapıyor, bu yüzden şemanızdaki ek yük tek bir dizide çalışan tipik C # kodundan daha kötü olmaz.
    • C # kodunun da dizi aralığı kontrolü yaptığını unutmayın; ve bu gerçek C # 'ın C ++ kodu ile karşılaştırılabilir dizi işleme performansına ulaşmasını engellemez.
    • Bunun nedeni, performansın çoğunlukla bellek bant genişliği tarafından engellenmesidir.
    • Bellek bant genişliğinden fayda maksimize etme hilesi, bellek okuma / yazma işlemleri için SIMD talimatlarını kullanmaktır. Ne tipik C # ne de tipik C ++ bunu yapmaz; kütüphanelere veya dil eklentilerine başvurmanız gerekir.

Nedenini göstermek için:

  • Adres hesaplaması yap
  • (OP durumunda, yığın taban adresini yükleyin (zaten önbellekte) ve daha fazla adres hesaplaması yapın)
  • Eleman adresinden okuma / yazma

Son adım hala aslanın zaman payını alıyor.

Kişisel öneri

  • CopyRangeİşlev gibi davranan Array.Copyancak iki örneğiniz NonContiguousByteArrayarasında veya bir örnek ile başka bir normal arasında çalışan bir işlev sağlayabilirsiniz byte[]. bu işlevler, bellek bant genişliği kullanımını en üst düzeye çıkarmak için SIMD kodunu (C ++ veya C #) kullanabilir ve C # kodunuz, çoklu kayıttan kaldırma veya adres hesaplaması yükü olmadan kopyalanan aralıkta çalışabilir.

Kullanılabilirlik ve birlikte çalışabilirlik endişeleri

  • Görünüşe göre bunu bitişik bayt dizileri veya sabitlenebilen bayt dizileri bekleyenNonContiguousByteArray herhangi bir C #, C ++ veya yabancı dil kitaplığıyla kullanamazsınız.
  • Ancak, kendi C ++ hızlandırma kitaplığınızı yazarsanız (P / Invoke veya C ++ / CLI ile), temel kod içine birkaç 4MB'lık blokların temel adresleri listesini iletebilirsiniz.
    • Örneğin, başlangıç (3 * 1024 * 1024)ve bitiş öğelerine erişim izni vermeniz gerekiyorsa (5 * 1024 * 1024 - 1), erişim chunk[0]ve ve arasında geçiş yapar chunk[1]. Daha sonra bir dizi bayt dizisi (boyut 4M) oluşturabilir, bu yığın adreslerini sabitleyebilir ve bunları alttaki koda aktarabilirsiniz.
  • Başka kullanılabilirlik endişe uygulamak mümkün olmayacaktır olmasıdır IList<byte>: verimli bir arayüz Insertve Removeonlar gerektirecektir, çünkü sadece sürecine çok uzun süreceğini O(N)zaman.
    • Aslında, başka bir şey uygulayamayacağınız IEnumerable<byte>anlaşılıyor, yani sırayla taranabilir ve hepsi bu kadar.

2
Veri yapısının ana avantajını kaçırmış görünüyorsunuz, bu da hafızanız tükenmeden çok büyük listeler oluşturmanıza izin veriyor. Liste <T> genişletilirken, eskisinin iki katı büyüklüğünde yeni bir dizi gerekir ve her ikisinin de bellekte aynı anda bulunması gerekir.
Frank Hileman

6

C ++ 'nın Standard, tarafından zaten eşdeğer bir yapıya sahip olduğunu belirtmek gerekir std::deque. Şu anda, rastgele erişilen bir şeyler dizisine ihtiyaç duymak için varsayılan seçim olarak önerilmektedir.

Gerçek şu ki, veriler belirli bir boyutu aştığında bitişik bellek neredeyse tamamen gereksizdir - önbellek satırı sadece 64 bayttır ve sayfa boyutu sadece 4-8KB'dir (şu anda tipik değerler). Birkaç MB hakkında konuşmaya başladığınızda, gerçekten bir endişe olarak pencereden çıkıyor. Aynısı dağıtım maliyeti için de geçerlidir. Tüm bu verileri işlemenin bedeli - sadece okumak bile - tahsislerin bedelini zaten gösterir.

Endişelenmenin diğer tek nedeni C API'leriyle arayüz oluşturmaktır. Ancak yine de bir Listenin arabelleğine bir işaretçi alamazsınız, bu yüzden burada endişelenmeyin.


Bu ilginç, dequebenzer bir uygulama olduğunu bilmiyordum
noisecapella

Şu anda std :: deque'yi kim tavsiye ediyor? Bir kaynak sağlayabilir misiniz? Ben her zaman std :: vector önerilen varsayılan seçim olarak düşünülmüştü.
Teimpz

std::dequeaslında MS standart kütüphane uygulaması çok kötü olduğu için büyük ölçüde cesaret kırılmıştır.
Sebastian Redl

3

Bellek parçaları, veri yapınızdaki alt dizilerde olduğu gibi zaman içinde farklı noktalara tahsis edildiğinde, bellekte birbirinden uzağa yerleştirilebilirler. Bunun bir sorun olup olmadığı CPU'ya bağlıdır ve artık tahmin edilmesi çok zordur. Test etmelisin.

Bu mükemmel bir fikir ve geçmişte kullandığım bir fikir. Tabii ki alt-dizi boyutlarınız ve bölme için bit değişimi için sadece iki güç kullanmalısınız (optimizasyonun bir parçası olabilir). Bu tür bir yapıyı biraz daha yavaş buldum, çünkü derleyiciler tek bir dizi dolaylamasını daha kolay bir şekilde optimize edebilir. Bu optimizasyon türleri her zaman değiştiği için test etmeniz gerekir.

Ana avantaj, bu tür yapıları tutarlı bir şekilde kullandığınız sürece sisteminizdeki üst bellek sınırına daha yakın çalışabilmenizdir. Veri yapılarınızı büyüttüğünüz ve çöp üretmediğiniz sürece, sıradan bir Liste için oluşacak ekstra çöp toplamalarından kaçınabilirsiniz. Dev bir liste için çok büyük bir fark yaratabilir: koşmaya devam etmek ile hafızanın bitmesi arasındaki fark.

Ek ayırmalar, her bir dizi ayırmada bellek ek yükü olduğundan, yalnızca alt dizi parçalarınız küçükse bir sorundur.

Sözlükler (hash tabloları) için benzer yapılar oluşturdum. .Net çerçevesi tarafından sağlanan Sözlük, List ile aynı soruna sahiptir. Sözlükler de yeniden şekillenmekten kaçınmanız gerektiğinden daha zordur.


Sıkıştırıcı bir toplayıcı, parçaları birbirine yakın şekilde sıkıştırabilir.
DeadMG

@DeadMG Bunun gerçekleşemeyeceği duruma atıfta bulunuyordum: aralarında çöp olmayan başka parçalar var. Liste <T> ile diziniz için bitişik bellek garanti edilir. Parçalanmış bir listeyle, bahsettiğiniz şanslı sıkıştırma durumuna sahip değilseniz, bellek sadece bir yığın içinde bitişiktir. Ancak bir sıkıştırma da çok sayıda verinin taşınmasını gerektirebilir ve büyük diziler Büyük Nesne Yığını'na girer. Karmaşık bir durum.
Frank Hileman

2

4M blok boyutuyla, tek bir bloğun bile fiziksel bellekte bitişik olduğu garanti edilmez; tipik bir VM sayfa boyutundan daha büyüktür. Yerellik bu ölçekte anlamlı değil.

Öbek parçalanması konusunda endişelenmeniz gerekecek: tahsisler, bloklarınız yığında büyük ölçüde bitişik olmayacak şekilde gerçekleşirse, GC tarafından geri alındıklarında, müteakip tahsis. İlişkisiz yerlerde başarısızlıklar olacağından ve muhtemelen uygulamanın yeniden başlatılmasına zorlanacağı için bu genellikle daha kötü bir durumdur.


Kompakt GC'ler parçalanma gerektirmez.
DeadMG

Bu doğrudur, ancak LOH sıkıştırması yalnızca doğru hatırladığımda .NET 4.5'ten itibaren kullanılabilir.
user2313838

Yığın sıkıştırması, standardın yeniden tahsise kopyalama davranışından daha fazla yüke neden olabilir List.
user2313838

Yeterince büyük ve uygun büyüklükte bir nesne, etkili bir şekilde parçalanma gerektirmez.
DeadMG

2
@DeadMG: GC sıkıştırmasıyla ilgili gerçek endişe (bu 4 MB şema ile), bu 4 MB'lık bifteklerin etrafında kürek çekmeye yararsız zaman harcayabileceğidir. Sonuç olarak büyük GC duraklamaları ile sonuçlanabilir. Bu nedenle, bu 4 MB şemasını kullanırken, ne yaptığını görmek ve düzeltici önlemler almak için hayati GC istatistiklerini izlemek önemlidir.
rwong

1

Kod tabanımın (ECS motoru) en merkezi kısımlarından bazılarını, daha küçük bitişik bloklar (4 megabayt yerine 4 kilobayt gibi) kullansa da, tanımladığınız veri yapısı türünde döndürüyorum.

resim açıklamasını buraya girin

Eklenmeye hazır (tam olmayan bloklar) için serbest bloklar için tek bir ücretsiz liste ve bu bloktaki endeksler için blok içinde bir alt serbest liste ile sabit zamanlı ekleme ve kaldırma işlemleri elde etmek için çift serbest liste kullanır. takıldıktan sonra geri kazanılmaya hazırdır.

Bu yapının artılarını ve eksilerini kapsayacağım. Bazı eksileri ile başlayalım çünkü bunlardan birkaçı var:

Eksileri

  1. Bu yapıya birkaç yüz milyon eleman eklemek std::vector(tamamen bitişik bir yapıdan) yaklaşık 4 kat daha uzun sürer . Ve mikro optimizasyonlarda oldukça iyiyim ama genel durum ilk önce blok ücretsiz listesinin üstündeki ücretsiz bloğu incelemek, sonra bloğa erişmek ve bloktan ücretsiz bir dizin açmak zorunda olduğu için kavramsal olarak daha fazla iş var. boş liste, öğeyi boş konuma yazın ve sonra bloğun dolu olup olmadığını kontrol edin ve varsa bloğu serbest listeden açın. Hâlâ sabit zamanlı bir işlem ancak geri itmekten çok daha büyük bir sabit std::vector.
  2. İndeksleme için ekstra aritmetik ve ekstra dolaylı katman verildiğinde rastgele erişimli bir desen kullanarak öğelere erişilirken yaklaşık iki kat daha uzun sürer.
  3. Sıralı erişim, yineleyici her artırıldığında ek dallandırma yapmak zorunda olduğundan bir yineleyici tasarımıyla verimli bir şekilde eşleşmez.
  4. Bellek biraz yükü vardır, genellikle eleman başına yaklaşık 1 bittir. Öğe başına 1 bit çok fazla gelmeyebilir, ancak bunu bir milyon 16 bit tam sayı saklamak için kullanıyorsanız, bu mükemmel bir kompakt diziden% 6.25 daha fazla bellek kullanımıdır. Bununla birlikte, pratikte bu std::vector, ayırdığı vectorfazla kapasiteyi ortadan kaldırmak için sıkıştırmadığınız sürece daha az bellek kullanma eğilimindedir . Ayrıca genellikle bu ufacık öğeleri saklamak için kullanmıyorum.

Artıları

  1. for_eachBir blok içindeki öğelerin geri arama işleme aralıklarını alan bir işlevi kullanarak sıralı erişim, sıralı erişim hızıyla neredeyse std::vector(yalnızca% 10 fark gibi) rakip olur , bu yüzden benim için en önemli performans kullanım durumlarında çok daha az verimli değildir ( ECS motorunda geçirilen çoğu zaman sıralı erişime sahiptir).
  2. Tamamen boş olduklarında blokların yer değiştirmesi ile ortadan sabit zamanlı olarak çıkarılmalarına izin verir. Sonuç olarak, veri yapısının asla gerekenden çok daha fazla bellek kullanmadığından emin olmak genellikle oldukça uygundur.
  3. Daha sonra yerleştirme üzerine bu delikleri geri almak için bir serbest liste yaklaşımı kullanarak arkasındaki delikler bıraktığından, doğrudan kaptan çıkarılmayan elemanlara endeksleri geçersiz kılmaz.
  4. Bu yapı epik sayıda öğe barındırsa bile hafızanın tükenmesi konusunda çok endişelenmenize gerek yok, çünkü sadece işletim sistemi için çok sayıda bitişik kullanılmayan bulmak için zorluk çekmeyen küçük bitişik bloklar istiyor sayfaları.
  5. Operasyonlar genellikle bireysel bloklara lokalize olduğundan, tüm yapıyı kilitlemeden eşzamanlılık ve iplik güvenliğine iyi borç verir.

Benim için en büyük profesyonellerden biri, bu veri yapısının değişmez bir versiyonunu yapmanın önemsiz hale gelmesiydi, şöyle:

resim açıklamasını buraya girin

O zamandan beri, istisna güvenliği, iş parçacığı güvenliği vb. bu veri yapısı gezerek ve kazayla, ama tartışmasız kod tabanını korumayı çok daha kolay hale getirdiği için sahip olduğu en güzel faydalardan biri.

Bitişik olmayan dizilerde önbellek yeri yoktur ve bu da kötü performans sağlar. Ancak 4M blok boyutunda iyi önbellekleme için yeterli yer olacak gibi görünüyor.

Referans konumu, 4 kilobaytlık bloklar hariç, bu boyuttaki bloklarda kendinizi ilgilendiren bir şey değildir. Önbellek satırı genellikle yalnızca 64 bayttır. Önbellek hatalarını azaltmak istiyorsanız, bu blokları düzgün bir şekilde hizalamaya odaklanın ve mümkünse daha fazla sıralı erişim düzenini tercih edin.

Rasgele erişimli bellek düzenini sıralı olana dönüştürmenin çok hızlı bir yolu bir bit kümesi kullanmaktır. Diyelim ki bir tekne yükünüz var ve bunlar rastgele sırada. Sadece onları sürebilir ve bit setindeki bitleri işaretleyebilirsiniz. Daha sonra bit setinizi tekrarlayabilir ve hangi baytların sıfır olmadığını kontrol edebilirsiniz, örneğin, bir seferde 64 bit. En az bir biti ayarlanmış 64 bitlik bir kümeyle karşılaştığınızda, hangi bitlerin ayarlandığını hızlı bir şekilde belirlemek için FFS talimatlarını kullanabilirsiniz . Bitler size hangi indekslere erişmeniz gerektiğini söyler, ancak şimdi endeksleri sıralı olarak sıralarsınız.

Bunun bir yükü vardır, ancak bazı durumlarda, özellikle de bu endekslerin üzerinde defalarca döngü yapacaksanız, değerli bir değişim olabilir.

Bir öğeye erişmek o kadar basit değil, fazladan bir dolaylama seviyesi var. Bu optimize edilebilir mi? Önbellek sorunlarına neden olur mu?

Hayır, optimize edilemez. En azından rastgele erişim bu yapı ile her zaman daha pahalıya mal olacaktır. Önbellek özlemlerinizi o kadar çok artırmaz, çünkü özellikle ortak vaka yürütme yollarınız sıralı erişim kalıpları kullanıyorsa, bloklara işaretçiler dizisi ile yüksek geçici konum elde etme eğiliminde olursunuz.

4M sınırına ulaşıldıktan sonra doğrusal büyüme olduğundan, normalde olduğundan çok daha fazla ayırmaya sahip olabilirsiniz (örneğin, 1GB bellek için maksimum 250 ayırma). 4M'den sonra fazladan bellek kopyalanmaz, ancak fazladan ayırmaların büyük bellek parçalarını kopyalamaktan daha pahalı olup olmadığından emin değilim.

Pratikte kopyalama genellikle daha hızlıdır, çünkü nadir bir durumdur, sadece log(N)/log(2)toplam kez gibi bir şey meydana gelirken, aynı zamanda, bir öğeyi dolmadan ve tekrar yeniden ayrılmadan önce diziye birçok kez yazabileceğiniz kir ucuz ortak durumu aynı anda basitleştirir. Bu nedenle, tipik olarak bu tür bir yapıya daha hızlı eklemeler elde edemezsiniz çünkü ortak vaka çalışması, büyük dizilerin yeniden tahsis edilmesi gibi pahalı nadir durumla uğraşmak zorunda olmasa bile daha pahalıdır.

Tüm eksilere rağmen bu yapının birincil cazibesi, bellek kullanımını azaltmak, OOM hakkında endişelenmek zorunda kalmamak, geçersiz olmayan endeksleri ve işaretçileri saklamak, eşzamanlılık ve değişmezliktir. Kendini temizlerken ve yapıya işaretçileri ve indeksleri geçersiz kılmazken, şeyleri sabit bir zamanda ekleyip kaldırabileceğiniz bir veri yapısına sahip olmak güzeldir.

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.