Özel C ++ ayırıcılarının çekici örnekleri?


176

std::allocatorÖzel bir çözüm lehine hendek için gerçekten iyi nedenler nelerdir? Doğruluk, performans, ölçeklenebilirlik vb. İçin kesinlikle gerekli olan herhangi bir durumla karşılaştınız mı? Gerçekten akıllı örnekler var mı?

Özel ayırıcılar her zaman çok fazla ihtiyaç duymadığım Standart Kütüphane'nin bir özelliği olmuştur. Ben sadece burada SO üzerinde varlıklarını haklı çıkarmak için bazı zorlayıcı örnekler verebilir olup olmadığını merak ediyordum.

Yanıtlar:


121

Bahsetmek gibi burada , ben Intel TBB en özel STL ayırıcısı önemli ölçüde sadece tek değiştirerek parçacıklı app performansını artırmak gördüm

std::vector<T>

için

std::vector<T,tbb::scalable_allocator<T> >

(bu, ayırıcıyı TBB'nin şık iplik-özel yığınlarını kullanacak şekilde değiştirmenin hızlı ve kullanışlı bir yoludur; bu belgedeki sayfa 7'ye bakın )


3
İkinci bağlantı için teşekkürler. İplik-özel yığınlar uygulamak için ayırıcıların kullanımı zekidir. Bu özel ayırıcılar kaynak sınırlı olmayan bir senaryoda (gömme veya konsol) açık bir avantajı var iyi bir örnek olduğunu seviyorum.
Naaff

7
Orijinal bağlantı artık feshedilmiş, ancak CiteSeer PDF vardır: citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.71.8289
Arto Bendiken

1
Sormak zorundayım: Böyle bir vektörü başka bir iş parçacığına güvenilir bir şekilde taşıyabilir misiniz? (Hayır tahmin ediyorum)
sellibitze

@sellibitze: Vektörler TBB görevlerinden manipüle edildiğinden ve çoklu paralel işlemlerde yeniden kullanıldığından ve TBB işçi iş parçacığının görevleri toplayacağının garantisi yoktur, bunun iyi çalıştığı sonucuna varıyorum. Her ne kadar başka bir iş parçacığında bir iş parçacığı üzerinde oluşturulan TBB serbest malzeme ile bazı tarihi sorunlar olduğunu unutmayın (görünüşe göre iş parçacığı özel yığınlar ve üretici-tüketici tahsis ve anlaşma desenleri ile klasik bir sorun. . Belki yeni sürümlerinde sabit).
timday

@ArtoBendiken: Bağlantınızdaki indirme bağlantısı geçerli görünmüyor.
einpoklum

81

Özel ayırıcıların yararlı olabileceği alanlardan biri, özellikle oyun konsollarında, yalnızca az miktarda belleğe sahip oldukları ve takasları olmadığı için oyun geliştirmedir. Bu tür sistemlerde, kritik olmayan bir sistemin belleği kritik bir sistemden çalamaması için her alt sistem üzerinde sıkı bir kontrole sahip olduğunuzdan emin olmak istersiniz. Havuz ayırıcılar gibi diğer şeyler de bellek parçalanmasını azaltmaya yardımcı olabilir. Konu hakkında uzun ve ayrıntılı bir makale bulabilirsiniz:

EASTL - Electronic Arts Standart Şablon Kütüphanesi


14
EASTL bağlantısı için +1: "Oyun geliştiricileri arasında [STL'nin] en temel zayıflığı std ayırıcı tasarımıdır ve EASTL'nin oluşturulmasına katkıda bulunan en büyük faktör bu zayıflıktır."
Naaff

65

Vektörlerin bellek eşlemeli bir dosyadan bellek kullanmasına izin veren bir mmap-ayırıcı üzerinde çalışıyorum. Amaç, doğrudan mmap tarafından eşlenen sanal bellekte depo kullanan vektörlere sahip olmaktır. Bizim sorunumuz, gerçekten büyük dosyaların (> 10GB) kopya yükü olmadan belleğe okunmasını iyileştirmektir, bu nedenle bu özel ayırıcıya ihtiyacım var.

Şimdiye kadar (std :: allocator'dan türetilen) özel bir ayırıcı iskeletine sahibim, kendi ayırıcılarını yazmak için iyi bir başlangıç ​​noktası olduğunu düşünüyorum. Bu kod parçasını istediğiniz şekilde kullanmaktan çekinmeyin:

#include <memory>
#include <stdio.h>

namespace mmap_allocator_namespace
{
        // See StackOverflow replies to this answer for important commentary about inheriting from std::allocator before replicating this code.
        template <typename T>
        class mmap_allocator: public std::allocator<T>
        {
public:
                typedef size_t size_type;
                typedef T* pointer;
                typedef const T* const_pointer;

                template<typename _Tp1>
                struct rebind
                {
                        typedef mmap_allocator<_Tp1> other;
                };

                pointer allocate(size_type n, const void *hint=0)
                {
                        fprintf(stderr, "Alloc %d bytes.\n", n*sizeof(T));
                        return std::allocator<T>::allocate(n, hint);
                }

                void deallocate(pointer p, size_type n)
                {
                        fprintf(stderr, "Dealloc %d bytes (%p).\n", n*sizeof(T), p);
                        return std::allocator<T>::deallocate(p, n);
                }

                mmap_allocator() throw(): std::allocator<T>() { fprintf(stderr, "Hello allocator!\n"); }
                mmap_allocator(const mmap_allocator &a) throw(): std::allocator<T>(a) { }
                template <class U>                    
                mmap_allocator(const mmap_allocator<U> &a) throw(): std::allocator<T>(a) { }
                ~mmap_allocator() throw() { }
        };
}

Bunu kullanmak için, bir STL kabını aşağıdaki gibi bildirin:

using namespace std;
using namespace mmap_allocator_namespace;

vector<int, mmap_allocator<int> > int_vec(1024, 0, mmap_allocator<int>());

Örneğin, bellek her tahsis edildiğinde günlüğe kaydetmek için kullanılabilir. Gerekli olan yeniden bağlama yapısıdır, aksi takdirde vektör kabı üst sınıfların tahsis / dağıtma yöntemlerini kullanır.

Güncelleme: Bellek eşleme ayırıcısı artık https://github.com/johannesthoma/mmap_allocator adresinde mevcuttur ve LGPL'dir. Projeleriniz için kullanmaktan çekinmeyin.


17
Sadece bir adım yukarı, std :: allocator'dan türetmek, allocator yazmanın deyimsel yolu değildir. Bunun yerine, asgari işlevsellik sağlamanıza izin veren allocator_traits'e bakmalısınız ve traits sınıfı gerisini sağlayacaktır. STL'nin allocator'unuzu her zaman doğrudan değil allocator_traits aracılığıyla kullandığını unutmayın, bu yüzden allocator_traits'e kendiniz başvurmanıza gerek yoktur (std :: allocator'dan türetmek için çok fazla teşvik yoktur (bu kod ne olursa olsun yararlı bir başlangıç ​​noktası olabilir).
Nir Friedman

25

Kod için c ++ kullanan bir MySQL depolama motoru ile çalışıyorum. Bellek için MySQL ile rekabet etmek yerine, MySQL bellek sistemini kullanmak için özel bir ayırıcı kullanıyoruz. Belleği, kullanıcının "ekstra" değil, MySQL'i kullanması için yapılandırmış olarak kullandığımızdan emin olmamızı sağlar.


21

Yığın yerine bellek havuzu kullanmak için özel ayırıcılar kullanmak yararlı olabilir. Bu diğerleri arasında bir örnek.

Çoğu durumda, bu kesinlikle erken bir optimizasyondur. Ancak bazı bağlamlarda (gömülü cihazlar, oyunlar vb.) Çok yararlı olabilir.


3
Veya, bu bellek havuzu paylaşıldığında.
Anthony

9

Özel bir STL ayırıcısı ile C ++ kodu yazmadım, ancak bir HTTP isteğine yanıt vermek için gerekli geçici verilerin otomatik olarak silinmesi için özel bir ayırıcı kullanan C ++ ile yazılmış bir web sunucusu hayal edebiliyorum. Özel ayırıcı, yanıt oluşturulduktan sonra tüm geçici verileri bir kerede serbest bırakabilir.

Özel bir ayırıcı (kullandığım) için bir başka olası kullanım durumu, bir işlevin davranışının girdisinin bir kısmına bağlı olmadığını kanıtlamak için bir birim testi yazmaktır. Özel ayırıcı, bellek bölgesini herhangi bir desenle doldurabilir.


5
İlk örnek, tahsisatörün işi değil, yıkıcının işi gibi görünüyor.
Michael Dorst

2
Öbekteki belleğin ilk içeriğine bağlı olarak programınızdan endişe ediyorsanız, valgrind'de hızlı bir şekilde (yani bir gecede!) Çalıştırmak size şu ya da bu şekilde bilgi verecektir.
cdyson37

3
@anthropomorphic: Yıkıcı ve özel ayırıcı birlikte çalışır, önce yıkıcı çalışır, daha sonra henüz ücretsiz (...) çağırmayacak, ancak ücretsiz (...) olarak adlandırılacak olan özel ayırıcıyı siler daha sonra, hizmet sunulduğunda bitmiştir. Bu, varsayılan ayırıcıdan daha hızlı olabilir ve adres alanı parçalanmasını azaltabilir.
Puan

8

GPU'lar veya diğer ortak işlemcilerle çalışırken bazen ana yapıdaki veri yapılarını özel bir şekilde tahsis etmek faydalı olabilir . Bellek ayırmanın bu özel yolu , uygun bir şekilde özel bir ayırıcıda uygulanabilir.

Hızlandırıcıları kullanırken özel hızlandırıcı aracılığıyla özel ayırmanın yararlı olmasının nedeni şudur:

  1. Özel ayırma yoluyla hızlandırıcı çalışma zamanı veya sürücüye bellek bloğu bildirilir
  2. ek olarak işletim sistemi, ayrılan bellek bloğunun sayfa kilitli olduğundan emin olabilir (bazıları bu sabitlenmiş belleği çağırır ), yani işletim sisteminin sanal bellek alt sistemi sayfayı bellek içinde veya bellekten kaldıramayabilir
  3. 1. ve 2. bekletme ve sayfa kilitli bir bellek bloğu ile hızlandırıcı arasında veri aktarımı isteniyorsa, çalışma zamanı nerede olduğunu bildiği için ana bellekteki verilere doğrudan erişebilir ve işletim sisteminin emin olmadığından emin olabilir taşı / kaldır
  4. bu, sayfa kilitli olmayan bir şekilde tahsis edilen bellekle oluşacak bir bellek kopyasını kaydeder: Verilerin ana bellekte hızlandırıcıyla sayfa kilitli bir hazırlama alanına kopyalanması gerekir (DMA aracılığıyla) )

1
... sayfa hizalanmış bellek bloklarını unutmamak için Bu, özellikle bir sürücü ile konuşuyorsanız (yani DMA aracılığıyla FPGA'larla) ve DMA dağılım listeleriniz için sayfa içi ofsetleri hesaplamanın zorluğunu ve ek yükünü istemiyorsanız kullanışlıdır.
Ocak

7

Burada özel ayırıcılar kullanıyorum; hatta o iş oldu diyebilirsiniz etrafında diğer özel dinamik bellek yönetimi.

Arka plan: malloc, calloc, free ve operatörün çeşitli ve yeni değişkenleri için aşırı yüklemelerimiz var ve bağlayıcı mutlu bir şekilde STL'nin bunları bizim için kullanmasını sağlıyor. Bu, otomatik küçük nesne havuzlaması, sızıntı tespiti, tahsis dolgusu, serbest dolum, nöbetçilerle dolgu tahsisi, belirli alaşımlar için önbellek hizalama ve gecikmeli serbest bırakma gibi şeyleri yapmamızı sağlar.

Sorun şu ki, gömülü bir ortamda çalışıyoruz - kaçak tespit muhasebesini uzun bir süre boyunca doğru şekilde yapmak için yeterli bellek yok. En azından standart RAM'de değil - özel tahsis fonksiyonları ile başka bir yerde başka bir RAM yığını var.

Çözüm: Genişletilmiş yığını kullanan özel bir ayırıcı yazın ve yalnızca bellek sızıntısı izleme mimarisinin iç kısımlarında ... Geri kalan her şey varsayılan olarak sızıntı izleme yapan normal yeni / silme aşırı yüklerini içerir. Bu, izleyicinin kendisini izlemesini önler (ve biraz ekstra paketleme işlevi de sağlar, izleyici düğümlerinin boyutunu biliyoruz).

Bunu, aynı nedenden ötürü, işlev maliyeti profili verilerini tutmak için de kullanırız; her fonksiyon çağrısı ve dönüşü için bir giriş yazmak ve iplik anahtarları pahalıya mal olabilir. Özel ayırıcı bize daha büyük bir hata ayıklama bellek alanında daha küçük alaşımlar verir.


5

Programımın bir bölümündeki ayırma / ayırma sayısını saymak ve ne kadar sürdüğünü ölçmek için özel bir ayırıcı kullanıyorum. Bunu başarmanın başka yolları da var ama bu yöntem benim için çok uygun. Özel ayırıcıyı yalnızca kaplarımın bir alt kümesi için kullanabilmem özellikle yararlıdır.


4

Önemli bir durum: Modül (EXE / DLL) sınırları boyunca çalışması gereken kod yazarken, ayırma ve silme işlemlerinizi tek bir modülde yapmak önemlidir.

Burada karşılaştığım Windows üzerinde bir Eklenti mimarisi vardı. Örneğin, bir std :: dizesini DLL sınırı boyunca geçirirseniz, dizenin yeniden tahsislerinin kaynaklandığı yığından, farklı olabilecek DLL'deki yığından * oluşmaması önemlidir.

* Aslında bundan daha karmaşıktır, sanki CRT'ye dinamik olarak bağlanmışsanız, bu yine de işe yarayabilir. Ancak her DLL'in CRT'ye statik bir bağlantısı varsa, fantom tahsis hatalarının sürekli olarak meydana geldiği bir acı dünyasına gidiyorsunuz.


Nesneleri DLL sınırları üzerinden geçirirseniz, her iki taraf için Çok iş parçacıklı (Hata Ayıklama) DLL (/ MD (d)) ayarını kullanmanız gerekir. C ++ modül desteği göz önünde bulundurularak tasarlanmamıştır. Alternatif olarak, COM arabirimlerinin arkasındaki her şeyi koruyabilir ve CoTaskMemAlloc kullanabilirsiniz. Belirli bir derleyiciye, STL'ye veya satıcıya bağlı olmayan eklenti arabirimlerini kullanmanın en iyi yolu budur.
gast128

Yaşlı adamlar bunun için hüküm sürüyor: Yapma. DLL API'sinde STL türlerini kullanmayın. Dinamik API'sız sınırlamaları DLL API sınırları boyunca geçirmeyin. C ++ ABI yok - bu yüzden her DLL'i bir C API olarak ele alırsanız, potansiyel sorunların bir sınıfından kaçının. Tabii ki, "c ++ güzellik" pahasına. Veya diğer yorumun önerdiği gibi: COM kullanın. Sadece düz C ++ kötü bir fikirdir.
BitTickler

3

Bunları kullandığım zamanların bir örneği, çok kaynak kısıtlı gömülü sistemlerle çalışmaktı. Diyelim ki 2k koç ücretsiz ve programınız bu hafızanın bir kısmını kullanmak zorunda. 4-5 dizileri yığın üzerinde olmayan bir yerde saklamanız gerekiyor ve ayrıca bu şeylerin depolandığı yere çok hassas bir şekilde erişmeniz gerekiyor, bu kendi bölücünüzü yazmak isteyebileceğiniz bir durum. Varsayılan uygulamalar belleği parçalayabilir, yeterli belleğiniz yoksa ve programınızı yeniden başlatamazsanız bu kabul edilemez.

Üzerinde çalıştığım bir proje AVR-GCC'yi düşük güçlü bazı yongalarda kullanmaktı. Değişken uzunlukta 8 dizi saklamak zorunda kaldık, ancak bilinen bir maksimum değerdi. Bellek yönetiminin standart kütüphane uygulamasımalloc / free etrafında, tahsis edilen her bellek bloğunu, tahsis edilen bellek parçasının sonunu geçecek şekilde bir işaretçi ile ekleyerek öğelerin nereye yerleştirileceğini izleyen ince bir pakettir. Yeni bir bellek parçası tahsis edilirken, standart ayırıcı, istenen bellek boyutunun sığacağı bir sonraki bloğu bulmak için bellek parçalarının her birinin üzerinden geçmelidir. Bir masaüstü platformunda bu, bu birkaç öğe için çok hızlı olacaktır, ancak bu mikrodenetleyicilerin bazılarının karşılaştırıldığında çok yavaş ve ilkel olduğunu unutmayın. Ayrıca, bellek parçalanması sorunu, gerçekten farklı bir yaklaşım benimsemekten başka seçeneğimiz olmadığı anlamına gelen büyük bir sorundu.

Yaptığımız şey kendi hafıza havuzumuzu uygulamaktı . Her bellek bloğu, içinde ihtiyacımız olan en büyük diziye uyacak kadar büyüktü. Bu, önceden sabit boyutlu bellek blokları tahsis etti ve şu anda hangi bellek bloklarının kullanımda olduğunu işaretledi. Bunu, belirli bir blok kullanıldığında her bitin temsil edildiği bir 8 bit tam sayı tutarak yaptık. Tüm işlemi daha hızlı hale getirmeye çalışmak için bellek kullanımını burada değiştirdik, bu da bu mikrodenetleyici yongasını maksimum işleme kapasitesine yakınlaştırdığımız için haklıydı.

Gömülü sistemler bağlamında kendi özel bölücünüzü yazarken görebildiğim birkaç başka zaman var, örneğin dizinin hafızası bu platformlarda olduğu gibi ana koçta değilse .



2

Paylaşılan hafıza için sadece kap kafasının değil, içerdiği verilerin de paylaşılan hafızada saklanması hayati önem taşır.

Boost :: Interprocess'in ayırıcısı iyi bir örnektir. Ancak, burada okuyabileceğiniz gibi, bu allon yeterli değildir, tüm STL kaplarını paylaşılan bellek uyumlu hale getirmek için (Farklı işlemlerde farklı eşleme ofsetleri nedeniyle, işaretçiler "kırılabilir").


2

Bir süre önce bu çözümü benim için çok yararlı buldum: STL kapları için hızlı C ++ 11 ayırıcı . VS2017 (~ 5x) ve GCC (~ 7x) üzerinde STL kaplarını biraz hızlandırır. Bellek havuzuna dayalı özel amaçlı bir ayırıcıdır. STL konteynırları ile sadece istediğiniz mekanizma sayesinde kullanılabilir.


1

Kişisel olarak Loki :: Allocator / SmallObject'i küçük nesneler için bellek kullanımını optimize etmek için kullanıyorum - orta derecede küçük nesnelerle (1 ila 256 bayt) çalışmak zorundaysanız iyi verimlilik ve tatmin edici performans gösterir. Birçok farklı boyuttaki küçük nesnelerin orta miktarlarını tahsis etmekten bahsedersek, standart C ++ yeni / silme tahsisinden ~ 30 kat daha verimli olabilir. Ayrıca, "QuickHeap" olarak adlandırılan VC'ye özgü bir çözüm var, mümkün olan en iyi performansı getiriyor (tahsis ve dağıtma işlemleri, sırasıyla 99'a kadar, ayrılan / yığına döndürülen bloğun adresini okuyup yazıyor. (9)% vaka - ayarlara ve başlatmaya bağlıdır), ancak dikkate değer bir ek maliyetle - her bir yeni bellek bloğu için her bir uzantı için iki noktaya ve bir ekstraa ihtiyaç duyar. O'

Standart C ++ yeni / silme uygulaması ile ilgili sorun genellikle C malloc / free tahsisi için bir sarıcı ve 1024+ bayt gibi daha büyük bellek blokları için iyi çalışmasıdır. Performans açısından dikkate değer bir ek yüke ve bazen haritalama için kullanılan fazladan belleğe sahiptir. Bu nedenle, çoğu durumda özel ayırıcılar, performansı en üst düzeye çıkarmak ve / veya küçük (≤1024 bayt) nesneleri ayırmak için gereken ek bellek miktarını en aza indirecek şekilde uygulanır.


1

Bir grafik simülasyonunda,

  1. std::allocatorDoğrudan desteklemeyen hizalama kısıtlamaları .
  2. Kısa ömürlü (sadece bu çerçeve) ve uzun ömürlü tahsisler için ayrı havuzlar kullanarak parçalanmanın en aza indirilmesi.
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.