Maneira profissional de produzir um grande problema sem preencher grandes matrizes: C ++, libera memória de parte de uma matriz

20

Estou desenvolvendo uma simulação de física e, como sou um iniciante em programação, continuo tendo problemas ao produzir programas grandes (principalmente problemas de memória). Eu sei sobre alocação e exclusão de memória dinâmica (nova / exclusão, etc.), mas preciso de uma abordagem melhor para estruturar o programa.

Digamos que estou simulando um experimento que está sendo executado por alguns dias, com uma taxa de amostragem muito grande. Eu precisaria simular um bilhão de amostras e atropelar elas.

Como uma versão super simplificada, diremos que um programa utiliza as voltagens V [i] e as soma em cinco:

ie NovoV [0] = V [0] + V [1] + V [2] + V [3] + V [4]

então NewV [1] = V [1] + V [2] + V [3] + V [4] + V [5]

então NewV [2] = V [2] + V [3] + V [4] + V [5] + V [6] ... e isso se aplica a um bilhão de amostras.

No final, eu teria V [0], V [1], ..., V [1000000000], quando os únicos que eu precisaria armazenar para a próxima etapa são os últimos 5 V [i] s.

Como excluir / desalocar parte da matriz para que a memória fique livre para uso novamente (digamos V [0] após a primeira parte do exemplo, onde ela não é mais necessária)? Existem alternativas para estruturar esse programa?

Eu ouvi falar do malloc / free, mas ouvi dizer que eles não devem ser usados ​​em C ++ e que existem alternativas melhores.

Muito obrigado!

tldr; o que fazer com partes de matrizes (elementos individuais) Eu não preciso mais de uma quantidade enorme de memória?

Drummermean
fonte
2
Você não pode desalocar parte de uma matriz. Você pode realocá-lo para uma matriz menor em outro lugar, mas isso pode ser caro. Você poderia usar uma estrutura de dados diferente, como uma lista vinculada, possivelmente. Talvez você também possa armazenar as etapas em Vvez de em uma nova matriz. Fundamentalmente, porém, acho que seu problema está em seus algoritmos ou em suas estruturas de dados e, como não temos detalhes, é difícil saber como fazê-lo com eficiência.
22417 Vincent Vincentardard
4
Nota lateral: SMAs de comprimento arbitrário podem ser calculados particularmente rápido com esta relação de recorrência: NewV [n] = NewV [n-1] - V [n-1] + V [n + 4] (sua notação). Mas lembre-se de que esses filtros não são particularmente úteis. A resposta de frequência deles é sinc, o que quase nunca é o que você deseja (lóbulos laterais altos).
Steve Cox
2
SMA = média móvel simples, para quem se pergunta.
Charles
3
@SteveCox, do jeito que ele escreveu, ele tem um filtro FIR. Sua recorrência é o formulário IIR equivalente. De qualquer forma, você consegue manter um buffer circular das últimas N leituras.
John R. Strohm
@ JohnR.Strohm a resposta ao impulso é idêntico, e finito
Steve Cox

Respostas:

58

O que você descreve, "suavização por cinco", é um filtro digital de resposta finita ao impulso (FIR). Esses filtros são implementados com buffers circulares. Você mantém apenas os últimos N valores, mantém um índice no buffer que informa onde está o valor mais antigo, substitui o valor mais antigo atual pelo mais recente em cada etapa e alterna o índice, circularmente, a cada vez.

Você mantém os dados coletados, que serão processados, em disco.

Dependendo do seu ambiente, este pode ser um daqueles lugares onde é melhor obter ajuda experiente. Em uma universidade, você coloca uma nota no quadro de avisos do Departamento de Ciência da Computação, oferecendo salários aos alunos (ou mesmo taxas de consultoria para estudantes) por algumas horas de trabalho, para ajudá-lo a analisar seus dados. Ou talvez você ofereça pontos de oportunidade de pesquisa de graduação. Ou alguma coisa.

John R. Strohm
fonte
6
De fato, um buffer circular parece ser o que estou procurando! Agora instalei as bibliotecas C ++ boost e incluí o boost / circular_buffer.hpp, e está funcionando conforme o esperado. Obrigado, @John
Drummermean
2
apenas filtros FIR muito curtos são implementados de forma direta em software, e os SMA quase nunca são.
Steve Cox
@SteveCox: a fórmula das bordas da janela usada é bastante eficaz para filtros de ponto inteiro e de ponto fixo; no entanto, é incorreta para ponto flutuante, onde as operações não são comutativas.
Ben Voigt
@BenVoigt, acho que você quis responder ao meu outro comentário, mas sim, esse formulário introduz um ciclo limite em torno de uma quantização que pode ser muito complicada. felizmente, porém, esse ciclo de limite específico é estável.
Steve Cox
Você realmente não precisa de impulso para um buffer circular para esse uso uu Você estará usando muito mais memória do que o necessário.
GameDeveloper 25/01
13

Todo problema pode ser resolvido adicionando um nível adicional de indireção. Então faça isso.

Você não pode excluir parte de uma matriz em C ++. Mas você pode criar uma nova matriz contendo apenas os dados que deseja manter e excluir a antiga. Assim, você pode criar uma estrutura de dados que permita "remover" elementos que você não deseja da frente. O que ele realmente fará é criar uma nova matriz e copiar os elementos não removidos para a nova e excluir a antiga.

Ou você pode simplesmente usar std::deque, o que já pode efetivamente fazer isso. deque, ou "fila dupla", é uma estrutura de dados destinada a casos em que você exclui elementos de uma extremidade e adiciona elementos à outra.

Nicol Bolas
fonte
30
Todo problema pode ser resolvido adicionando um nível adicional de indireção ... exceto em muitos níveis de indireção.
YSC
17
@YSC: e ortografia :)
Lightness Races com Monica
1
para este problema particular std::dequeé o caminho a percorrer
davidbak
7
@davidbak - O quê? Não é necessário estar constantemente alocando e liberando memória. Um buffer circular de tamanho fixo que é alocado uma vez no momento da inicialização é um ajuste muito melhor para esse problema.
David Hammen
2
@ David David: Talvez, mas 1) A biblioteca padrão não tem um "buffer circular de tamanho fixo" em seu kit de ferramentas. 2) Se você realmente precisa dessa otimização, pode fazer algumas coisas de alocador para minimizar as realocações deque. Ou seja, armazenar e reutilizar alocações, conforme solicitado. Assim, dequeparece uma solução perfeitamente adequada para o problema.
Nicol Bolas
4

As respostas FIR e SMA que você obteve são boas no seu caso, no entanto, gostaria de aproveitar a oportunidade para avançar com uma abordagem mais genérica.

O que você tem aqui é um fluxo de dados: em vez de estruturar seu programa em 3 grandes etapas (obter dados, calcular, resultado de saída) que exigem o carregamento de todos os dados na memória de uma só vez, você pode estruturá-lo como um pipeline .

Um pipeline começa com um fluxo, o transforma e o empurra para uma pia.

No seu caso, o pipeline se parece com:

  1. Leia itens do disco, emita itens um de cada vez
  2. Receba itens um de cada vez, pois cada item recebido emite os últimos 5 recebidos (onde seu buffer circular entra)
  3. Receba os itens 5 de cada vez, para cada grupo calcule o resultado
  4. Receba o resultado, grave-o no disco

O C ++ tende a usar iteradores em vez de fluxos, mas, para ser honesto, os fluxos são mais fáceis de modelar (existe uma proposta para intervalos que seriam semelhantes aos fluxos):

template <typename T>
class Stream {
public:
    virtual boost::optional<T> next() = 0;
    virtual ~Stream() {}
};

class ReaderStream: public Stream<Item> {
public:
    boost::optional<Item> next() override final;

private:
    std::ifstream file;
};

class WindowStream: public Stream<Window> {
public:
    boost::optional<Window> next() override final;

private:
    Window window;
    Stream<Item>& items;
};

class ResultStream: public Stream<Result> {
public:
    boost::optional<Result> next() override final;

private:
    Stream<Window>& windows;
};

E então, o pipeline se parece com:

ReaderStream itemStream("input.txt");
WindowStream windowStream(itemsStream, 5);
ResultStream resultStream(windowStream);
std::ofstream results("output.txt", std::ios::binary);

while (boost::optional<Result> result = resultStream.next()) {
    results << *result << "\n";
}

Os fluxos nem sempre são aplicáveis ​​(eles não funcionam quando você precisa de acesso aleatório aos dados), mas quando são, eles agitam: ao operar com uma quantidade muito pequena de memória, você mantém tudo no cache da CPU.


Em outra nota: parece que seu problema pode ser "embaraçosamente paralelo", convém dividir seu arquivo grande em pedaços (lembre-se, para processar pelo Windows 5, é necessário ter 4 elementos comuns em cada limite) e processe os pedaços em paralelo.

Se a CPU for o gargalo (e não a E / S), você poderá acelerá-la iniciando um processo por núcleo depois de dividir os arquivos em quantidades aproximadamente iguais.

Matthieu M.
fonte