Manera profesional de producir un gran problema sin llenar grandes matrices: C ++, memoria libre de parte de una matriz

20

Estoy desarrollando una simulación física, y como soy bastante nuevo en programación, sigo teniendo problemas al producir programas grandes (principalmente problemas de memoria). Sé acerca de la asignación y eliminación de memoria dinámica (nuevo / eliminar, etc.), pero necesito un mejor enfoque sobre cómo estructurar el programa.

Digamos que estoy simulando un experimento que se está ejecutando durante unos días, con una frecuencia de muestreo muy grande. Necesitaría simular mil millones de muestras y atropellarlas.

Como una versión súper simplificada, diremos que un programa toma voltajes V [i] y los suma en cinco:

es decir, NewV [0] = V [0] + V [1] + V [2] + V [3] + V [4]

entonces NewV [1] = V [1] + V [2] + V [3] + V [4] + V [5]

entonces NewV [2] = V [2] + V [3] + V [4] + V [5] + V [6] ... y esto continúa para mil millones de muestras.

Al final, tendría V [0], V [1], ..., V [1000000000], cuando los únicos que necesitaría almacenar para el siguiente paso son los últimos 5 V [i] s.

¿Cómo eliminaría / desasignaría parte de la matriz para que la memoria se pueda volver a usar (por ejemplo, V [0] después de la primera parte del ejemplo donde ya no es necesaria)? ¿Existen alternativas a cómo estructurar un programa de este tipo?

Escuché sobre malloc / free, pero escuché que no deberían usarse en C ++ y que existen mejores alternativas.

¡Muchas gracias!

tldr; ¿Qué hacer con partes de matrices (elementos individuales) que ya no necesito y que ocupan una gran cantidad de memoria?

Drummermean
fuente
2
No puede desasignar parte de una matriz. Puede reasignarlo a una matriz más pequeña en otro lugar, pero esto puede resultar costoso. En su lugar, podría utilizar una estructura de datos diferente, como una lista vinculada. Tal vez también podría almacenar los pasos en Vlugar de en una nueva matriz. Sin embargo, fundamentalmente, creo que su problema está en sus algoritmos o en sus estructuras de datos, y como no tenemos ningún detalle, es difícil saber cómo hacerlo de manera eficiente.
Vincent Savard el
44
Nota al margen: los SMA de longitud arbitraria se pueden calcular particularmente rápido con esta relación de recurrencia: NewV [n] = NewV [n-1] - V [n-1] + V [n + 4] (su notación). Pero tenga en cuenta que estos no son filtros particularmente útiles. Su respuesta de frecuencia es sinc, que casi nunca es lo que quieres (lóbulos laterales muy altos).
Steve Cox
2
SMA = promedio móvil simple, para cualquiera que se pregunte.
Charles
3
@SteveCox, la forma en que lo escribió, tiene un filtro FIR. Su recurrencia es la forma equivalente de IIR. De cualquier manera, puedes mantener un búfer circular de las últimas N lecturas.
John R. Strohm
@ JohnR.Strohm la respuesta al impulso es idéntica y finita
Steve Cox

Respuestas:

58

Lo que usted describe, "suavizado por cinco", es un filtro digital de respuesta de impulso finito (FIR). Dichos filtros se implementan con amortiguadores circulares. Mantiene solo los últimos N valores, mantiene un índice en el búfer que le indica dónde está el valor más antiguo, sobrescribe el valor más antiguo actual con el más nuevo en cada paso y escalona el índice, circularmente, cada vez.

Mantiene sus datos recopilados, que va a procesar, en el disco.

Dependiendo de su entorno, este puede ser uno de esos lugares donde es mejor que obtenga ayuda con experiencia. En una universidad, pones una nota en el tablón de anuncios en el Departamento de Ciencias de la Computación, ofreciendo salarios de los estudiantes (o incluso tarifas de consultoría estudiantil) por unas pocas horas de trabajo, para ayudarte a procesar tus datos. O tal vez ofrezca puntos de oportunidad de investigación de pregrado. O algo.

John R. Strohm
fuente
66
¡Un búfer circular parece ser lo que estoy buscando! Ahora instalé las bibliotecas boost C ++ e incluí boost / circular_buffer.hpp, y funciona como se esperaba. Gracias, @John
Drummermean
2
solo se implementan filtros FIR muy cortos en forma directa en el software, y los SMA casi nunca se implementan.
Steve Cox
@SteveCox: la fórmula de bordes de ventana que usó es bastante efectiva para filtros enteros y de punto fijo, sin embargo, es incorrecta para punto flotante, donde las operaciones no son conmutativas.
Ben Voigt
@BenVoigt, creo que querías responder a mi otro comentario, pero sí, ese formulario introduce un ciclo límite alrededor de una cuantización que puede ser muy complicado. Afortunadamente, este ciclo límite en particular es estable.
Steve Cox
Realmente no necesita un impulso para un búfer circular para ese uso uu Utilizará mucha más memoria de la necesaria.
GameDeveloper
13

Cada problema se puede resolver agregando un nivel adicional de indirección. Entonces haz eso.

No puede eliminar parte de una matriz en C ++. Pero puede crear una nueva matriz que contenga solo los datos que desea conservar y luego eliminar la anterior. Por lo tanto, puede crear una estructura de datos que le permita "eliminar" elementos que no desea desde el frente. Lo que realmente hará es crear una nueva matriz y copiar los elementos no eliminados a la nueva, luego eliminar la anterior.

O simplemente podría usar std::deque, que ya puede hacer esto de manera efectiva. deque, o "cola de doble extremo", es una estructura de datos destinada a casos en los que elimina elementos de un extremo mientras agrega elementos al otro.

Nicol Bolas
fuente
30
Todos los problemas se pueden resolver agregando un nivel adicional de indirección ... excepto a muchos niveles de indirección.
YSC
17
@YSC: y ortografía :)
Lightness compite con Monica el
1
para este problema en particular std::dequees el camino a seguir
davidbak
77
@davidbak - ¿Qué? No hay necesidad de estar constantemente asignando y liberando memoria. Un buffer circular de tamaño fijo que se asigna una vez en el momento de la inicialización se ajusta mucho mejor a este problema.
David Hammen
2
@DavidHammen: Quizás, pero 1) La biblioteca estándar no tiene un "búfer circular de tamaño fijo" en su kit de herramientas. 2) Si realmente necesita dicha optimización, puede hacer algunas tareas de asignación para minimizar las reasignaciones deque. Es decir, almacenar y reutilizar asignaciones según lo solicitado. Entonces dequeparece una solución perfectamente adecuada para el problema.
Nicol Bolas
4

Las respuestas FIR y SMA que obtuvo son buenas en su caso, sin embargo, me gustaría aprovechar la oportunidad para impulsar un enfoque más genérico.

Lo que tiene aquí es una secuencia de datos: en lugar de estructurar su programa en 3 grandes pasos (obtener datos, calcular, resultado de salida) que requieren cargar todos los datos en la memoria a la vez, puede estructurarlo como una tubería .

Una tubería comienza con una corriente, la transforma y la empuja a un sumidero.

En su caso, la tubería se ve así:

  1. Leer elementos del disco, emitir elementos uno a la vez
  2. Reciba ítems uno a la vez, por cada ítem recibido emite los últimos 5 recibidos (donde entra su búfer circular)
  3. Reciba artículos 5 a la vez, para cada grupo calcule el resultado
  4. Recibe el resultado, escríbelo en el disco

C ++ tiende a usar iteradores en lugar de secuencias, pero para ser honesto, las secuencias son más fáciles de modelar (hay una propuesta para rangos que serían similares a los flujos):

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

Y luego, la tubería se ve así:

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";
}

Las secuencias no siempre son aplicables (no funcionan cuando se necesita acceso aleatorio a los datos), pero cuando lo son, se mueven: al operar en una cantidad muy pequeña de memoria, lo mantiene todo en la memoria caché de la CPU.


En otra nota: parece que su problema podría ser "vergonzosamente paralelo", es posible que desee dividir su archivo grande en trozos (tenga en cuenta, para el procesamiento por ventanas de 5, que necesita tener 4 elementos comunes en cada límite) y luego procesar los trozos en paralelo.

Si la CPU es el cuello de botella (y no la E / S), puede acelerarla iniciando un proceso por núcleo que tenga después de haber dividido los archivos en cantidades aproximadamente iguales.

Matthieu M.
fuente