Dentro del alcance del desarrollo de juegos en C ++, ¿cuáles son sus patrones preferidos con respecto al uso de punteros (ya sea ninguno, sin formato, con alcance, compartido o de otro modo entre inteligente y tonto)?
Usted puede considerar
- propiedad del objeto
- facilidad de uso
- política de copia
- gastos generales
- referencias cíclicas
- plataforma objetivo
- utilizar con contenedores
fuente
También sigo el tren de pensamiento de "propiedad fuerte". Me gusta delinear claramente que "esta clase posee este miembro" cuando es apropiado.
Raramente uso
shared_ptr
. Si lo hago, hago un uso generosoweak_ptr
siempre que puedo para poder tratarlo como un identificador del objeto en lugar de aumentar el recuento de referencias.Yo uso
scoped_ptr
todo el lugar. Muestra una propiedad obvia. La única razón por la que no solo hago objetos como ese miembro es porque puedes reenviarlos si están en un scoped_ptr.Si necesito una lista de objetos, los uso
ptr_vector
. Es más eficiente y tiene menos efectos secundarios que el usovector<shared_ptr>
. Creo que es posible que no pueda reenviar la declaración del tipo en el ptr_vector (ha pasado un tiempo), pero la semántica hace que valga la pena en mi opinión. Básicamente, si elimina un objeto de la lista, se elimina automáticamente. Esto también muestra una propiedad obvia.Si necesito referencia a algo, trato de hacer una referencia en lugar de un puntero desnudo. A veces esto no es práctico (es decir, cada vez que necesita una referencia después de que se construye el objeto). De cualquier manera, las referencias muestran obviamente que no es el propietario del objeto, y si sigue la semántica de puntero compartido en cualquier otro lugar, los punteros desnudos generalmente no causan ninguna confusión adicional (especialmente si sigue una regla de "no eliminar manualmente") .
Con este método, un juego de iPhone en el que trabajé solo podía tener una sola
delete
llamada, y eso fue en el puente Obj-C a C ++ que escribí.En general, opino que la gestión de la memoria es demasiado importante para dejarla a los humanos. Si puede automatizar la eliminación, debería hacerlo. Si la sobrecarga de shared_ptr es demasiado costosa en el tiempo de ejecución (suponiendo que desactivó el soporte de subprocesos, etc.), probablemente debería estar usando otra cosa (es decir, un patrón de depósito) para reducir sus asignaciones dinámicas.
fuente
Use la herramienta adecuada para el trabajo.
Si su programa puede lanzar excepciones, asegúrese de que su código tenga en cuenta las excepciones. Usar punteros inteligentes, RAII y evitar la construcción en 2 fases son buenos puntos de partida.
Si tiene referencias cíclicas sin una semántica de propiedad clara, puede considerar usar una biblioteca de recolección de basura o refactorizar su diseño.
Las buenas bibliotecas le permitirán codificar el concepto, no el tipo, por lo que no debería importar en la mayoría de los casos qué tipo de puntero está utilizando más allá de los problemas de gestión de recursos.
Si está trabajando en un entorno de subprocesos múltiples, asegúrese de comprender si su objeto es potencialmente compartido entre subprocesos. Una de las razones principales para considerar el uso de boost :: shared_ptr o std :: tr1 :: shared_ptr es porque utiliza un recuento de referencia seguro para subprocesos.
Si le preocupa la asignación separada de los recuentos de referencia, hay muchas maneras de evitar esto. Usando la biblioteca boost :: shared_ptr, puede asignar en conjunto los contadores de referencia o usar boost :: make_shared (mi preferencia) que asigna el objeto y el recuento de referencia en una sola asignación, aliviando así la mayoría de las preocupaciones de pérdida de caché que tienen las personas. Puede evitar el impacto de rendimiento de actualizar el recuento de referencias en el código crítico de rendimiento manteniendo una referencia al objeto en el nivel más alto y pasando referencias directas al objeto.
Si necesita una propiedad compartida pero no quiere pagar el costo del recuento de referencias o la recolección de basura, considere usar objetos inmutables o una copia en el idioma de escritura.
Tenga en cuenta que, de lejos, sus mayores logros de rendimiento serán a nivel de arquitectura, seguidos de un nivel de algoritmo, y si bien estas preocupaciones de bajo nivel son muy importantes, deben abordarse solo después de que haya abordado los principales problemas. Si está lidiando con problemas de rendimiento en el nivel de errores de caché, entonces tiene una gran cantidad de problemas que también debe tener en cuenta, como el intercambio falso que no tiene nada que ver con los punteros por decir.
Si está utilizando punteros inteligentes solo para compartir recursos como texturas o modelos, considere una biblioteca más especializada como Boost.Flyweight.
Una vez que se adopte el nuevo estándar, la semántica de movimiento, las referencias de valor y el reenvío perfecto harán que trabajar con objetos y contenedores caros sea mucho más fácil y más eficiente. Hasta entonces, no almacene punteros con semántica de copia destructiva, como auto_ptr o unique_ptr, en un Contenedor (el concepto estándar). Considere usar la biblioteca Boost.Pointer Container o almacenar punteros inteligentes de propiedad compartida en Contenedores. En el código crítico de rendimiento, puede considerar evitar ambos a favor de contenedores intrusivos como los de Boost.Intrusive.
La plataforma objetivo realmente no debería influir demasiado en su decisión. Los dispositivos integrados, teléfonos inteligentes, teléfonos tontos, PC y consolas pueden ejecutar el código perfectamente. Los requisitos del proyecto, como presupuestos estrictos de memoria o ninguna asignación dinámica alguna vez / después de la carga, son preocupaciones más válidas y deberían influir en sus elecciones.
fuente
Si usa C ++ 0x, use
std::unique_ptr<T>
.No tiene sobrecarga de rendimiento, a diferencia de lo
std::shared_ptr<T>
que tiene sobrecarga de conteo de referencia. Un unique_ptr posee su puntero, y puede transferir la propiedad con la semántica de movimiento de C ++ 0x . No puedes copiarlos, solo moverlos.También se puede usar en contenedores, por ejemplo
std::vector<std::unique_ptr<T>>
, que es compatible con binarios e idéntico en rendimientostd::vector<T*>
, pero no perderá memoria si borra elementos o borra el vector. Esto también tiene una mejor compatibilidad con algoritmos STL queptr_vector
.OMI para muchos propósitos, este es un contenedor ideal: acceso aleatorio, excepción segura, evita pérdidas de memoria, baja sobrecarga para la reasignación de vectores (simplemente baraja los punteros detrás de escena). Muy útil para muchos propósitos.
fuente
Es una buena práctica documentar qué clases poseen qué punteros. Preferiblemente, solo usa objetos normales y no punteros siempre que puede.
Sin embargo, cuando necesita hacer un seguimiento de los recursos, pasar punteros es la única opción. Hay algunos casos:
Creo que eso cubre más o menos cómo administro mis recursos en este momento. El costo de memoria de un puntero como shared_ptr es generalmente el doble del costo de memoria de un puntero normal. No creo que esta sobrecarga sea demasiado grande, pero si tienes pocos recursos, deberías considerar diseñar tu juego para reducir la cantidad de punteros inteligentes. En otros casos, solo diseño buenos principios como las viñetas anteriores y el generador de perfiles me dirá dónde necesitaré más velocidad.
fuente
Cuando se trata específicamente de los indicadores de impulso, creo que deberían evitarse siempre que su implementación no sea exactamente lo que necesita. Tienen un costo que es más grande de lo que cualquiera esperaría inicialmente. Proporcionan una interfaz que le permite omitir partes vitales e importantes de su memoria y gestión de recursos.
Cuando se trata de cualquier desarrollo de software, creo que es importante pensar en sus datos. Es muy importante cómo se representan sus datos en la memoria. La razón de esto es que la velocidad de la CPU ha aumentado a un ritmo mucho mayor que el tiempo de acceso a la memoria. Esto a menudo hace que las memorias caché sean el principal cuello de botella de la mayoría de los juegos de computadora modernos. Al tener sus datos alineados linealmente en la memoria de acuerdo con el orden de acceso, es mucho más amigable con el caché. Este tipo de soluciones a menudo conducen a diseños más limpios, un código más simple y un código definitivamente más fácil de depurar. Los punteros inteligentes conducen fácilmente a asignaciones frecuentes de memoria dinámica de recursos, lo que hace que se dispersen por toda la memoria.
Esta no es una optimización prematura, es una decisión saludable que puede y debe tomarse lo antes posible. Es una cuestión de comprensión arquitectónica del hardware en el que se ejecutará su software y es importante.
Editar: Hay algunas cosas a tener en cuenta con respecto al rendimiento de los punteros compartidos:
fuente
Tiendo a usar punteros inteligentes en todas partes. No estoy seguro de si esta es una idea totalmente buena, pero soy flojo y no puedo ver ningún inconveniente real [excepto si quisiera hacer algo de aritmética de puntero estilo C]. Uso boost :: shared_ptr porque sé que puedo copiarlo: si dos entidades comparten una imagen, si una muere, la otra no debería perder la imagen también.
La desventaja de esto es que si un objeto elimina algo a lo que señala y posee, pero algo más también lo señala, entonces no se elimina.
fuente
Los beneficios de la gestión de la memoria y la documentación proporcionada por buenos punteros inteligentes significan que los uso regularmente. Sin embargo, cuando el generador de perfiles se abre paso y me dice que un uso particular me está costando, volveré a una gestión de puntero más neolítica.
fuente
Soy viejo, oldskool y contador de ciclos. En mi propio trabajo, uso punteros sin procesar y sin asignaciones dinámicas en tiempo de ejecución (excepto los grupos en sí). Todo está agrupado, y la propiedad es muy estricta y nunca transferible, si realmente es necesario, escribo un asignador de bloque pequeño personalizado. Me aseguro de que haya un estado durante el juego para que cada grupo se elimine. Cuando las cosas se ponen difíciles, envuelvo los objetos en los mangos para poder reubicarlos, pero prefiero no hacerlo. Los contenedores son personalizados y huesos extremadamente desnudos. Tampoco reutilizo el código.
Si bien nunca discutiría la virtud de todos los punteros inteligentes, contenedores e iteradores y demás, soy conocido por poder codificar extremadamente rápido (y razonablemente confiable, aunque no es aconsejable que otros salten a mi código por razones algo obvias, como ataques al corazón y pesadillas perpetuas).
En el trabajo, por supuesto, todo es diferente, a menos que esté haciendo prototipos, que afortunadamente puedo hacer mucho.
fuente
Sin embargo, casi ninguna es una respuesta extraña, y probablemente ni mucho menos adecuada para todos.
Pero he encontrado que es mucho más útil en mi caso personal almacenar todas las instancias de un tipo particular en una secuencia central de acceso aleatorio (seguro para subprocesos) y, en cambio, trabajar con índices de 32 bits (direcciones relativas, es decir) , en lugar de punteros absolutos.
Para comenzar:
T
nunca se dispersen demasiado en la memoria. Eso tiende a reducir las pérdidas de caché para todo tipo de patrones de acceso, incluso atravesando estructuras vinculadas como árboles si los nodos se vinculan entre sí mediante índices en lugar de punteros.Dicho esto, la conveniencia es una desventaja, así como la seguridad de tipo. No podemos acceder a una instancia de
T
sin tener acceso tanto al contenedor como al índice. Y un viejoint32_t
no nos dice nada sobre el tipo de datos al que se refiere, por lo que no hay seguridad de tipo. Podríamos intentar acceder accidentalmente aBar
utilizando un índice paraFoo
. Para mitigar el segundo problema, a menudo hago este tipo de cosas:Lo que parece un poco tonto, pero me devuelve la seguridad de tipo para que las personas no puedan intentar acceder accidentalmente a
Bar
través de un índiceFoo
sin un error del compilador. Por conveniencia, acepto las pequeñas molestias.Otra cosa que podría ser un gran inconveniente para las personas es que no puedo usar el polimorfismo basado en la herencia de estilo OOP, ya que eso requeriría un puntero base que pueda apuntar a todo tipo de subtipos diferentes con diferentes requisitos de tamaño y alineación. Pero no uso mucho la herencia en estos días, prefiero el enfoque ECS.
En cuanto a
shared_ptr
, trato de no usarlo tanto. La mayoría de las veces no creo que tenga sentido compartir la propiedad, y hacerlo al azar puede conducir a fugas lógicas. A menudo, al menos en un nivel superior, una cosa tiende a pertenecer a una sola. Donde a menudo me pareció tentador usarshared_ptr
era extender la vida útil de un objeto en lugares que realmente no trataban tanto con la propiedad, como solo una función local en un hilo para asegurarse de que el objeto no se destruya antes de que el hilo termine usándoloPara abordar ese problema, en lugar de usar
shared_ptr
GC o algo así, a menudo prefiero las tareas de corta duración que se ejecutan desde un grupo de subprocesos, y hago que si ese subproceso solicite destruir un objeto, la destrucción real se difiera a una caja fuerte momento en que el sistema puede garantizar que ningún hilo necesite acceder a dicho tipo de objeto.Todavía a veces termino usando el recuento de ref, pero lo trato como una estrategia de último recurso. Y hay algunos casos en los que realmente tiene sentido compartir la propiedad, como la implementación de una estructura de datos persistente, y creo que tiene mucho sentido llegar de
shared_ptr
inmediato.De todos modos, en su mayoría uso índices, y uso punteros crudos e inteligentes con moderación. Me gustan los índices y el tipo de puertas que se abren cuando sabes que tus objetos se almacenan de forma contigua y no se encuentran dispersos en el espacio de la memoria.
fuente