İyi bir C Değişken Uzunluk Dizisi örneği [kapalı]


9

Bu soru so so dondurucu bir resepsiyon var, bu yüzden orada silmek ve bunun yerine burada deneyin karar verdi. Buraya da uygun olmadığını düşünüyorsanız, lütfen en azından peşinde olduğum bir örneği nasıl bulacağınıza dair bir yorum bırakın ...

C99 VLA'ları kullanmanın, mevcut standart yığın kullanan C ++ RAII mekanizmalarına göre gerçek bir avantaj sunduğu bir örnek verebilir misiniz ?

Sonra olduğum örnek:

  1. Yığın kullanımına göre kolayca ölçülebilir (% 10 belki) bir performans avantajı elde edin.
  2. Tüm diziye hiç ihtiyaç duymayacak iyi bir geçici çözüm yoktur.
  3. Aslında sabit maksimum boyut yerine dinamik boyuttan yararlanın.
  4. Normal kullanım senaryosunda yığın taşmasına neden olma olasılığı düşüktür.
  5. Bir C ++ projesine bir C99 kaynak dosyası eklemek için performansa ihtiyaç duyan bir geliştiriciyi cezbedecek kadar güçlü olun.

Bağlam hakkında bazı açıklamalar ekleyerek: C99 ile kastedilen ve standart C ++ 'da yer almayan VLA'yı kastediyorum: int array[n]burada nbir değişkendir. Ve ben diğer standartlar (C90, C ++ 11) tarafından sunulan alternatifler koyar kullanım örneği bir örnek sonra:

int array[MAXSIZE]; // C stack array with compile time constant size
int *array = calloc(n, sizeof int); // C heap array with manual free
int *array = new int[n]; // C++ heap array with manual delete
std::unique_ptr<int[]> array(new int[n]); // C++ heap array with RAII
std::vector<int> array(n); // STL container with preallocated size

Bazı fikirler:

  • Doğal olarak öğe sayımını makul bir şeyle sınırlayan varargs alan işlevler, herhangi bir yararlı API düzeyinde üst sınır içermez.
  • Atık israfının istenmeyen olduğu özyinelemeli fonksiyonlar
  • Yığın yükünün kötü olacağı birçok küçük tahsis ve sürüm.
  • Performansın kritik olduğu ve küçük işlevlerin çok fazla satır içi olması beklenen çok boyutlu dizileri (rastgele boyutlandırılmış matrisler gibi) ele almak.
  • Yorumdan: yığın tahsisinin senkronizasyon yükü olduğu eşzamanlı algoritma .

Wikipedia'nın kriterlerimi karşılamayan bir örneği var , çünkü öbek kullanmanın pratik farkı en azından bağlamsız olarak ilgisiz görünüyor. Ayrıca ideal değildir, çünkü daha fazla bağlam olmadan, öğe sayısı çok iyi yığın taşmasına neden olabilir gibi görünüyor.

Not: Özellikle bir örnek kod veya bundan yarar sağlayacak bir algoritma önerisi sonrasında, örneği kendim uygulamak için kullanıyorum.


1
Biraz spekülatif (bu bir çivi arayan bir çekiç olduğu için), ancak ikincisindeki kilit çekişmesi nedeniyle belki alloca()de çok malloc()iş parçacıklı bir ortamda gerçekten gölgede kalır . Ancak bu, küçük dizilerin yalnızca sabit bir boyut kullanması gerektiği için büyük bir dizidir ve büyük dizilerin muhtemelen yığınlara ihtiyacı olacaktır.
chrisaycock

1
@chrisaycock Evet, çok fazla çekiç çivi arıyor, ama aslında var olan bir çekiç ( allocatemelde aynı şey olduğunu düşündüğüm C99 VLA veya aslında herhangi bir standart değil ). Ama bu çok iş parçacıklı şey, onu dahil etmek için soru düzenleme iyi!
hyde

VLA'ların bir dezavantajı, bir tahsis hatasını tespit etmek için herhangi bir mekanizma olmamasıdır; yeterli bellek yoksa, davranış tanımsızdır. (Aynı durum sabit boyutlu diziler için - ve alloca () için de geçerlidir.)
Keith Thompson

@KeithThompson Eh, malloc / new'in ayırma hatasını tespit edeceğine dair bir garanti yoktur, örneğin Linux malloc man notları sayfasına bakınız ( linux.die.net/man/3/malloc ).
hyde

@hyde: Ve Linux'un mallocdavranışının C standardına uyup uymadığı tartışmalıdır .
Keith Thompson

Yanıtlar:


9

"Adil" ve "karşılaştırılabilir" olduğundan emin olmak için her seferinde aynı tohumda yeniden başlayan bir dizi rastgele sayı üreten küçük bir programı hackledim. İlerledikçe, bu değerlerin min ve maks'lerini bulur. O sayıların kümesini üretti ve çıktığında da her ortalama üstünde kaç sayar minve max.

ÇOK küçük diziler için, VLA'lar bittiğinde açık bir fayda sağlar std::vector<>.

Bu gerçek bir sorun değil, ancak rastgele sayılar kullanmak yerine küçük bir dosyadaki değerleri okuduğumuz ve aynı tür kodlarla daha anlamlı sayım / dak / maks hesaplamaları yaptığımız bir şeyi kolayca hayal edebiliyoruz. .

İlgili işlevlerdeki "rastgele sayıların sayısı (x) 'nın ÇOK küçük değerleri için, vlaçözüm büyük bir farkla kazanır. Boyut büyüdükçe, "kazanç" küçüldükçe ve yeterli boyut verildiğinde, vektör çözümü DAHA VERİMLİ gibi görünmektedir - bir VLA'da binlerce öğeye sahip olmaya başladığımızda olduğu gibi, bu varyantı çok fazla çalışmadı, gerçekten ne yapmaları gerekiyordu ...

Ve eminim birisi bana tüm bu kodu bir sürü şablonla yazmanın ve RDTSC ve coutbitlerden daha fazla çalıştırmadan bunu yapmanın bir yolu olduğunu söyleyecektir ... Ama bunun gerçekten olduğunu düşünmüyorum nokta.

Bu özel varyantı çalıştırırken, func1(VLA) ve func2(std :: vector) arasında yaklaşık% 10 fark elde ederim .

count = 9884
func1 time in clocks per iteration 7048685
count = 9884
func2 time in clocks per iteration 7661067
count = 9884
func3 time in clocks per iteration 8971878

Bu derleme: g++ -O3 -Wall -Wextra -std=gnu++0x -o vla vla.cpp

İşte kod:

#include <iostream>
#include <vector>
#include <cstdint>
#include <cstdlib>

using namespace std;

const int SIZE = 1000000;

uint64_t g_val[SIZE];


static __inline__ unsigned long long rdtsc(void)
{
    unsigned hi, lo;
    __asm__ __volatile__ ("rdtsc" : "=a"(lo), "=d"(hi));
    return ( (unsigned long long)lo)|( ((unsigned long long)hi)<<32 );
}


int func1(int x)
{
    int v[x];

    int vmax = 0;
    int vmin = x;
    for(int i = 0; i < x; i++)
    {
        v[i] = rand() % x;
        if (v[i] > vmax) 
            vmax = v[i];
        if (v[i] < vmin) 
            vmin = v[i];
    }
    int avg = (vmax + vmin) / 2;
    int count = 0;
    for(int i = 0; i < x; i++)
    {
        if (v[i] > avg)
        {
            count++;
        }
    }
    return count;
}

int func2(int x)
{
    vector<int> v;
    v.resize(x); 

    int vmax = 0;
    int vmin = x;
    for(int i = 0; i < x; i++)
    {
        v[i] = rand() % x;
        if (v[i] > vmax) 
            vmax = v[i];
        if (v[i] < vmin) 
            vmin = v[i];
    }
    int avg = (vmax + vmin) / 2;
    int count = 0;
    for(int i = 0; i < x; i++)
    {
        if (v[i] > avg)
        {
            count++;
        }
    }
    return count;
}    

int func3(int x)
{
    vector<int> v;

    int vmax = 0;
    int vmin = x;
    for(int i = 0; i < x; i++)
    {
        v.push_back(rand() % x);
        if (v[i] > vmax) 
            vmax = v[i];
        if (v[i] < vmin) 
            vmin = v[i];
    }
    int avg = (vmax + vmin) / 2;
    int count = 0;
    for(int i = 0; i < x; i++)
    {
        if (v[i] > avg)
        {
            count++;
        }
    }
    return count;
}    

void runbench(int (*f)(int), const char *name)
{
    srand(41711211);
    uint64_t long t = rdtsc();
    int count = 0;
    for(int i = 20; i < 200; i++)
    {
        count += f(i);
    }
    t = rdtsc() - t;
    cout << "count = " << count << endl;
    cout << name << " time in clocks per iteration " << dec << t << endl;
}

struct function
{
    int (*func)(int);
    const char *name;
};


#define FUNC(f) { f, #f }

function funcs[] = 
{
    FUNC(func1),
    FUNC(func2),
    FUNC(func3),
}; 


int main()
{
    for(size_t i = 0; i < sizeof(funcs)/sizeof(funcs[0]); i++)
    {
        runbench(funcs[i].func, funcs[i].name);
    }
}

Vay canına, sistemim VLA sürümünde% 30 iyileşme gösteriyor std::vector.
chrisaycock

1
20-200 yerine yaklaşık 5-15 boyut aralığında deneyin ve muhtemelen% 1000 veya daha fazla iyileşme elde edersiniz. [Ayrıca derleyici seçeneklerine bağlıdır - gcc üzerinde derleyici seçeneklerimi göstermek için yukarıdaki kodu düzenleyeceğim]
Mats Petersson

Ben sadece yerine func3kullanır ve ihtiyacı ortadan kaldırır bir ekledi . Kullanandan yaklaşık% 10 daha uzun sürer . [Tabii ki, süreçte, kullanımının işlevin sürdüğü zamana önemli bir katkıda bulunduğunu buldum - bu konuda biraz şaşırdım]. v.push_back(rand())v[i] = rand();resize()resize()v[i]
Mats Petersson

1
@MikeBrown std::vectorVLA / kullanacak gerçek bir uygulama biliyor musunuz alloca, yoksa bu sadece spekülasyon mu?
hyde

3
Vektör gerçekten dahili olarak bir dizi kullanıyor ancak anladığım kadarıyla VLA kullanmanın bir yolu yok. Örneğim, VLA'ların veri miktarının az olduğu bazı (belki de birçok) durumda yararlı olduğunu gösterdiğine inanıyorum. Vektör VLA'ları kullanıyor olsa bile, vectoruygulama içinde ek çabadan sonra olacaktır .
Mats Petersson

0

VLA'lara karşı Vector

Bir Vector'in VLA'ların kendisinden yararlanabileceğini düşündünüz mü? VLA'lar olmadan, Vektör, depolama için 10, 100, 10000 gibi belirli dizilerin "ölçeklerini" belirtmelidir, böylece 101 öğeyi tutmak için 10000 öğe dizisi ayırırsınız. VLA'larda, 200 olarak yeniden boyutlandırırsanız, algoritma yalnızca 200'e ihtiyacınız olduğunu varsayabilir ve 200 öğe dizisi ayırabilir. Veya n * 1.5 gibi bir tampon tahsis edebilir.

Her neyse, çalışma zamanında kaç öğeye ihtiyacınız olacağını biliyorsanız, bir VLA'nın daha performanslı olduğunu (Mats'ın karşılaştırmalı testinin gösterdiği gibi) olduğunu iddia ediyorum. Gösterdiği basit iki geçiş tekrarıydı. Rastgele örneklerin tekrar tekrar alındığı monte carlo simülasyonlarını veya her elemanda birden çok kez hesaplamaların yapıldığı görüntü manipülasyonunu (Photoshop filtreleri gibi) ve her elemandaki her hesaplamanın komşulara bakmayı içerdiğini düşünün.

Vektörden dahili diziye bu ekstra işaretçi atlama eklenir.

Ana soruyu cevaplama

Ancak LinkedList gibi dinamik olarak ayrılmış bir yapı kullanmaktan bahsettiğinizde, karşılaştırma yapılmaz. Bir dizi, öğelerine işaretçi aritmetiği kullanarak doğrudan erişim sağlar. Bağlantılı bir listeyi kullanarak belirli bir öğeye ulaşmak için düğümleri yürümeniz gerekir. Böylece VLA bu senaryoda eller kazanır.

Bu cevaba göre , mimari olarak bağımlıdır, ancak bazı durumlarda yığının önbellekte bulunması nedeniyle yığına bellek erişimi daha hızlı olacaktır. Çok sayıda elemanla bu ihmal edilebilir (potansiyel olarak Mats'ın testlerinde gördüğü azalan getirilerin nedeni). Ancak, Önbellek boyutlarının önemli ölçüde büyüdüğünü ve potansiyel olarak bu sayının buna göre arttığını göreceğinizi belirtmek gerekir.


Bağlantılı listelere referansınızı anladığımdan emin değilim, bu yüzden soruya bir bölüm ekledim, bağlamı biraz daha açıkladım ve düşündüğüm alternatiflere örnekler ekledim.
hyde

Neden std::vectordizilere ihtiyaç duyulur? Sadece 101'e ihtiyaç duyduğunda neden 10K elemanları için alana ihtiyaç duysun? Ayrıca, soru asla bağlantılı listelerden bahsetmez, bu yüzden bunu nereden aldığınızdan emin değilim. Son olarak, C99'daki VLA'lar yığın tahsis edilir; onlar standart bir biçimidir alloca(). Yığın depolama (işlev döndükten sonra yaşar) veya a realloc()(dizi kendini yeniden boyutlandırır) gerektiren her şey VLA'ları yine de yasaklar.
chrisaycock

@chrisaycock C ++, hafızanın yeni [] ile ayrıldığı varsayılarak, bir nedenden dolayı realloc () işlevinden yoksundur. Std :: vector'un ölçekleri kullanmasının ana nedeni bu değil mi?

@Lundin C ++ vektörü on güçle ölçeklendirir mi? Bağlantılı liste referansı göz önüne alındığında, Mike Brown'un soru ile gerçekten karıştırıldığı izlenimini edindim. (Ayrıca C99 VLA'ların öbek üzerinde yaşadığını ima eden daha erken bir iddiada bulundu.)
chrisaycock

@hyde Bahsettiğin şey bu değildi. Diğer yığın tabanlı veri yapıları demek istediğinizi düşündüm. İlginçtir, bu açıklamayı eklediniz. Bunlar arasındaki farkı söylemek için bir C ++ geek yeterli değil.
Michael Brown

0

Bir VLA kullanmanın nedeni öncelikle performanstır. Wiki örneğini sadece "alakasız" bir farka sahip olarak göz ardı etmek bir hatadır. Kolayca bu kodun büyük bir fark olabilir, örneğin, bu işlev sıkı bir döngü çağrılırsa, read_valburada hızın kritik olduğu bir tür sistemde çok hızlı bir şekilde dönen bir IO işlevi olduğu durumlarda görebilirsiniz.

Aslında, VLA'ların bu şekilde kullanıldığı çoğu yerde, yığın çağrılarının yerine değil, bunun gibi bir şeyin yerine geçerler:

float vals[256]; /* I hope we never get more! */

Herhangi bir yerel bildirimle ilgili olan şey, son derece hızlı olmasıdır. Çizgi float vals[n]genellikle yalnızca birkaç işlemci talimatı gerektirir (belki sadece bir tane). nYığın işaretçisine değeri ekler .

Öte yandan, bir yığın tahsisi, boş bir alan bulmak için bir veri yapısının yürümesini gerektirir. Zaman muhtemelen en şanslı durumda bile daha uzun bir büyüklük sırasıdır. (Yani sadece nyığının üzerine koyma ve arama eylemi mallocmuhtemelen 5-10 talimattır.) Öbekte makul miktarda veri varsa muhtemelen çok daha kötüdür. mallocGerçek bir programda 100x ila 1000x daha yavaş olan bir durumu görmek beni hiç şaşırtmaz .

Tabii ki, o zaman eşleştirme ile de bazı performans etkileriniz var free, muhtemelen mallocçağrıya benzer büyüklükte .

Ek olarak, bellek parçalanması sorunu var. Pek çok küçük tahsis, yığını parçalama eğilimindedir. Parçalanmış, hem atık hafızayı biriktirir hem de hafızayı ayırmak için gereken süreyi artırır.


Wikipedia örneği hakkında: iyi bir örneğin bir parçası olabilir , ancak bağlam olmadan, etrafında daha fazla kod varsa, sorumun numaralandırıldığı 5 şeyden hiçbirini göstermiyor . Aksi takdirde evet, açıklamanızı kabul ediyorum. Akılda tutulması gereken bir şey olsa da: VLA'ları kullanmanın yerel değişkenlere erişim maliyeti olabilir, onlarla birlikte tüm yerel değişkenlerin ofsetleri derleme zamanında bilinmemektedir, bu nedenle bir kerelik yığın maliyetini bir her döngü için iç döngü cezası.
hyde

Um ... ne demek istediğinden emin değilim. Lokal değişken bildirimleri tek bir işlemdir ve hafifçe optimize edilmiş herhangi bir derleyici ayırmayı bir iç döngüden çeker. Yerel değişkenlere erişimde belirli bir "maliyet" yoktur, kesinlikle bir VLA'nın artacağı bir maliyet yoktur.
Robot

Somut örnek:: int vla[n]; if(test()) { struct LargeStruct s; int i; }yığın ofseti sderleme zamanında bilinmeyecektir ve derleyicinin depolamayı iiç kapsamın dışına sabit yığın ofsetine taşıyacağı şüphelidir . Dolayısı ile ekstra makine koduna ihtiyaç vardır ve bu da PC donanımında önemli olan kayıtları yiyebilir. Derleyici derleme çıktısı dahil örnek kod istiyorsanız, lütfen ayrı bir soru sorun;)
hyde

Derleyicinin kodda karşılaşılan sırayla ayrılması gerekmez ve alan ayrılıp kullanılmadığı önemli değildir. Bir akıllı optimizer , fonksiyon için girildiğinde sve igirildiğinde, daha önce testçağrılır veya vlatahsis edilirse, tahsisler olarak tahsis edilir sve iyan etkisi yoktur. (Ve aslında, ibir kayıt defterine bile yerleştirilebilir, yani hiçbir "ayırma" yoktur.) Yığındaki ayırma sırasına, hatta yığının kullanıldığına dair derleyici garantisi yoktur.
Robot

(aptalca bir hata nedeniyle yanlış olan bir yorumu sildi)
hyde
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.