c++ - guia - ¿Por qué uno debería reemplazar operadores predeterminados nuevos y eliminar?
qgis manual (7)
¿Por qué debería uno reemplazar el operador predeterminado new
y delete
con un operador personalizado new
y delete
?
Esto es una continuación de Sobrecarga nueva y eliminar en las Preguntas frecuentes de C ++ inmensamente esclarecedoras:
Sobrecarga del operador.
Una entrada de seguimiento a esta pregunta frecuente es:
¿Cómo debo escribir los operadores new
y delete
personalizados conforme a la norma ISO C ++?
Nota: La respuesta se basa en las lecciones de C ++ más efectivo de Scott Meyers.
(Nota: Esto debe ser una entrada a las preguntas frecuentes de C ++ de Stack Overflow . Si desea criticar la idea de proporcionar una pregunta frecuente en este formulario, entonces la publicación en meta que inició todo esto sería el lugar para hacerlo). esa pregunta se monitorea en la sala de chat de C ++ , donde la idea de las preguntas frecuentes comenzó en primer lugar, por lo que es muy probable que su respuesta sea leída por aquellos a quienes se les ocurrió la idea).
El operador nuevo que envía con algunos compiladores no garantiza la alineación de ocho bytes para las asignaciones dinámicas de dobles.
Cita, por favor Normalmente, el nuevo operador predeterminado es solo un poco más complejo que un contenedor malloc, que, según el estándar, devuelve la memoria que está adecuadamente alineada para CUALQUIER tipo de datos que admita la arquitectura de destino.
No es que esté diciendo que no hay buenas razones para sobrecargar nuevas y eliminar para las propias clases ... y has tocado varias legítimas aquí, pero lo anterior no es una de ellas.
Muchas arquitecturas de computadora requieren que los datos de tipos particulares se coloquen en la memoria en clases particulares de direcciones. Por ejemplo, una arquitectura puede requerir que los punteros ocurran en direcciones que son múltiplos de cuatro (es decir, que estén alineados en cuatro bytes) o que los dobles tengan lugar en direcciones que sean múltiplo de ocho (es decir, que estén alineadas en ocho bytes). El incumplimiento de tales restricciones puede generar excepciones de hardware en tiempo de ejecución. Otras arquitecturas son más indulgentes, y pueden permitir que funcione aunque reduciendo el rendimiento.
Para aclarar: si una arquitectura requiere, por ejemplo, que los datos double
estén alineados en ocho bytes, entonces no hay nada que optimizar. Cualquier tipo de asignación dinámica del tamaño apropiado (p. Ej. malloc(size)
, operator new(size)
, operator new[](size)
, new char[size]
donde size >= sizeof(double)
) esté garantizado que esté alineado correctamente . Si una implementación no hace esta garantía, no es conforme. Cambiar el operator new
para hacer ''lo correcto'' en ese caso sería un intento de ''arreglar'' la implementación, no una optimización.
Por otro lado, algunas arquitecturas permiten diferentes (o todos) los tipos de alineación para uno o más tipos de datos, pero proporcionan diferentes garantías de rendimiento dependiendo de la alineación para esos mismos tipos. Una implementación puede devolver la memoria (nuevamente, asumiendo una solicitud del tamaño apropiado) que está sub-óptimamente alineada, y aún se está conformando. De esto es de lo que trata el ejemplo.
En primer lugar, hay realmente una cantidad de operadores diferentes new
y de delete
(un número arbitrario, en realidad).
Primero, hay ::operator new
, ::operator new[]
, ::operator delete
y ::operator delete[]
. Segundo, para cualquier clase X
, hay X::operator new
, X::operator new[]
, X::operator delete
y X::operator delete[]
.
Entre estos, es mucho más común sobrecargar a los operadores de clase que a los operadores globales: es bastante común que el uso de memoria de una clase en particular siga un patrón lo suficientemente específico como para escribir operadores que proporcionan mejoras sustanciales sobre los valores predeterminados. En general, es mucho más difícil predecir el uso de la memoria casi de manera precisa o específica a nivel mundial.
Probablemente también valga la pena mencionar que aunque el operator new
y el operator new[]
están separados el uno del otro (del mismo modo para cualquier X::operator new
y X::operator new[]
), no hay diferencia entre los requisitos para los dos. Uno se invocará para asignar un único objeto y el otro para asignar una matriz de objetos, pero cada uno aún recibe una cantidad de memoria necesaria y debe devolver la dirección de un bloque de memoria (al menos) tan grande.
Hablando de requisitos, es probable que valga la pena revisar los otros requisitos 1 : los operadores globales deben ser verdaderamente globales: no puede poner uno dentro de un espacio de nombres o hacer uno estático en una unidad de traducción particular. En otras palabras, solo hay dos niveles en los que pueden producirse sobrecargas: una sobrecarga específica de clase o una sobrecarga global. Los puntos intermedios como "todas las clases en el espacio de nombres X" o "todas las asignaciones en la unidad de traducción Y" no están permitidos. Se requiere que los operadores específicos de clase sean static
, pero en realidad no se requiere que los declare como estáticos, serán estáticos ya sea que los declare explícitamente static
o no. Oficialmente, los operadores globales devuelven la memoria alineada para que pueda ser utilizada para un objeto de cualquier tipo. Extraoficialmente, hay un poco de margen de maniobra en un aspecto: si obtiene una solicitud de un bloque pequeño (por ejemplo, 2 bytes), solo necesita proporcionar memoria alineada para un objeto de ese tamaño, ya que intenta almacenar algo más grande allí. conduciría a un comportamiento indefinido de todos modos.
Habiendo cubierto esos preliminares, volvamos a la pregunta original sobre por qué querría sobrecargar estos operadores. Primero, debo señalar que las razones para sobrecargar a los operadores globales tienden a ser sustancialmente diferentes de las razones para sobrecargar a los operadores específicos de clase.
Como es más común, primero hablaré sobre los operadores específicos de clase. La razón principal para la gestión de memoria específica de clase es el rendimiento. Esto comúnmente viene en una (o ambas) dos formas: ya sea mejorando la velocidad o reduciendo la fragmentación. La velocidad se mejora por el hecho de que el administrador de memoria solo tratará con bloques de un tamaño particular, por lo que puede devolver la dirección de cualquier bloque libre en lugar de perder tiempo para verificar si un bloque es lo suficientemente grande, dividiendo un bloque en dos si es demasiado grande, etc. La fragmentación se reduce (principalmente) de la misma manera; por ejemplo, la asignación previa de un bloque lo suficientemente grande para N objetos proporciona exactamente el espacio necesario para N objetos; la asignación del valor de memoria de un objeto asignará exactamente el espacio para un objeto, y no un byte más.
Hay una variedad mucho mayor de razones para sobrecargar a los operadores de administración de memoria global. Muchos de estos están orientados a la depuración o instrumentación, como el seguimiento de la memoria total que necesita una aplicación (por ejemplo, en preparación para la transferencia a un sistema integrado) o la depuración de problemas de memoria al mostrar desajustes entre asignar y liberar memoria. Otra estrategia común es asignar memoria extra antes y después de los límites de cada bloque solicitado, y escribir patrones únicos en esas áreas. Al final de la ejecución (y posiblemente en otras ocasiones también), esas áreas se examinan para ver si el código ha escrito fuera de los límites asignados. Otra más es intentar mejorar la facilidad de uso automatizando al menos algunos aspectos de la asignación o eliminación de memoria, como con un recolector de basura automatizado .
También se puede usar un asignador global no predeterminado para mejorar el rendimiento. Un caso típico sería reemplazar un asignador predeterminado que fuera simplemente lento en general (por ejemplo, al menos algunas versiones de MS VC ++ alrededor de 4.x llamarían a las funciones HeapAlloc
y HeapFree
del sistema para cada operación de asignación / eliminación). Otra posibilidad que he visto en la práctica se produjo en los procesadores Intel al usar las operaciones SSE. Estos operan en datos de 128 bits. Si bien las operaciones funcionarán independientemente de la alineación, la velocidad se mejora cuando los datos están alineados con los límites de 128 bits. Algunos compiladores (p. Ej., MS VC ++ otra vez 2 ) no necesariamente han aplicado la alineación a ese límite más grande, por lo que aunque el código que utiliza el asignador predeterminado funcionaría, reemplazar la asignación podría proporcionar una mejora de velocidad sustancial para esas operaciones.
- La mayoría de los requisitos están cubiertos en §3.7.3 y §18.4 del estándar C ++ (o §3.7.4 y §18.6 en C ++ 0x, al menos a partir de N3291).
- Me siento obligado a señalar que no tengo la intención de elegir el compilador de Microsoft. Dudo que tenga una cantidad inusual de tales problemas, pero lo uso mucho, por lo que tiendo a ser bastante consciente de sus problemas.
Lo usé para asignar objetos en una arena de memoria compartida específica. (Esto es similar a lo que @Russell Borogove mencionó).
Hace años desarrollé software para CAVE . Es un sistema VR de pared múltiple. Usó una computadora para manejar cada proyector; 6 era el máximo (4 paredes, piso y techo) mientras que 3 era más común (2 paredes y el piso). Las máquinas se comunicaron a través de hardware especial de memoria compartida.
Para apoyarlo, obtuve de mis clases de escena normales (no CAVE) el uso de un nuevo "nuevo" que coloca la información de la escena directamente en la arena de la memoria compartida. Luego pasé ese puntero a los renderizadores de esclavos en las diferentes máquinas.
Merece la pena repetir la lista de mi respuesta de "¿Hay alguna razón para sobrecargar global nuevo y eliminar?" aquí - vea esa respuesta (o, de hecho, otras respuestas a esa pregunta ) para una discusión más detallada, referencias y otras razones. Estas razones generalmente se aplican a las sobrecargas del operador local así como a las predeterminadas / globales, y a las sobrecargas o ganchos C malloc
/ calloc
/ realloc
/ free
también.
Sobrecargamos los operadores globales nuevos y eliminados donde trabajo por varias razones:
- agrupación de todas las asignaciones pequeñas: disminuye los gastos generales, disminuye la fragmentación y puede aumentar el rendimiento de las aplicaciones pequeñas y cargadas de objetos pesados
- enmarcar asignaciones con una vida útil conocida - ignorar todas las liberaciones hasta el final de este período, luego liberarlas todas juntas (de hecho, hacemos esto más con sobrecargas de operadores locales que globales)
- ajuste de alineación - a límites de caché, etc.
- alloc fill : ayuda a exponer el uso de variables no inicializadas
- relleno libre - ayuda a exponer el uso de memoria borrada previamente
- retraso libre : aumenta la eficacia del relleno libre, ocasionalmente aumenta el rendimiento
- centinelas o postes de cerca - ayudando a exponer los desbordamientos de búfer, infrautilizaciones y ocasionales punteros salvajes
- Redirigir las asignaciones: para tener en cuenta NUMA, áreas de memoria especiales o incluso para mantener separados los sistemas por separado en la memoria (por ejemplo, lenguajes de scripts incorporados o DSL)
- Recolección de basura o limpieza: una vez más útil para los lenguajes de scripting incrustados
- Verificación del montón : puede recorrer la estructura de datos del montón cada N allocs / libres para asegurarse de que todo se ve bien
- contabilidad , incluido el seguimiento de fugas y las instantáneas / estadísticas de uso (pilas, edades de asignación, etc.)
Relacionado con las estadísticas de uso: presupuestación por subsistema. Por ejemplo, en un juego basado en una consola, es posible que desee reservar una fracción de memoria para la geometría del modelo 3D, algunas para texturas, otras para sonidos, otras para guiones de juegos, etc. Los asignadores personalizados pueden etiquetar cada asignación por subsistema y emitir un advertencia cuando se exceden los presupuestos individuales.
Uno puede intentar reemplazar operadores new
y delete
por varias razones, a saber:
Para detectar errores de uso:
Hay una serie de formas en que el uso incorrecto de new
y delete
puede conducir a las bestias temidas de las fugas de Memoria y Comportamiento Indefinido . Ejemplos respectivos de cada uno son:
Usar más de una delete
en la new
memoria ed y no llamar a delete
en la memoria asignada usando new
.
Un operador new
sobrecargado puede mantener una lista de direcciones asignadas y el operador sobrecargado delete
puede eliminar direcciones de la lista, entonces es fácil detectar tales errores de uso.
Del mismo modo, una variedad de errores de programación puede llevar a sobrepasar los datos (escribir más allá del final de un bloque asignado) y a errores (escribir antes del comienzo de un bloque asignado).
Un operador sobrecargado new
puede sobreasignar bloques y colocar patrones de bytes conocidos ("firmas") antes y después de que la memoria esté disponible para los clientes. El operador sobrecargado elimina puede verificar si las firmas aún están intactas. Por lo tanto, al verificar si estas firmas no están intactas, es posible determinar que ocurrió un desbordamiento o subutilización en algún momento durante la vida del bloque asignado, y el operador delete puede registrar ese hecho, junto con el valor del puntero infractor, ayudando así en proporcionar una buena información de diagnóstico.
Para mejorar la eficiencia (velocidad y memoria):
Los operadores new
y delete
funcionan razonablemente bien para todos, pero de manera óptima para nadie. Este comportamiento surge del hecho de que están diseñados solo para uso general. Deben adaptarse a los patrones de asignación que van desde la asignación dinámica de unos pocos bloques que existen durante la duración del programa hasta la asignación constante y la desasignación de un gran número de objetos efímeros. Eventualmente, el operador new
y el operador delete
esa nave con los compiladores que toman una estrategia de medio camino.
Si comprende bien los patrones de uso de la memoria dinámica de su programa, a menudo puede encontrar que las versiones personalizadas del operador nuevo y la eliminación del operador tienen un rendimiento superior (más rápido en rendimiento, o requieren menos memoria hasta en un 50%) que las predeterminadas. Por supuesto, a menos que esté seguro de lo que está haciendo, no es una buena idea hacerlo (ni siquiera intente esto si no comprende las complejidades involucradas).
Para recopilar estadísticas de uso:
Antes de pensar en reemplazar new
y delete
para mejorar la eficiencia como se menciona en el n. ° 2, debe recopilar información sobre cómo su aplicación / programa usa la asignación dinámica. Es posible que desee recopilar información sobre:
Distribución de bloques de asignación,
Distribución de tiempos de vida,
Orden de asignaciones (FIFO o LIFO o al azar),
Comprender los cambios en los patrones de uso durante un período de tiempo, la cantidad máxima de memoria dinámica utilizada, etc.
Además, a veces puede necesitar recopilar información de uso como, por ejemplo:
Cuenta el número de objetos dinámicamente de una clase,
Restrinja la cantidad de objetos que se están creando mediante la asignación dinámica, etc.
En general, esta información se puede recopilar reemplazando el new
y personalizado, y agregando el mecanismo de recopilación de diagnósticos en el new
y sobrecargado sobrecargado.
Para compensar la alineación de memoria subóptima en new
:
Muchas arquitecturas de computadora requieren que los datos de tipos particulares se coloquen en la memoria en clases particulares de direcciones. Por ejemplo, una arquitectura puede requerir que los punteros ocurran en direcciones que son múltiplos de cuatro (es decir, que estén alineados en cuatro bytes) o que los dobles tengan lugar en direcciones que sean múltiplo de ocho (es decir, que estén alineadas en ocho bytes). El incumplimiento de tales restricciones puede generar excepciones de hardware en tiempo de ejecución. Otras arquitecturas son más indulgentes y pueden permitir que funcione aunque reduzca el rendimiento. El operador new
que envía con algunos compiladores no garantiza la alineación de ocho bytes para las asignaciones dinámicas de dobles. En tales casos, reemplazar el operador predeterminado new
por uno que garantice la alineación de ocho bytes podría generar grandes aumentos en el rendimiento del programa y puede ser una buena razón para reemplazar operadores new
y delete
.
Para agrupar objetos relacionados cerca uno del otro:
Si sabe que las estructuras de datos particulares generalmente se utilizan juntas y desea minimizar la frecuencia de las fallas de página al trabajar con los datos, puede tener sentido crear un montón separado para las estructuras de datos para agruparlas en unas pocas. páginas como sea posible. Las versiones de colocación personalizadas de new
y delete
pueden permitir dicho agrupamiento.
Para obtener un comportamiento no convencional:
Algunas veces quiere operadores nuevos y eliminar para hacer algo que las versiones proporcionadas por el compilador no ofrecen.
Por ejemplo: puede escribir una delete
operador personalizado que sobrescribe la memoria desasignada con ceros para aumentar la seguridad de los datos de la aplicación.