Um bom exemplo de matriz de comprimento variável C [fechado]

9

Como essa pergunta ficou um pouco congelada na SO, decidi excluí-la e tentar aqui. Se você acha que ele também não se encaixa aqui, pelo menos deixe um comentário na sugestão de como encontrar um exemplo que eu estou procurando ...

Você pode dar um exemplo , onde o uso de V99s C99 oferece uma vantagem real sobre algo como os atuais mecanismos C ++ RAII Ceap-heap atuais?

O exemplo que estou procurando deve:

  1. Obtenha uma vantagem de desempenho facilmente mensurável (talvez 10%) sobre o uso de heap.
  2. Não tem uma boa solução alternativa, que não precisaria de toda a matriz.
  3. Beneficie realmente do tamanho dinâmico, em vez do tamanho máximo fixo.
  4. É improvável que cause excesso de pilha no cenário de uso normal.
  5. Seja forte o suficiente para tentar um desenvolvedor que precisa do desempenho para incluir um arquivo de origem C99 em um projeto C ++.

Adicionando alguns esclarecimentos sobre o contexto: refiro-me ao VLA como significou C99 e não incluído no padrão C ++: int array[n]where né uma variável. E estou atrás de um exemplo de caso de uso em que ele supera as alternativas oferecidas por outros padrões (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

Algumas ideias:

  • Funções que utilizam varargs, que limitam naturalmente a contagem de itens a algo razoável, ainda não possuem nenhum limite superior útil no nível da API.
  • Funções recursivas, onde pilha desperdiçada é indesejável
  • Muitas pequenas alocações e liberações, onde a sobrecarga da pilha seria ruim.
  • Manipulação de matrizes multidimensionais (como matrizes de tamanho arbitrário), onde o desempenho é crítico, e espera-se que pequenas funções sejam incorporadas muito.
  • Do comentário: algoritmo simultâneo, em que a alocação de heap tem sobrecarga de sincronização .

A Wikipedia tem um exemplo que não atende aos meus critérios , porque a diferença prática de usar heap parece irrelevante, pelo menos sem contexto. Também não é ideal, porque sem mais contexto, parece que a contagem de itens pode muito bem causar estouro de pilha.

Nota: Estou especificamente após um código de exemplo, ou sugestão de um algoritmo que se beneficiaria disso, para eu implementar o exemplo eu mesmo.

hyde
fonte
11
Um pouco especulativo (já que este é um martelo à procura de um prego), mas talvez alloca()realmente superasse malloc()em um ambiente multithread por causa da contenção de trava no último . Mas essa é uma extensão real, uma vez que matrizes pequenas devem usar apenas um tamanho fixo, e matrizes grandes provavelmente precisarão da pilha de qualquer maneira.
chrisaycock
11
@chrisaycock Sim, muito martelo à procura de um prego, mas um martelo que realmente existe (seja o C99 VLA ou o não-realmente-em-qualquer-padrão alloca, que eu acho que são basicamente a mesma coisa). Mas essa coisa multithread é boa, editando a pergunta para incluí-la!
Hyde
Uma desvantagem dos VLAs é que não há mecanismo para detectar uma falha de alocação; se não houver memória suficiente, o comportamento é indefinido. (O mesmo é verdade para as matrizes de tamanho fixo - e para alloca ().)
Keith Thompson
@KeithThompson Bem, não há garantia de que malloc / new também detecte falha na alocação, por exemplo, consulte a página de manual do Notes para Linux malloc ( linux.die.net/man/3/malloc ).
Hyde
@hyde: E é discutível se o malloccomportamento do Linux está em conformidade com o padrão C.
Keith Thompson

Respostas:

9

Acabei de hackear um pequeno programa que gera um conjunto de números aleatórios reiniciando na mesma semente a cada vez, para garantir que seja "justo" e "comparável". À medida que avança, ele descobre o mínimo e o máximo desses valores. E quando gera o conjunto de números, conta quantos estão acima da média de mine max.

Para matrizes MUITO pequenas, mostra um benefício claro com o término do VLA std::vector<>.

Não é um problema real, mas podemos facilmente imaginar algo em que estaríamos lendo os valores de um arquivo pequeno, em vez de usar números aleatórios, e fazendo outros cálculos de contagem / min / max mais significativos com o mesmo tipo de código .

Para valores MUITO pequenos do "número de números aleatórios" (x) nas funções relevantes, a vlasolução vence por uma margem enorme. À medida que o tamanho aumenta, a "vitória" diminui e, com tamanho suficiente, a solução vetorial parece ser MAIS eficiente - não estudou muito essa variante, como quando começamos a ter milhares de elementos em um VLA, não é realmente o que eles deveriam fazer ...

E tenho certeza de que alguém me dirá que há alguma maneira de escrever todo esse código com vários modelos e fazê-lo sem executar mais do que o RDTSC e os coutbits em tempo de execução ... Mas não acho que seja realmente o ponto.

Ao executar esta variante específica, recebo uma diferença de cerca de 10% entre o 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

Isso é compilado com: g++ -O3 -Wall -Wextra -std=gnu++0x -o vla vla.cpp

Aqui está o código:

#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);
    }
}
Mats Petersson
fonte
Uau, meu sistema mostra uma melhoria de 30% na versão do VLA std::vector.
Chrisaycock # 14/13
11
Bem, tente com um intervalo de tamanho de cerca de 5-15 em vez de 20-200, e você provavelmente terá uma melhoria de 1000% ou mais. [Também depende de opções do compilador - Vou editar o código acima para mostrar minhas opções do compilador no gcc]
Mats Petersson
Acabei de adicionar um func3que usa em v.push_back(rand())vez de v[i] = rand();e remove a necessidade resize(). Demora cerca de 10% a mais em comparação com o uso resize(). [É claro que, no processo, descobri que o uso de v[i]é um dos principais contribuintes para o tempo que a função leva - estou um pouco surpreso com isso].
214131 Mats Matsson
11
@MikeBrown Você conhece uma std::vectorimplementação real que usaria o VLA / alloca, ou isso é apenas especulação?
Hyde
3
O vetor realmente usa uma matriz internamente, mas, pelo que entendi, não tem como usar um VLA. Acredito que meu exemplo mostra que os VLAs são úteis em alguns (talvez até muitos) casos em que a quantidade de dados é pequena. Mesmo que o vetor use VLAs, seria após um esforço adicional dentro da vectorimplementação.
precisa
0

Em relação a VLAs versus um vetor

Você considerou que um vetor pode tirar proveito dos próprios VLAs. Sem os VLAs, o vetor precisa especificar certas "escalas" de matrizes, por exemplo, 10, 100, 10000 para armazenamento, para que você aloque uma matriz de 10000 itens para armazenar 101 itens. Com os VLAs, se você redimensionar para 200, o algoritmo pode assumir que você precisará apenas de 200 e pode alocar uma matriz de 200 itens. Ou pode alocar um buffer de dizer n * 1,5.

De qualquer forma, eu argumentaria que, se você souber quantos itens precisará em tempo de execução, um VLA terá melhor desempenho (como demonstrado pelo benchmark de Mats). O que ele demonstrou foi uma iteração simples de duas passagens. Pense em simulações de monte carlo, onde amostras aleatórias são coletadas repetidamente, ou manipulação de imagem (como filtros do Photoshop), onde os cálculos são feitos em cada elemento várias vezes e, possivelmente, cada cálculo em cada elemento envolve a observação de vizinhos.

Esse ponteiro extra salta do vetor para sua matriz interna.

Respondendo à pergunta principal

Mas quando você fala sobre o uso de uma estrutura alocada dinamicamente como um LinkedList, não há comparação. Uma matriz fornece acesso direto usando aritmética de ponteiro para seus elementos. Usando uma lista vinculada, você precisa percorrer os nós para chegar a um elemento específico. Portanto, o VLA ganha as mãos nesse cenário.

De acordo com esta resposta , ele é dependente da arquitetura, mas em alguns casos o acesso à memória na pilha será mais rápido devido à disponibilidade da pilha no cache. Com um grande número de elementos, isso pode ser negado (potencialmente a causa dos retornos decrescentes que Mats viu em seus benchmarks). No entanto, vale a pena notar que os tamanhos de cache estão crescendo significativamente e você potencialmente verá mais esse número crescer de acordo.

Michael Brown
fonte
Não sei se entendi sua referência a listas vinculadas, então adicionei uma seção à pergunta, explicando um pouco mais o contexto e adicionando exemplos de alternativas nas quais estou pensando.
Hyde
Por que uma std::vectornecessidade de escalas de matrizes? Por que precisaria de espaço para 10 mil elementos quando precisa apenas de 101? Além disso, a pergunta nunca menciona listas vinculadas, por isso não sei de onde você tirou isso. Finalmente, os VLAs no C99 são alocados à pilha; eles são uma forma padrão de alloca(). Qualquer coisa que exija armazenamento em heap (permanece após a função retornar) ou a realloc()(a matriz é redimensionada) proibiria os VLAs de qualquer maneira.
Chrisaycock # 14/13
@chrisaycock O C ++ não possui uma função realloc () por algum motivo, assumindo que a memória está alocada com new []. Não é essa a principal razão pela qual std :: vector deve usar escalas?
@Lundin O C ++ escala o vetor com potências de dez? Acabei de ter a impressão de que Mike Brown estava realmente confuso com a pergunta, dada a referência da lista vinculada. (Ele também fez uma afirmação anterior que sugeria C99 VLAs vivem na pilha.)
chrisaycock
@hyde Eu não sabia que era disso que você estava falando. Eu pensei que você quis dizer outras estruturas de dados baseadas em heap. Interessante agora que você adicionou este esclarecimento. Eu não sou um nerd de C ++ suficiente para dizer a diferença entre eles.
22713 Michael Michael
0

O motivo para usar um VLA é principalmente o desempenho. É um erro desconsiderar o exemplo do wiki como tendo apenas uma diferença "irrelevante". Eu posso ver facilmente casos em que exatamente esse código poderia ter uma grande diferença, por exemplo, se essa função fosse chamada em um loop apertado, onde read_valhavia uma função de E / S que retornava muito rapidamente em algum tipo de sistema em que a velocidade era crítica.

De fato, na maioria dos lugares onde os VLAs são usados ​​dessa maneira, eles não substituem as chamadas de heap, mas substituem algo como:

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

A questão de qualquer declaração local é que ela é extremamente rápida. A linha float vals[n]geralmente requer apenas algumas instruções do processador (talvez apenas uma). Simplesmente adiciona o valor nao ponteiro da pilha.

Por outro lado, uma alocação de heap requer caminhar uma estrutura de dados para encontrar uma área livre. O tempo é provavelmente uma ordem de magnitude mais longa, mesmo nos casos mais afortunados. (Ou seja, apenas o ato de colocar nna pilha e chamar mallocé provavelmente de 5 a 10 instruções.) Provavelmente muito pior se houver uma quantidade razoável de dados no heap. Não me surpreenderia ver um caso de malloc100x a 1000x mais lento em um programa real.

Obviamente, você também terá algum impacto no desempenho com a correspondência free, provavelmente semelhante em magnitude à mallocchamada.

Além disso, há o problema da fragmentação da memória. Muitas pequenas alocações tendem a fragmentar a pilha. Amontoados fragmentados desperdiçam memória e aumentam o tempo necessário para alocar memória.

Gort the Robot
fonte
Sobre o exemplo da Wikipedia: poderia fazer parte de um bom exemplo, mas sem contexto, mais código ao seu redor, ele realmente não mostra nenhuma das 5 coisas enumeradas na minha pergunta. Caso contrário, sim, eu concordo com sua explicação. Apesar de uma coisa a ter em mente: o uso de VLAs pode ter um custo para acessar variáveis ​​locais, com elas as compensações de todas as variáveis ​​locais não são necessariamente conhecidas no momento da compilação; portanto, deve-se tomar cuidado para não substituir um custo de heap único por um penalidade de loop interno para cada iteração.
Hyde
Hum ... não sei o que você quer dizer. As declarações de variáveis ​​locais são uma única operação e qualquer compilador levemente otimizado extrai a alocação de um loop interno. Não existe um "custo" específico no acesso a variáveis ​​locais, certamente nenhum que um VLA aumente.
Gort the Robot
Exemplo concreto:: o int vla[n]; if(test()) { struct LargeStruct s; int i; }deslocamento da pilha de snão será conhecido no momento da compilação, e também é duvidoso que o compilador mova o armazenamento ifora do escopo interno para o deslocamento da pilha fixo. Portanto, código de máquina extra é necessário porque a indireção e isso também podem consumir registros, importantes no hardware do PC. Se você quiser código de exemplo com saída de montagem compilador incluído, para fazer uma pergunta separada;)
Hyde
O compilador não precisa ser alocado na ordem encontrada no código e não importa se o espaço é alocado e não usado. Um otimizador inteligente alocaria espaço para se iquando a função é inserida, antes testé chamada ou vlaé alocada, como alocações se isem efeitos colaterais. (E, de fato, ipode até ser colocado em um registro, o que significa que não há "alocação"). Não há garantias do compilador para a ordem das alocações na pilha, ou mesmo que a pilha seja usada.
Gort the Robot
(suprimido um comentário que estava errado devido a um erro estúpido)
Hyde