FIFO için hangi STL kabını kullanmalıyım?


93

Hangi STL kabı ihtiyaçlarımı en iyi şekilde karşılar? Temelde , en eski öğeyi (yaklaşık bir milyon kez) oluştururken içinde sürekli olarak push_backyeni öğeler içeren 10 öğe genişliğinde bir kabım var pop_front.

Şu anda bir kullanıyorum std::dequegörev için ancak eğer merak std::listBen kendisini yeniden tahsis etmek gerek olmazdı çünkü daha verimli olacaktır (ya da belki bir karıştırıyorsun ediyorum std::dequebir için std::vector?). Yoksa ihtiyacım için daha verimli bir konteyner var mı?

PS rastgele erişime ihtiyacım yok


5
İhtiyacınız için hangisinin daha hızlı olduğunu görmek için neden ikisini birden denemiyorsunuz?
KTC

5
Bunu yapmak üzereydim ama teorik bir cevap da arıyordum.
Gab Royer

2
std::dequeyeniden tahsis olmaz. Bu bir melez olduğunu std::listve bir std::vectoro daha büyük boyutta ayırır std::listancak böyle yeniden tahsis olmaz std::vector.
Matt Price

2
Hayır, standarttan ilgili garanti şu şekildedir: "Bir dizinin başına veya sonuna tek bir öğe eklemek her zaman sabit zaman alır ve T'nin kopya oluşturucusuna tek bir çağrıya neden olur."
Matt Price

1
@John: Hayır, yine ayırıyor. Belki sadece şartları karıştırıyoruzdur. Bence yeniden tahsis, eski tahsisi almak, yeni bir tahsise kopyalamak ve eskisini atmak anlamına geliyor.
GManNickG

Yanıtlar:


198

Sayısız cevap olduğu için kafanız karışabilir, ancak özetlemek gerekirse:

Bir std::queue. Bunun nedeni basit: bir FIFO yapısıdır. FIFO istiyorsanız, bir std::queue.

Niyetinizi başkalarına ve hatta kendinize açık hale getirir. A std::listya std::dequeda değil. Bir liste herhangi bir yere eklenebilir ve kaldırılabilir, ki bu bir FIFO yapısının yapması gereken şey değildir ve bir dequeuçtan bir ekleyip çıkarabilir, ki bu da bir FIFO yapısının yapamayacağı bir şeydir.

Bu nedenle bir queue.

Şimdi, performansı sordun. İlk olarak, her zaman şu önemli kuralı hatırlayın: Önce iyi kod, sonra performans.

Bunun nedeni basit: Temizlik ve şıklıktan önce performans için çabalayan insanlar neredeyse her zaman en son bitiriyor. Kodları bir lapa haline gelir, çünkü ondan gerçekten hiçbir şey elde etmek için iyi olan her şeyi terk etmişlerdir.

Önce iyi, okunabilir kod yazarak, çoğunuz performans problemlerini kendiliğinden çözecektir. Ve daha sonra performansınızın yetersiz olduğunu fark ederseniz, artık güzel, temiz kodunuza bir profilleyici eklemek ve sorunun nerede olduğunu bulmak çok kolay.

Hepsi söylendi, std::queuesadece bir adaptör. Güvenli arayüz sağlar, ancak içeride farklı bir konteyner kullanır. Bu temel kapsayıcıyı seçebilirsiniz ve bu büyük ölçüde esneklik sağlar.

Peki hangi temel kapsayıcıyı kullanmalısınız? Bunu biliyoruz std::listve std::dequehem gerekli işlevleri sağlamak ( push_back(), pop_front()ve front()), bu yüzden nasıl karar vereceğiz?

İlk olarak, bellek ayırmanın (ve ayırmanın) genellikle yapılacak hızlı bir şey olmadığını anlayın, çünkü bu, işletim sistemine gidip ondan bir şey yapmasını istemeyi içerir. A list, her bir şey eklendiğinde bellek ayırmalı ve gittiğinde onu serbest bırakmalıdır.

dequeÖte yandan A , parçalar halinde tahsis eder. A'dan daha az sıklıkta ayıracaktır list. Bunu bir liste olarak düşünün, ancak her bellek parçası birden fazla düğümü tutabilir. (Elbette, nasıl çalıştığını gerçekten öğrenmenizi öneririm .)

Yani tek başına dequebununla daha iyi performans göstermelidir çünkü bellekle pek sık ilgilenmez. Sabit büyüklükteki verileri işlediğiniz gerçeğiyle karıştırıldığında, muhtemelen verilerden ilk geçişten sonra ayırmak zorunda kalmayacak, oysa bir liste sürekli olarak tahsis edecek ve serbest bırakacaktır.

Anlaşılması gereken ikinci bir şey de önbellek performansı . RAM'e çıkmak yavaştır, bu nedenle CPU gerçekten ihtiyaç duyduğunda, bir yığın belleği önbelleğe geri alarak bu zamandan en iyi şekilde yararlanır. dequeBellek yığınlarında bir ayırma olduğu için, bu kapsayıcıdaki bir öğeye erişmenin CPU'nun kabın geri kalanını da geri getirmesine neden olması muhtemeldir. Şimdi deque, veri önbellekte olduğu için başka erişim hızlı olacaktır.

Bu, verilerin teker teker tahsis edildiği bir listeden farklıdır. Bu, verilerin belleğin her yerine yayılabileceği ve önbellek performansının kötü olacağı anlamına gelir.

Bu nedenle, bunu göz önünde bulundurarak, dequea daha iyi bir seçim olmalıdır. Bu nedenle, bir queue. Tüm bunlarla birlikte, bu hala sadece (çok) eğitimli bir tahmin: Bu kodun profilini, dequebir testte a kullanarak ve listdiğerinde gerçekten kesin olarak bilmek zorunda kalacaksın .

Ancak unutmayın: kodu temiz bir arayüzle çalıştırın, ardından performans konusunda endişelenin.

John, a listveya sarmanın dequeperformans düşüşüne neden olacağı endişesini dile getiriyor . Bir kez daha, kendisinin profilini çıkarmadan kesin olarak söyleyemeyiz, ancak derleyicinin yaptığı çağrıları satır içi yapması ihtimali vardır queue. Yani, dediğinizde queue.push(), gerçekten sadece queue.container.push_back()işlev çağrısını tamamen atlayarak diyecektir .

Bir kez daha, bu yalnızca eğitimli bir tahmindir, ancak queuetemeldeki kapsayıcı ham kullanımına kıyasla a kullanmak performansı düşürmez. Daha önce de söylediğim gibi, queuetemiz, kullanımı kolay ve güvenli olduğu için ve gerçekten bir sorun profili ve testi haline gelirse, kullanın.


10
+1 - ve eğer boost :: round_buffer <> 'in en iyi performansa sahip olduğu ortaya çıkarsa, o zaman bunu temel kap olarak kullanın (aynı zamanda gerekli push_back (), pop_front (), front () ve back () ).
Michael Burr

2
Ayrıntılı olarak açıklamak için kabul edildi (ihtiyacım olan şey bu, zaman ayırdığınız için teşekkürler). Son olarak iyi kod ilk performansına gelince, bunun en büyük varsayılanımdan biri olduğunu kabul etmeliyim, her zaman ilk çalıştırmada her şeyi mükemmel bir şekilde yapmaya çalışırım ... Kodu ilk başta zor bir şekilde yazdım, ama bu işe yaramadığı için Düşündüğüm kadar iyi performans sergilemek (neredeyse gerçek zamanlı olması gerekiyordu), onu biraz geliştirmem gerektiğini tahmin ettim. Neil'in de dediği gibi, gerçekten bir profil oluşturucu kullanmalıydım ... Gerçi bu hataları şimdi yaptığım için mutlu olsam da önemli değil. Hepinize çok teşekkürler.
Gab Royer

4
-1 problemi çözmemek ve şişirilmiş faydasız cevap için. Buradaki doğru cevap kısadır ve boost :: round_buffer <>.
Dmitry Chichkov

1
"Önce iyi kod, sonra performans", bu harika bir alıntı. Keşke herkes bunu anlasaydı :)
thegreendroid

Profil oluşturmadaki stresi takdir ediyorum. Pratik bir kural sağlamak bir şeydir ve bunu profilleme ile kanıtlamak daha iyidir
talekeDskobeDa

28

Kontrol edin std::queue. Temel bir kapsayıcı türünü sarar ve varsayılan kapsayıcı std::deque.


3
Her ekstra katman olacak derleyici tarafından giderilmelidir. Sizin mantığınıza göre, dil sadece araya giren bir kabuk olduğundan, hepimiz sadece assembly'de programlama yapmalıyız. Önemli olan, iş için doğru türü kullanmaktır. Ve queuebu türden. Önce iyi kod, sonra performans. Cehennem, çoğu performans ilk etapta iyi kod kullanmaktan kaynaklanır.
GManNickG

2
Muğlak olduğum için üzgünüm - demek istediğim, sıranın tam olarak sorduğu şey olmasıydı ve C ++ tasarımcıları deque'in bu kullanım durumu için iyi bir temel kapsayıcı olduğunu düşünüyorlardı.
Mark Ransom

2
Bu soruda performansı yetersiz bulduğunu gösteren hiçbir şey yok. Yeni başlayanların çoğu, mevcut çözümlerinin kabul edilebilir şekilde çalışıp çalışmadığına bakılmaksızın, herhangi bir soruna en verimli çözümü soruyor.
jalf

1
@John, performansın eksik olduğunu fark etseydi, dediğim gibi, güvenlik kabuğunu sıyırmak performansı queueartırmazdı. Önerilen bir listolasılıkla daha kötü yürütecek olan.
GManNickG

3
Std :: queue <> ile ilgili olan şey, deque <> istediğiniz şey değilse (performans için veya herhangi bir nedenle), onu destek deposu olarak std :: list kullanmak için değiştirmek tek satırlık bir işlemdir - GMan geri dönüş dedi. Ve eğer gerçekten bir liste yerine bir halka tamponu kullanmak istiyorsanız, boost :: round_buffer <> hemen ... std :: queue <> içine düşecektir, neredeyse kesinlikle kullanılması gereken 'arayüz'dür. Bunun için destek deposu irade ile hemen hemen değiştirilebilir.
Michael Burr


7

En eski öğeyi oluştururken (yaklaşık bir milyon kez) sürekli olarak push_backyeni öğeler pop_front.

Hesaplamada bir milyon gerçekten büyük bir sayı değildir. Başkalarının da önerdiği std::queuegibi, ilk çözümünüz olarak a kullanın . Çok yavaş olması beklenmedik bir durumda, bir profil oluşturucu kullanarak darboğazı tanımlayın (tahmin etmeyin!) Ve aynı arayüze sahip farklı bir kapsayıcı kullanarak yeniden uygulayın.


1
Sorun şu ki, yapmak istediğim şeyin gerçek zamanlı olması gerektiği için bu büyük bir sayı. Haklı olmanıza rağmen sebebini belirlemek için bir profil oluşturucu kullanmalıydım ...
Gab Royer

Mesele şu ki, profil oluşturucu kullanmaya pek alışkın değilim (sınıflarımızdan birinde biraz gprof kullandık ama gerçekten derinlemesine çalışmadık ...). Beni bazı kaynaklara yönlendirirseniz, çok memnun olurum! PS. VS2008 kullanıyorum
Gab Royer

@Gab: Hangi VS2008'e sahipsiniz (Ekspres, Pro ...)? Bazıları bir profilleyici ile birlikte gelir.
sbi

@Gab Üzgünüm, artık VS kullanmıyorum, bu yüzden gerçekten tavsiye edemem

@Sbi, gördüğüm kadarıyla, sadece takım sistemi sürümünde (erişebildiğim). Ben bununla ilgileneceğim.
Gab Royer

5

Neden olmasın std::queue? Sahip olduğu tek şey push_backve pop_front.


3

Bir kuyruk muhtemelen daha basit bir arayüz deque ama böyle küçük bir listesi için, performans farkı muhtemelen önemsiz.

Aynı liste için de geçerli . Bu, istediğiniz API'ye bağlı.


Ancak sürekli geri itmenin kuyruğu mu oluşturduğunu yoksa kendilerini yeniden tahsis mi ettiğini merak ediyordum
Gab Royer

std :: queue, başka bir konteynerin etrafındaki bir sarmalayıcıdır, bu nedenle bir diziyi saran bir kuyruk, bir ham deque'ten daha az verimli olacaktır.
John Millikin

1
10 öğe için, performans büyük olasılıkla o kadar küçük bir sorun olacak ki, "verimlilik" kod zamanından çok programcı zamanında ölçülebilir. Ve herhangi bir düzgün derleyici optimizasyonu tarafından kuyruktan sıraya geçiş çağrıları sıfıra iner.
lavinio

2
@John: Bana böyle bir performans farkını gösteren bir dizi kıyaslama göstermenizi istiyorum. Ham bir diziden daha az verimli değildir. C ++ derleyicileri çok agresif bir şekilde satır içi.
jalf

3
Ben denedim. : VC9'da hız için Sürüm derlemesinde 100.000.000 pop_front () ve push_back () rand () int sayıları içeren DA hızlı ve kirli 10 elemanlı konteyner şunları verir: list (27), queue (6), deque (6), array (8) .
KTC

0

A kullanın std::queue, ancak iki standart Containersınıfın performans ödünleşmelerinin farkında olun .

Varsayılan olarak, std::queueüstünde bir adaptördür std::deque. Tipik olarak, çok sayıda giriş içeren az sayıda kuyruğa sahip olduğunuzda iyi performans sağlar ki bu muhtemelen yaygın bir durumdur.

Bununla birlikte, std :: deque uygulamasına kör olmayın . Özellikle:

"... dekorlar tipik olarak büyük minimum bellek maliyetine sahiptir; sadece bir öğeyi tutan bir geri çekilme, tüm dahili dizisini tahsis etmek zorundadır (örneğin, 64 bit libstdc ++ 'da nesne boyutunun 8 katı; nesne boyutunun 16 katı veya 4096 bayt, hangisi daha büyükse , 64 bit libc ++ üzerinde). "

Bunu netleştirmek için, bir kuyruk girişinin sıraya koymak isteyeceğiniz bir şey olduğunu varsayarsak, yani boyut olarak oldukça küçükse, her biri 30.000 giriş içeren 4 kuyruğunuz varsa, std::dequeuygulama tercih edilen seçenek olacaktır. Tersine, her biri 4 giriş içeren 30.000 kuyruğunuz varsa , bu senaryoda ek yükü std::listasla amorti etmeyeceğiniz için uygulama büyük olasılıkla optimal olacaktır std::deque.

Önbelleğin ne kadar kral olduğu, Stroustrup'un bağlantılı listelerden nasıl nefret ettiği vb. Hakkında birçok fikir okuyacaksınız ve bunların hepsi belirli koşullar altında doğru. Sadece körü körüne inanarak kabul etmeyin, çünkü oradaki ikinci senaryomuzda, varsayılan std::dequeuygulamanın çalışması pek olası değildir . Kullanımınızı değerlendirin ve ölçün.


-1

Bu dava yeterince basittir ki kendi yazınızı yazabilirsiniz. STL kullanımının çok fazla yer kapladığı mikro denetleyici durumları için iyi çalışan bir şey burada. Veri ve sinyali kesme işleyicisinden ana döngünüze iletmenin güzel bir yolu.

// FIFO with circular buffer
#define fifo_size 4

class Fifo {
  uint8_t buff[fifo_size];
  int writePtr = 0;
  int readPtr = 0;
  
public:  
  void put(uint8_t val) {
    buff[writePtr%fifo_size] = val;
    writePtr++;
  }
  uint8_t get() {
    uint8_t val = NULL;
    if(readPtr < writePtr) {
      val = buff[readPtr%fifo_size];
      readPtr++;
      
      // reset pointers to avoid overflow
      if(readPtr > fifo_size) {
        writePtr = writePtr%fifo_size;
        readPtr = readPtr%fifo_size;
      }
    }
    return val;
  }
  int count() { return (writePtr - readPtr);}
};

Ama bu nasıl / ne zaman olacak?
user10658782

Oh, bir nedenden dolayı olabileceğini düşündüm. Boşver!
Ry-
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.