Aslında standart set kapları kendim çoğunlukla işe yaramaz buluyorum ve sadece dizileri kullanmayı tercih ediyorum ama farklı bir şekilde yapıyorum.
Küme kavşaklarını hesaplamak için, ilk dizi boyunca yinelenir ve öğeleri tek bir bitle işaretlerim. Sonra ikinci dizi üzerinden yineleme ve işaretli öğeleri arıyorum. Voila, bir karma tablodan çok daha az çalışma ve bellekle doğrusal zamanda kesişim ayarlayın, örneğin Sendikalar ve farklılıklar bu yöntemle uygulamak aynı derecede kolaydır. Kod tabanımın onları çoğaltmak yerine indeksleme öğeleri etrafında dönmesine yardımcı olur (endeksleri öğelerin kendilerine değil, öğelere çoğaltırım) ve nadiren sıralanacak bir şeye ihtiyaç duymaz, ancak yıllardır belirli bir veri yapısı kullanmadım sonuç.
Öğeler bu tür amaçlar için veri alanı sunmasa bile kullandığım bazı kötü bit-ciddling C kodu var. Enine biti (asla kullanmadığım) çaprazlanmış öğeleri işaretlemek için ayarlayarak elemanların belleğini kullanmayı içerir. Bu oldukça iğrenç, gerçekten yakın montaj seviyesinde çalışmadığınız sürece bunu yapmayın, ancak öğelerin geçiş için belirli bir alan sağlamadığı durumlarda bile bunun nasıl uygulanabileceğini belirtmek istedim. bazı bitler asla kullanılmayacaktır. Dinky i7'mde bir saniyeden daha az bir sürede 200 milyon eleman (2.4 gig veri) arasında belirli bir kavşak hesaplayabilir. std::set
Her biri aynı anda yüz milyon eleman içeren iki örnek arasında belirli bir kavşak yapmayı deneyin ; yaklaşmıyor bile.
Bu bir yana ...
Bununla birlikte, her bir elementi başka bir vektöre ekleyerek ve öğenin zaten var olup olmadığını kontrol ederek de yapabilirim.
Yeni vektörde bir elemanın mevcut olup olmadığını kontrol etmek, genellikle ayarlanan kavşağın kuadratik bir operasyon olmasını sağlayacak doğrusal bir zaman operasyonu olacaktır (patlayıcı çalışma miktarı giriş boyutu büyüdükçe). Sadece düz eski vektörleri veya dizileri kullanmak ve harika bir şekilde ölçeklendirilecek şekilde yapmak istiyorsanız yukarıdaki tekniği tavsiye ederim.
Temel olarak: ne tür algoritmalar bir set gerektirir ve başka bir kap tipi ile yapılmamalıdır?
Önyargılı görüşümü, konteyner düzeyinde (özellikle set işlemlerini verimli bir şekilde sağlamak için özel olarak uygulanan bir veri yapısında olduğu gibi) konuşuyorsanız sormazsınız, ancak kavramsal düzeyde set mantığı gerektiren çok şey vardır. Örneğin, bir oyun dünyasında hem uçan hem de yüzebilen yaratıkları bulmak istediğinizi ve bir sette (aslında bir set kabı kullanıp kullanmadığınıza bakılmaksızın) ve başka bir sette yüzebilen yaratıklara sahip olduğunuzu varsayalım . Bu durumda, ayarlanmış bir kavşak istersiniz. Uçabilecek ya da büyülü olabilecek yaratıklar istiyorsanız, o zaman belirli bir birlik kullanırsınız. Tabii ki bunu uygulamak için bir set konteynerine ihtiyacınız yoktur ve en uygun uygulama genellikle bir set olarak özel olarak tasarlanmış bir konteynere ihtiyaç duymaz veya bunu istemez.
Teğetten Çıkmak
Pekala, JimmyJames'ten bu set kavşak yaklaşımıyla ilgili bazı güzel sorular aldım. Konu biraz saptırıyor ama ah, daha fazla insanın sadece temel operasyonlar için dengeli ikili ağaçlar ve hash tabloları gibi tüm yardımcı yapıları inşa etmemeleri için kesişmeyi ayarlamak için bu temel müdahaleci yaklaşımı kullandığını görmekle ilgileniyorum. Belirtildiği gibi, temel gereklilik, listelerin sığ kopya öğelerini, ilk sıralanmamış listeden veya diziden veya daha sonra ikinci olarak alınacak her şeyden geçerken "işaretlenebilen" paylaşılan bir öğeyi dizine ekleyecek veya işaret edecek şekilde olmasıdır. ikinci listeden geçmek.
Bununla birlikte, bu, aşağıdaki unsurlara dokunmadan pratik olarak çok iş parçacıklı bir bağlamda gerçekleştirilebilir:
- İki küme elemanlara endeksler içerir.
- Endeks aralığı çok fazla değil (diyelim [0, 2 ^ 26), milyarlarca veya daha fazla değil) ve makul derecede yoğun.
Bu, ayarlanan işlemler için paralel bir dizi (öğe başına sadece bir bit) kullanmamıza izin verir. Diyagram:
İş parçacığı senkronizasyonu yalnızca havuzdan paralel bir bit dizisi alırken ve onu havuza geri gönderirken (kapsam dışına çıkıldığında örtük olarak yapılır) orada olmalıdır. Ayarlanan işlemi gerçekleştirmek için gerçek iki döngü herhangi bir evre senkronizasyonu gerektirmez. İş parçacığı bitleri yerel olarak tahsis edip serbest bırakabiliyorsa paralel bir bit havuzu kullanmamız bile gerekmez, ancak bit havuzu, merkezi öğelerin sıklıkla başvuruda bulunduğu bu tür bir veri sunumuna uyan kod tabanlarındaki deseni genelleştirmek için kullanışlı olabilir Böylece her bir iş parçacığının verimli bellek yönetimi ile uğraşmasına gerek kalmaz. Bölgem için başlıca örnekler, varlık bileşeni sistemleri ve dizin oluşturulmuş ağ temsilleridir. Her ikisi de sık sık kesişme noktalarına ihtiyaç duyar ve merkezi olarak depolanan her şeye gönderme eğilimindedir (ECS ve köşelerde bileşenler ve varlıklar, kenarlar,
Endeksler yoğun bir şekilde işgal edilmez ve seyrek olarak dağılmazsa, bu, paralel bit / boolean dizisinin makul bir seyrek uygulanmasıyla uygulanabilir, örneğin yalnızca 512 bitlik yığınlarda belleği depolayan (512 bitişik indeksi temsil eden kaydedilmemiş düğüm başına 64 bayt) ) ve tamamen boş bitişik blokların tahsis edilmesini atlar. Eğer merkezi veri yapılarınız elementlerin kendileri tarafından çok az işgal ediliyorsa, zaten böyle bir şey kullanıyorsunuzdur.
... seyrek bir bit kümesinin paralel bit dizisi olarak hizmet etmesi için benzer bir fikir. Bu yapılar aynı zamanda değişmezliğe de katkıda bulunur, çünkü yeni bir değişmez kopya oluşturmak için derin kopyalanması gerekmeyen sığ kopya tıknaz blokları kolaydır.
Yine yüz milyonlarca eleman arasındaki kesişimleri çok ortalama bir makinede bu yaklaşım kullanılarak bir saniyenin altında yapılabilir ve bu tek bir iş parçacığının içindedir.
İstemci, sonuçta elde edilen kavşak için bir öğe listesine ihtiyaç duymuyorsa, her iki listede bulunan öğelere sadece bir mantık uygulamak istiyorsa, hangi noktada geçebilecekleri gibi yarıdan daha kısa sürede yapılabilir. bir işlev işaretçisi veya işlevi veya temsilci veya kesişen öğelerin işlem aralığına geri çağrılacak her şey. Bu etki için bir şey:
// 'func' receives a range of indices to
// process.
set_intersection(func):
{
parallel_bits = bit_pool.acquire()
// Mark the indices found in the first list.
for each index in list1:
parallel_bits[index] = 1
// Look for the first element in the second list
// that intersects.
first = -1
for each index in list2:
{
if parallel_bits[index] == 1:
{
first = index
break
}
}
// Look for elements that don't intersect in the second
// list to call func for each range of elements that do
// intersect.
for each index in list2 starting from first:
{
if parallel_bits[index] != 1:
{
func(first, index)
first = index
}
}
If first != list2.num-1:
func(first, list2.num)
}
... ya da bu yönde bir şey. İlk diyagramdaki sözde kodun en pahalı kısmı intersection.append(index)
ikinci döngüdedir ve bu std::vector
, daha küçük listenin boyutuna önceden ayrılmış için bile geçerlidir .
Ya Her Şeyi Derin Kopyalarsam?
Kes şunu! Ayarlı kavşaklar yapmanız gerekiyorsa, kesişmek için verileri çoğalttığınız anlamına gelir. En küçük nesneleriniz bile 32 bitlik bir dizinden daha küçük olmayabilir. Gerçekte ~ 4.3 milyardan fazla öğeye ihtiyaç duyulmadığı sürece, öğelerinizin adresleme aralığını 2 ^ 32 (2 ^ 32 eleman, 2 ^ 32 bayt değil) azaltmak çok mümkündür, bu noktada tamamen farklı bir çözüme ihtiyaç vardır ( ve bu kesinlikle bellekte set kapları kullanmaz).
Anahtar Maçlar
Peki, öğelerin aynı olmadığı ancak eşleşen anahtarları olabileceği ayarlanmış işlemler yapmamız gereken durumlara ne dersiniz? Bu durumda, yukarıdakiyle aynı fikir. Her benzersiz anahtarı bir dizine eşlememiz yeterlidir. Örneğin, anahtar bir dize ise, stajyer dizeler bunu yapabilir. Bu durumlarda, dize anahtarlarını 32 bit dizinlere eşlemek için bir trie veya karma tablo gibi güzel bir veri yapısı çağrılır, ancak sonuçta elde edilen 32 bit dizinlerde ayarlanan işlemleri yapmak için bu tür yapılara ihtiyacımız yoktur.
Makinenin tüm adresleme aralığı değil, çok makul bir aralıktaki elemanlarla endekslerle çalışabildiğimizde, çok sayıda ucuz ve anlaşılır algoritmik çözüm ve veri yapısı bu şekilde açılır ve bu yüzden genellikle buna değmez. her benzersiz anahtar için benzersiz bir dizin elde edebilir.
Endeksleri Seviyorum!
Pizza ve bira kadar endeksleri seviyorum. 20'li yaşlarımdayken, C ++ 'a girdim ve her türlü tam standart uyumlu veri yapısını tasarlamaya başladım (derleme zamanında bir dolum cihazından bir ayırma cihazını ayırmak için gerekli numaralar dahil). Geçmişe bakıldığında bu büyük bir zaman kaybıydı.
Veritabanınızı parçaları diziler halinde merkezi olarak depolamak ve bunları parçalanmış ve potansiyel olarak makinenin tüm adreslenebilir aralığı boyunca depolamak yerine endekslemek etrafında döndürürseniz, algoritmik ve veri yapısı olasılıkları dünyasını sadece düz eski etrafında dönen kaplar ve algoritmalar tasarlama int
veya int32_t
. Ve sonuçların sürekli olarak bir veri yapısından diğerine öğeleri aktarmadığım yerlerde çok daha verimli ve bakımı kolay buldum.
Bazı örneklerde, herhangi bir benzersiz değerinin T
benzersiz bir dizine ve merkezi bir dizide yer alan örneklere sahip olacağını varsayabileceğiniz durumlar kullanılır :
Endeksler için işaretsiz tamsayılarla iyi çalışan çok iş parçacıklı sayı tabanı sıralamaları . Aslında, Intel'in kendi paralel sıralaması olarak yüz milyon elemanı sıralamak için yaklaşık 1/10 zaman alan çok iş parçacıklı bir radyan türüne sahibim ve Intel'in zaten std::sort
bu kadar büyük girdilere göre 4 kat daha hızlı . Elbette Intel'in karşılaştırma tabanlı bir türü olduğu ve şeyleri sözlükbilimsel olarak sıralayabildiği için çok daha esnektir, bu yüzden elmaları portakallarla karşılaştırır. Ama burada sadece portakallara ihtiyacım var, sadece önbellek dostu bellek erişim kalıplarını elde etmek veya kopyaları hızlı bir şekilde filtrelemek için bir sayı tabanı sıralaması yapabilirim.
Düğüm başına yığın tahsisi olmadan bağlantılı listeler, ağaçlar, grafikler, ayrı zincirleme karma tabloları vb . Düğümleri, elemanlara paralel olarak toplu olarak tahsis edebilir ve bunları endekslerle birbirine bağlayabiliriz. Düğümlerin kendileri bir sonraki düğüme 32 bitlik bir dizin haline gelir ve şöyle büyük bir dizide saklanır:
Paralel işleme için kolay. Bağlantılı yapılar genellikle paralel işleme için o kadar kolay değildir, çünkü en azından ağaçta veya bağlantılı liste geçişinde paralellik elde etmeye çalışmak, örneğin bir dizi boyunca döngü için bir paralel yapmak yerine getirmek oldukça gariptir. Dizin / merkezi dizi gösterimi ile her zaman bu merkezi diziye gidebilir ve her şeyi tıknaz paralel döngülerde işleyebiliriz. Her zaman bu şekilde işleyebileceğimiz tüm öğelerin merkezi dizisine sahibiz, ancak sadece bazılarını işlemek istesek bile (hangi noktada merkezi diziden önbellek dostu erişim için sayı tabanı sıralı bir liste tarafından dizine alınan öğeleri işleyebilirsiniz).
Verileri anında her öğeye sabit zamanda ilişkilendirebilir . Yukarıdaki paralel bit dizisinde olduğu gibi, paralel verileri, örneğin geçici işleme için elemanlarla kolayca ve son derece ucuz bir şekilde ilişkilendirebiliriz. Bunun geçici verilerin ötesinde kullanım örnekleri vardır. Örneğin, bir ağ sistemi, kullanıcıların bir ağa istedikleri kadar UV haritası eklemesine izin vermek isteyebilir. Böyle bir durumda, AoS yaklaşımı kullanarak her bir köşe ve yüzde kaç UV haritasının olacağını kodlayamayız. Bu tür verileri anında ilişkilendirebilmeliyiz ve paralel diziler orada kullanışlı ve her türlü karmaşık ilişkisel kaptan, hatta karma tablolardan çok daha ucuzdur.
Tabii ki paralel diziler, paralel dizileri birbirleriyle senkronize tutmada hataya eğilimli olmaları nedeniyle kaşlarını çatır. Örneğin, dizin 7'deki bir öğeyi "kök" dizisinden kaldırdığımızda, aynı şekilde "çocuklar" için de aynı şeyi yapmamız gerekir. Bununla birlikte, çoğu dilde, bu kavramı genel amaçlı bir kapsayıcıya genelleştirmek için yeterince kolaydır, böylece paralel dizileri birbiriyle senkronize tutmak için zor mantık, tüm kod tabanı boyunca tek bir yerde bulunmalıdır ve böyle bir paralel dizi kabı sonraki eklemeler sonrasında dizideki bitişik boş alanlar için çok fazla bellek israfını önlemek için yukarıdaki seyrek dizi uygulamasını kullanın.
Daha Ayrıntılı: Seyrek Bitset Ağacı
Pekala, alaycı olduğunu düşündüğüm biraz daha detaylandırma isteğim var, ama yine de yapacağım çünkü çok eğlenceli! İnsanlar bu fikri tamamen yeni seviyelere taşımak istiyorsa, N + M öğeleri arasında doğrusal olarak döngü yapmadan ayarlanmış kavşaklar gerçekleştirmek mümkündür. Bu, asırlardır ve temel olarak modeller için kullandığım nihai veri yapım set<int>
:
Her iki listedeki her öğeyi bile denetlemeden küme kesişmeleri gerçekleştirebilmesinin nedeni, hiyerarşinin kökündeki tek bir küme bitinin, örneğin kümede bir milyon bitişik öğenin işgal edildiğini gösterebilmesidir. Sadece bir biti inceleyerek, aralıktaki N indekslerinin [first,first+N)
N'de çok büyük bir sayı olabileceği sette olduğunu biliyoruz .
Bunu aslında işgal edilmiş endeksleri dolaşırken bir döngü iyileştirici olarak kullanıyorum, çünkü diyelim ki sette 8 milyon endeks var. Normalde bellekte 8 milyon tamsayıya erişmemiz gerekir. Bununla, potansiyel olarak sadece birkaç biti inceleyebilir ve işgal edilen endekslerin indeks aralıkları ile dolaşabilir. Ayrıca, ortaya çıkan endeks aralıkları, örneğin orijinal eleman verilerine erişmek için kullanılan sıralanmamış bir dizi indeks üzerinden yinelemenin aksine, çok önbellek dostu sıralı erişim sağlayan sıralı düzendedir. Tabii ki bu teknik son derece seyrek vakalar için daha kötüdür, en kötü senaryo her bir indeksin çift sayı (veya her biri garip) olması gibi bir durumdadır, bu durumda bitişik bölge yoktur. Ama en azından kullanım durumlarımda,