c++ new - ¿Qué usos hay para la "nueva colocación"?




allocation dynamic (19)

He visto que se usa como un pequeño hack de rendimiento para un puntero de "tipo dinámico" (en la sección "Bajo el capó"):

Pero aquí está el truco complicado que solía obtener rápido rendimiento para tipos pequeños: si el valor que se mantiene puede caber dentro de un vacío *, en realidad no me molesto en asignar un nuevo objeto, lo fuerzo en el puntero utilizando la ubicación nueva .

¿Alguien aquí alguna vez ha usado la "colocación nueva" de C ++? Si es así, ¿para qué? Me parece que solo sería útil en hardware mapeado en memoria.


Lo he usado para almacenar objetos con archivos asignados en memoria.
El ejemplo específico fue una base de datos de imágenes que procesó grandes cantidades de imágenes grandes (más de lo que cabía en la memoria).


También es útil cuando desea reinicializar estructuras globales o asignadas estáticamente.

La forma antigua de C usaba memset() para establecer todos los elementos en 0. No se puede hacer eso en C ++ debido a vtables y constructores de objetos personalizados.

Así que a veces uso lo siguiente

 static Mystruct m;

 for(...)  {
     // re-initialize the structure. Note the use of placement new
     // and the extra parenthesis after Mystruct to force initialization.
     new (&m) Mystruct();

     // do-some work that modifies m's content.
 }

Lo he usado para crear una clase Variant (es decir, un objeto que puede representar un valor único que puede ser uno de varios tipos diferentes).

Si todos los tipos de valor admitidos por la clase Variant son tipos de POD (por ejemplo, int, float, double, bool), entonces una unión de estilo C etiquetada es suficiente, pero si desea que algunos de los tipos de valor sean objetos C ++ ( por ejemplo, std :: string), la función de unión C no funcionará, ya que los tipos de datos que no son POD no pueden declararse como parte de una unión.

Entonces, en lugar de eso, asigno una matriz de bytes que es lo suficientemente grande (por ejemplo, sizeof (the_largest_data_type_I_support)) y uso la ubicación nueva para inicializar el objeto C ++ apropiado en esa área cuando la Variante está configurada para mantener un valor de ese tipo. (Y eliminar la ubicación de antemano al cambiar de un tipo de datos distinto de POD, por supuesto)


Vea el archivo fp.h en el proyecto xll en http://xll.codeplex.com. Resuelve el problema de la "falta de armonía con el compilador" para los arreglos que les gusta llevar sus dimensiones con ellos.

typedef struct _FP
{
    unsigned short int rows;
    unsigned short int columns;
    double array[1];        /* Actually, array[rows][columns] */
} FP;

Los motores de secuencias de comandos pueden usarlo en la interfaz nativa para asignar objetos nativos de secuencias de comandos. Consulte Angelscript (www.angelcode.com/angelscript) para ver ejemplos.



El único lugar en el que me he encontrado es en contenedores que asignan un búfer contiguo y luego lo llenan con objetos según sea necesario. Como se mencionó, std :: vector podría hacer esto, y sé que algunas versiones de MFC CArray y / o CList hicieron esto (porque ahí es donde lo encontré por primera vez). El método de asignación excesiva de búfer es una optimización muy útil, y la nueva ubicación es prácticamente la única forma de construir objetos en ese escenario. También se usa a veces para construir objetos en bloques de memoria asignados fuera de su código directo.

Lo he usado en una capacidad similar, aunque no aparece a menudo. Sin embargo, es una herramienta útil para la caja de herramientas de C ++.


Es útil si está creando un kernel. ¿Dónde coloca el código del kernel que leyó en el disco o en la tabla de páginas? Necesitas saber dónde saltar.

O en otras circunstancias muy raras, como cuando tienes un montón de espacio asignado y quieres colocar algunas estructuras una detrás de la otra. Se pueden empaquetar de esta manera sin la necesidad del operador offsetof (). Sin embargo, hay otros trucos para eso también.

También creo que algunas implementaciones de STL hacen uso de la ubicación nueva, como std :: vector. Asignan espacio para 2 ^ n elementos de esa manera y no necesitan reasignar siempre.


Lo he usado para crear objetos basados ​​en la memoria que contiene mensajes recibidos de la red.


La nueva ubicación le permite construir un objeto en la memoria que ya está asignada.

Es posible que desee hacer esto para las optimizaciones (es más rápido no reasignar todo el tiempo) pero necesita reconstruir un objeto varias veces. Si necesita seguir reasignándolo, puede ser más eficiente asignar más de lo que necesita, aunque todavía no quiera usarlo.

Devex da un buen ejemplo :

C ++ estándar también admite la colocación de un nuevo operador, que construye un objeto en un búfer asignado previamente. Esto es útil cuando se crea un grupo de memoria, un recolector de basura o simplemente cuando el rendimiento y la seguridad de excepción son primordiales (no hay peligro de falla de asignación ya que la memoria ya ha sido asignada, y construir un objeto en un búfer preasignado lleva menos tiempo) :

char *buf  = new char[sizeof(string)]; // pre-allocated buffer
string *p = new (buf) string("hi");    // placement new
string *q = new string("hi");          // ordinary heap allocation

También es posible que desee asegurarse de que no haya un error de asignación en una parte determinada del código crítico (por ejemplo, tal vez trabaje en un marcapasos). En ese caso usted querría utilizar la nueva ubicación.

Desasignación en colocación nueva.

No debe desasignar todos los objetos que utilizan el búfer de memoria. En su lugar, debe eliminar [] solo el búfer original. Tendrías que llamar a los destructores directamente de tus clases manualmente. Para una buena sugerencia sobre esto, consulte las preguntas frecuentes de Stroustrup en: ¿Hay una "eliminación de ubicación" ?


Creo que esto no se ha resaltado con ninguna respuesta, pero otro buen ejemplo y uso para la nueva ubicación es reducir la fragmentación de la memoria (mediante el uso de grupos de memoria). Esto es especialmente útil en sistemas integrados y de alta disponibilidad. En este último caso es especialmente importante porque para un sistema que tiene que funcionar 24/365 días es muy importante no tener fragmentación. Este problema no tiene nada que ver con la pérdida de memoria.

Incluso cuando se utiliza una implementación malloc muy buena (o una función de gestión de memoria similar) es muy difícil lidiar con la fragmentación durante mucho tiempo. En algún momento, si no maneja inteligentemente las llamadas de reserva / liberación de memoria, podría terminar con muchos huecos pequeños que son difíciles de reutilizar (asignar a nuevas reservas). Por lo tanto, una de las soluciones que se utilizan en este caso es usar un grupo de memoria para asignar de antemano la memoria para los objetos de la aplicación. Cada vez que necesite memoria para algún objeto, solo utilizará la nueva ubicación para crear un nuevo objeto en la memoria ya reservada.

De esta manera, una vez que se inicie la aplicación, ya tendrá reservada toda la memoria necesaria. Toda la nueva reserva / liberación de memoria va a los grupos asignados (puede tener varios grupos, uno para cada clase de objeto diferente). En este caso, no se produce una fragmentación de la memoria, ya que no habrá huecos y su sistema puede funcionar durante largos períodos (años) sin sufrir fragmentación.

Vi esto en la práctica especialmente para el VxWorks RTOS ya que su sistema de asignación de memoria predeterminado sufre mucho de la fragmentación. Por lo tanto, la asignación de memoria a través del método new / malloc estándar estaba básicamente prohibida en el proyecto. Todas las reservas de memoria deben ir a un grupo de memoria dedicado.


Head Geek: ¡BINGO! Lo tienes totalmente, para eso es perfecto. En muchos entornos integrados, las restricciones externas y / o el escenario de uso general obliga al programador a separar la asignación de un objeto de su inicialización. Reunidos, C ++ llama a esto "instanciación"; pero siempre que la acción del constructor se debe invocar explícitamente SIN asignación dinámica o automática, la ubicación nueva es la forma de hacerlo. También es la forma perfecta de ubicar un objeto C ++ global que está anclado a la dirección de un componente de hardware (E / S asignada en memoria), o para cualquier objeto estático que, por cualquier motivo, debe residir en una dirección fija.


Lo he usado en programación en tiempo real. Normalmente , no queremos realizar ninguna asignación dinámica (o desasignación) después de que se inicie el sistema, porque no hay garantía de cuánto tiempo tomará.

Lo que puedo hacer es preasignar una gran cantidad de memoria (lo suficientemente grande como para contener cualquier cantidad de lo que la clase pueda requerir). Luego, una vez que descubro en tiempo de ejecución cómo construir las cosas, la ubicación nueva se puede usar para construir objetos justo donde los quiero. Una situación en la que sé que lo usé fue para ayudar a crear un búfer circular heterogéneo.

Ciertamente no es para los débiles de corazón, pero es por eso que hacen que la sintaxis sea un poco retorcida.


Lo he usado para construir objetos asignados en la pila a través de alloca ().

enchufe descarado: blogueé sobre esto here .


Es útil si desea separar la asignación de la inicialización. STL utiliza la ubicación nueva para crear elementos de contenedor.


La nueva ubicación también es muy útil cuando se serializa (por ejemplo, con boost :: serialización). En 10 años de c ++, este es solo el segundo caso en el que he necesitado una nueva ubicación (tercero si incluye entrevistas :)).


Es utilizado por std::vector<> porque std::vector<> normalmente asigna más memoria que objects en el vector<> .


Las siguientes pruebas se realizaron con el compilador de Visual C ++, ya que se utiliza en la instalación predeterminada de Qt Creator (supongo que sin indicador de optimización). Al usar GCC, no hay una gran diferencia entre la versión de Mystical y mi código "optimizado". Así que la conclusión es que las optimizaciones del compilador se ocupan de la micro optimización mejor que los humanos (por fin yo). Dejo el resto de mi respuesta para referencia.

No es eficiente procesar imágenes de esta manera. Es mejor usar matrices de una sola dimensión. El procesamiento de todos los píxeles es el hecho en un bucle. El acceso aleatorio a los puntos se puede hacer usando:

pointer + (x + y*width)*(sizeOfOnePixel)

En este caso particular, es mejor calcular y almacenar en caché la suma de tres grupos de píxeles horizontalmente porque se usan tres veces cada uno.

He hecho algunas pruebas y creo que vale la pena compartirlas. Cada resultado es un promedio de cinco pruebas.

Código original por usuario1615209:

8193: 4392 ms
8192: 9570 ms

La versión mística:

8193: 2393 ms
8192: 2190 ms

Dos pasadas utilizando una matriz 1D: primera pasada para sumas horizontales, segunda para suma vertical y promedio. Direccionamiento de dos pasadas con tres punteros y solo incrementos como este:

imgPointer1 = &avg1[0][0];
imgPointer2 = &avg1[0][SIZE];
imgPointer3 = &avg1[0][SIZE+SIZE];

for(i=SIZE;i<totalSize-SIZE;i++){
    resPointer[i]=(*(imgPointer1++)+*(imgPointer2++)+*(imgPointer3++))/9;
}

8193: 938 ms
8192: 974 ms

Dos pases usando una matriz 1D y direccionando así:

for(i=SIZE;i<totalSize-SIZE;i++){
    resPointer[i]=(hsumPointer[i-SIZE]+hsumPointer[i]+hsumPointer[i+SIZE])/9;
}

8193: 932 ms
8192: 925 ms

Un paso que almacena sumas horizontales solo una fila adelante para que permanezcan en el caché:

// Horizontal sums for the first two lines
for(i=1;i<SIZE*2;i++){
    hsumPointer[i]=imgPointer[i-1]+imgPointer[i]+imgPointer[i+1];
}
// Rest of the computation
for(;i<totalSize;i++){
    // Compute horizontal sum for next line
    hsumPointer[i]=imgPointer[i-1]+imgPointer[i]+imgPointer[i+1];
    // Final result
    resPointer[i-SIZE]=(hsumPointer[i-SIZE-SIZE]+hsumPointer[i-SIZE]+hsumPointer[i])/9;
}

8193: 599 ms
8192: 652 ms

Conclusión:

  • No hay beneficios de usar varios punteros y solo incrementos (pensé que habría sido más rápido)
  • Almacenar en caché las sumas horizontales es mejor que calcularlas varias veces.
  • Dos pasadas no son tres veces más rápidas, solo dos veces.
  • Es posible lograr 3.6 veces más rápido utilizando tanto una sola pasada como el almacenamiento en caché de un resultado intermedio

Estoy seguro de que es posible hacerlo mucho mejor.

NOTA Por favor, tenga en cuenta que escribí esta respuesta para tratar los problemas generales de rendimiento en lugar del problema de caché explicado en la excelente respuesta de Mystical. Al principio era solo un pseudo código. Me pidieron que hiciera pruebas en los comentarios ... Aquí hay una versión completamente refactorizada con pruebas.







c++ memory-management new-operator