Un buon esempio di array di lunghezza variabile C [chiuso]


9

Questa domanda ha avuto una ricezione piuttosto gelida a SO, quindi ho deciso di eliminarlo lì e provare invece qui. Se pensi che non si adatti neanche qui, per favore lascia almeno un commento su un suggerimento su come trovare un esempio che sto cercando ...

Puoi fare un esempio , in cui l'utilizzo di VLA C99 offre un reale vantaggio rispetto a qualcosa come gli attuali meccanismi RAII C ++ che utilizzano heap?

L'esempio che sto seguendo dovrebbe:

  1. Ottieni un vantaggio prestazionale facilmente misurabile (forse il 10%) rispetto all'uso dell'heap.
  2. Non ha una buona soluzione, che non avrebbe bisogno dell'intero array.
  3. Beneficia effettivamente dell'uso della dimensione dinamica, anziché della dimensione massima fissa.
  4. È improbabile che causi un overflow dello stack in uno scenario di utilizzo normale.
  5. Sii abbastanza forte da tentare uno sviluppatore che necessita delle prestazioni per includere un file sorgente C99 in un progetto C ++.

Aggiungendo alcuni chiarimenti sul contesto: intendo VLA come inteso da C99 e non incluso nello standard C ++: int array[n]dove nè una variabile. E sto cercando un esempio di caso d'uso in cui vince le alternative offerte da altri standard (C90, C ++ 11):

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

Qualche idea:

  • Le funzioni che assumono varargs, che naturalmente limitano il conteggio degli oggetti a qualcosa di ragionevole, tuttavia non hanno alcun limite superiore utile a livello di API.
  • Funzioni ricorsive, dove lo stack sprecato è indesiderabile
  • Molte piccole allocazioni e rilasci, in cui l'heap overhead sarebbe male.
  • Gestire array multidimensionali (come matrici di dimensioni arbitrarie), in cui le prestazioni sono fondamentali e ci si aspetta che piccole funzioni vengano incorporate molto.
  • Dal commento: algoritmo concorrente, in cui l' allocazione dell'heap ha un sovraccarico di sincronizzazione .

Wikipedia ha un esempio che non soddisfa i miei criteri , perché la differenza pratica nell'uso dell'heap sembra irrilevante almeno senza contesto. È anche non ideale, perché senza più contesto, sembra che il conteggio degli oggetti potrebbe causare un overflow dello stack.

Nota: sto specificatamente cercando un codice di esempio o un suggerimento di un algoritmo che ne trarrebbe beneficio, per me stesso per implementare l'esempio.


1
Un po 'speculativo (dal momento che questo è un martello in cerca di un chiodo), ma forse alloca()sarebbe davvero eclissare malloc()in un ambiente multithread a causa della contesa di blocco in quest'ultimo . Ma questo è un vero allungamento poiché i piccoli array dovrebbero usare solo una dimensione fissa e gli array di grandi dimensioni probabilmente avranno comunque bisogno dell'heap.
chrisaycock,

1
@chrisaycock Sì, moltissimo martello in cerca di un chiodo, ma un martello che esiste realmente (sia esso C99 VLA o il non-in-qualsiasi-standard alloca, che penso siano sostanzialmente la stessa cosa). Ma quella cosa multithread è buona, modificando la domanda per includerla!
hyde,

Uno svantaggio dei VLA è che non esiste alcun meccanismo per rilevare un errore di allocazione; se non c'è memoria sufficiente, il comportamento non è definito. (Lo stesso vale per gli array di dimensioni fisse - e per alloca ().)
Keith Thompson

@KeithThompson Beh, non esiste alcuna garanzia che malloc / new rilevi un errore di allocazione, per esempio vedere la pagina man di Notes for Linux malloc ( linux.die.net/man/3/malloc ).
hyde,

@hyde: Ed è discutibile se il malloccomportamento di Linux sia conforme allo standard C.
Keith Thompson,

Risposte:


9

Ho appena creato un piccolo programma che genera ogni volta un numero di numeri casuali che si riavvia allo stesso seme, per assicurarmi che sia "giusto" e "comparabile". Mentre procede, capisce il minimo e il massimo di questi valori. E quando ha generato l'insieme di numeri, conta quanti sono al di sopra della media di mine max.

Per array MOLTO piccoli, mostra un chiaro vantaggio con VLA std::vector<>.

Non è un vero problema, ma possiamo facilmente immaginare qualcosa in cui leggeremmo i valori da un piccolo file invece di usare numeri casuali e fare qualche altro calcolo più significativo / conteggio / min / max con lo stesso tipo di codice .

Per valori MOLTO piccoli del "numero di numeri casuali" (x) nelle funzioni pertinenti, la vlasoluzione vince con un margine enorme. Man mano che le dimensioni aumentano, la "vincita" si riduce e, data la dimensione sufficiente, la soluzione vettoriale sembra PIÙ efficiente - non ha studiato quella variante troppo, come quando iniziamo a includere migliaia di elementi in un VLA, non lo è davvero quello che dovevano fare ...

E sono sicuro che qualcuno mi dirà che c'è un modo di scrivere tutto questo codice con un sacco di modelli e farlo farlo senza eseguire più di RDTSC e coutbit in fase di esecuzione ... Ma non penso che sia davvero il punto.

Quando eseguo questa particolare variante, ottengo circa il 10% di differenza tra func1(VLA) e func2(std :: vector).

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

Questo è compilato con: g++ -O3 -Wall -Wextra -std=gnu++0x -o vla vla.cpp

Ecco il codice:

#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);
    }
}

Wow, il mio sistema mostra un miglioramento del 30% rispetto alla versione VLA std::vector.
chrisaycock,

1
Bene, prova con una gamma di dimensioni di circa 5-15 invece di 20-200 e probabilmente avrai un miglioramento del 1000% o più. [Dipende anche dalle opzioni del compilatore - Modificherò il codice sopra per mostrare le mie opzioni del compilatore su gcc]
Mats Petersson

Ho appena aggiunto un func3che utilizza v.push_back(rand())invece di v[i] = rand();e rimuove la necessità di resize(). Ci vuole circa il 10% in più rispetto a quello in uso resize(). [Ovviamente, nel processo, ho scoperto che l'uso di v[i]è un grande contributo al tempo impiegato dalla funzione - ne sono un po 'sorpreso].
Mats Petersson

1
@MikeBrown Conoscete std::vectorun'implementazione effettiva che utilizzerebbe VLA / alloca, o è solo una speculazione?
hyde,

3
Il vettore utilizza davvero un array internamente, ma per quanto ho capito, non ha modo di usare un VLA. Credo che il mio esempio dimostri che i VLA sono utili in alcuni (forse anche molti) casi in cui la quantità di dati è piccola. Anche se il vettore utilizza VLA, sarebbe dopo ulteriore sforzo all'interno vectordell'implementazione.
Mats Petersson,

0

Per quanto riguarda i VLA contro un vettore

Hai considerato che un vettore può trarre vantaggio dagli stessi VLA. Senza VLA, Vector deve specificare alcune "scale" di array, ad esempio 10, 100, 10000 per la memorizzazione, in modo da finire per allocare un array di 10000 articoli per contenere 101 elementi. Con i VLA, se ridimensionate a 200, l'algoritmo potrebbe supporre che ne occorrano solo 200 e che sia possibile allocare un array di 200 elementi. Oppure può allocare un buffer di dire n * 1.5.

Comunque, direi che se sai di quanti elementi avrai bisogno in fase di esecuzione, un VLA è più performante (come dimostrato dal benchmark di Mats). Ciò che dimostrò fu una semplice iterazione in due passaggi. Pensa alle simulazioni monte carlo in cui vengono prelevati campioni casuali ripetutamente o alla manipolazione delle immagini (come i filtri di Photoshop) in cui i calcoli vengono eseguiti su ogni elemento più volte e molto probabilmente ogni calcolo su ciascun elemento comporta la ricerca di vicini.

Quel puntatore extra salta dal vettore al suo array interno si somma.

Rispondere alla domanda principale

Ma quando parli dell'utilizzo di una struttura allocata dinamicamente come una LinkedList, non c'è paragone. Un array fornisce l'accesso diretto usando l'aritmetica del puntatore ai suoi elementi. Utilizzando un elenco collegato, è necessario percorrere i nodi per raggiungere un elemento specifico. Quindi il VLA vince a mani basse in questo scenario.

Secondo questa risposta , è architettonicamente dipendente, ma in alcuni casi l'accesso alla memoria nello stack sarà più veloce a causa dello stack disponibile nella cache. Con un gran numero di elementi questo può essere negato (potenzialmente la causa dei rendimenti decrescenti che Mats ha visto nei suoi parametri di riferimento). Tuttavia, vale la pena notare che le dimensioni della cache stanno crescendo in modo significativo e potenzialmente vedrai che tale numero aumenta di conseguenza.


Non sono sicuro di aver compreso il tuo riferimento agli elenchi collegati, quindi ho aggiunto una sezione alla domanda, spiegando un po 'di più il contesto e aggiungendo esempi di alternative a cui sto pensando.
hyde,

Perché dovrebbero essere std::vectornecessarie scale di array? Perché dovrebbe aver bisogno di spazio per gli elementi 10K quando ha bisogno solo di 101? Inoltre, la domanda non menziona mai le liste collegate, quindi non sono sicuro da dove l'hai preso. Infine, i VLA in C99 sono allocati in pila; sono una forma standard di alloca(). Qualsiasi cosa che richieda l'archiviazione dell'heap ( realloc()sopravvive dopo il ritorno della funzione) o un (l'array si ridimensiona da solo) proibirebbe comunque i VLA.
chrisaycock,

@chrisaycock C ++ non ha una funzione realloc () per qualche motivo, supponendo che la memoria sia allocata con new []. Non è questo il motivo principale per cui std :: vector deve usare le scale?

@Lundin C ++ ridimensiona il vettore con potenze di dieci? Ho appena avuto l'impressione che Mike Brown fosse davvero confuso dalla domanda, dato il riferimento all'elenco collegato. (Ha anche fatto una precedente affermazione che implicava che i VLA C99 vivessero sul mucchio.)
chrisaycock

@hyde Non avevo capito che stavi parlando. Pensavo intendessi altre strutture di dati basate su heap. Interessante ora che hai aggiunto questo chiarimento. Non sono un fanatico del C ++ per dirti la differenza tra quelli.
Michael Brown,

0

Il motivo per utilizzare un VLA è principalmente la prestazione. È un errore ignorare l'esempio wiki in quanto ha solo una differenza "irrilevante". Posso facilmente vedere casi in cui esattamente quel codice potrebbe avere un'enorme differenza, ad esempio, se quella funzione fosse chiamata in un ciclo stretto, dove read_valc'era una funzione IO che è tornata molto rapidamente su una sorta di sistema in cui la velocità era critica.

Infatti, nella maggior parte dei luoghi in cui gli VLA vengono utilizzati in questo modo, non sostituiscono le chiamate heap ma sostituiscono invece qualcosa del tipo:

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

La cosa su qualsiasi dichiarazione locale è che è estremamente veloce. La linea float vals[n]richiede generalmente solo un paio di istruzioni del processore (forse solo una.) Aggiunge semplicemente il valore nal puntatore dello stack.

D'altra parte, un'allocazione di heap richiede di percorrere una struttura di dati per trovare un'area libera. Il tempo è probabilmente un ordine di grandezza più lungo anche nel caso più fortunato. (Vale a dire, l'atto di mettere nin pila e chiamare mallocè probabilmente 5-10 istruzioni.) Probabilmente molto peggio se c'è una quantità ragionevole di dati nell'heap. Non mi sorprenderebbe affatto vedere un caso in cui da malloc100 a 1000 volte più lento in un vero programma.

Certo, allora hai anche un certo impatto sulle prestazioni con la corrispondenza free, probabilmente simile in grandezza alla mallocchiamata.

Inoltre, c'è il problema della frammentazione della memoria. Molte piccole allocazioni tendono a frammentare il mucchio. I frammenti accumulano sia memoria sprecata che aumento del tempo necessario per allocare memoria.


Sull'esempio di Wikipedia: potrebbe far parte di un buon esempio, ma senza contesto, più codice attorno ad esso, in realtà non mostra nessuna delle 5 cose elencate nella mia domanda. Altrimenti sì, sono d'accordo con la tua spiegazione. Sebbene una cosa da tenere a mente: l'utilizzo di VLA può comportare un costo di accesso alle variabili locali, con esse gli offset di tutte le variabili locali non sono necessariamente noti al momento della compilazione, quindi è necessario fare attenzione a non sostituire un costo di heap una tantum con un penalità del ciclo interno per ogni iterazione.
hyde,

Um ... non sono sicuro di cosa intendi. Le dichiarazioni delle variabili locali sono una singola operazione e qualsiasi compilatore leggermente ottimizzato estrarrà l'allocazione da un ciclo interno. Non vi è alcun "costo" particolare nell'accesso alle variabili locali, certamente non uno che aumenterà un VLA.
Gort il robot

Esempio concreto: l' int vla[n]; if(test()) { struct LargeStruct s; int i; }offset dello stack di snon sarà noto al momento della compilazione, ed è anche dubbio se il compilatore sposta l'archiviazione idall'ambito interno allo offset dello stack fisso. Quindi è necessario un codice macchina aggiuntivo a causa dell'indirizzamento indiretto, e questo può anche divorare i registri, importante sull'hardware del PC. Se si desidera un codice di esempio con l'output dell'assemblatore del compilatore incluso, fare una domanda separata;)
hyde

Il compilatore non deve allocare nell'ordine riscontrato nel codice e non importa se lo spazio è allocato e non utilizzato. Un ottimizzatore intelligente alloca spazio per se iquando la funzione viene immessa, prima che testvenga chiamata o vlaallocata, poiché le allocazioni per se isenza effetti collaterali. (E, in effetti, ipotrebbe persino essere inserito in un registro, il che significa che non esiste alcuna "allocazione".) Non ci sono garanzie del compilatore per l'ordine delle allocazioni nello stack, o persino che lo stack venga utilizzato.
Gort il robot

(cancellato un commento che era sbagliato a causa di uno stupido errore)
hyde
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.