c++ - tiradero - servicio de recolección de basura
¿Por qué la recolección de basura cuando RAII está disponible? (7)
Escucho conversaciones de C ++ 14 presentando un recolector de basura en la biblioteca estándar de C ++. ¿Cuál es la razón de esta característica? ¿No es esta la razón por la que RAII existe en C ++?
- ¿Cómo afectará la presencia del recolector de basura de la biblioteca estándar a la semántica RAII?
- ¿Cómo me importa (el programador) o la forma en que escribo los programas en C ++?
Estoy de acuerdo con @DeadMG en que no hay GC en el estándar actual de C ++, pero me gustaría agregar la siguiente cita de B. Stroustrup:
Cuando (no si) la recolección automática de basura se vuelve parte de C ++, será opcional
Entonces, Bjarne está seguro de que se agregará en el futuro. Al menos el presidente del EWG (Evolution Working Group) y uno de los miembros más importantes del comité (y más importante, creador del lenguaje) quiere agregarlo.
A menos que cambie su opinión, podemos esperar que se agregue e implemente en el futuro.
GC tiene las siguientes ventajas:
- Puede manejar referencias circulares sin asistencia de programador (con estilo RAII, debe usar weak_ptr para romper círculos). Entonces, una aplicación de estilo RAII aún puede "fugarse" si se usa de forma incorrecta.
- Crear / destruir toneladas de shared_ptr''s para un objeto dado puede ser costoso porque el incremento / decremento de recuento son operaciones atómicas. En aplicaciones de subprocesos múltiples, las ubicaciones de memoria que contienen recuentos serán lugares "calientes", lo que ejercerá una gran presión sobre el subsistema de memoria. GC no es propenso a este problema específico, ya que utiliza conjuntos accesibles en lugar de recuentos.
No estoy diciendo que GC sea la mejor / buena opción. Solo digo que tiene diferentes características. En algunos escenarios eso podría ser una ventaja.
Hay algunos algoritmos que son complicados / ineficientes / imposibles de escribir sin un GC. Sospecho que este es el principal argumento de venta de GC en C ++, y nunca lo veo utilizado como un asignador de propósito general.
¿Por qué no un asignador de propósito general?
Primero, tenemos RAII, y la mayoría (incluido yo) parece creer que este es un método superior de gestión de recursos. Nos gusta el determinismo porque hace que la escritura sólida, el código libre de fugas sea mucho más simple y hace que el rendimiento sea predecible.
En segundo lugar, deberá colocar algunas restricciones muy poco similares a C ++ sobre cómo puede usar la memoria. Por ejemplo, necesitaría al menos un puntero accesible y no ofuscado. Los punteros ofuscados, como son populares en las bibliotecas de contenedores de árbol común (utilizando bits bajos garantizados por alineación para indicadores de color), entre otros, no serán reconocibles por el GC.
En relación con eso, las cosas que hacen que los GC modernos sean tan utilizables serán muy difíciles de aplicar a C ++ si admite cualquier cantidad de punteros ofuscados. Los GC desfragmentadores generacionales son geniales, porque la asignación es extremadamente económica (básicamente solo incrementa el puntero) y, finalmente, sus asignaciones se compactan en algo más pequeño con una localidad mejorada. Para hacer esto, los objetos deben ser movibles.
Para hacer que un objeto se mueva de forma segura, el GC necesita poder actualizar todos los indicadores a él. No podrá encontrar los ofuscados. Esto podría acomodarse, pero no sería bonito (probablemente un tipo gc_pin
o similar, usado como std::lock_guard
actual, que se usa siempre que necesite un puntero sin formato). La usabilidad estaría fuera de la puerta.
Sin hacer las cosas movibles, un GC sería significativamente más lento y menos escalable de lo que está acostumbrado en otro lugar.
Razones de la usabilidad (administración de recursos) y de la eficiencia (asignaciones rápidas, movibles) fuera del camino, ¿para qué más sirve GC? Ciertamente no de propósito general. Ingrese algoritmos sin bloqueo.
¿Por qué sin cerrojo?
Los algoritmos de bloqueo funcionan al permitir que una operación bajo contención se desconecte temporalmente de la estructura de datos y la detecte / corrija en un paso posterior. Un efecto de esto es que bajo la memoria de contención se puede acceder después de que se ha eliminado. Por ejemplo, si tiene varios hilos que compiten para abrir un nodo desde un LIFO, es posible que un hilo revele y elimine el nodo antes de que otro hilo se haya dado cuenta de que el nodo ya estaba ocupado:
Tema A:
- Obtener el puntero al nodo raíz.
- Obtenga el puntero al siguiente nodo desde el nodo raíz.
- Suspender
Hilo B:
- Obtener el puntero al nodo raíz.
- Suspender
Tema A:
- Nodo Pop (reemplace el puntero del nodo raíz con el siguiente puntero del nodo, si el puntero del nodo raíz no ha cambiado desde que se leyó).
- Eliminar nodo.
- Suspender
Hilo B:
- Obtenga el puntero al siguiente nodo de nuestro puntero del nodo raíz, que ahora está "fuera de sincronización" y se eliminó, por lo que fallamos.
Con GC, puede evitar la posibilidad de leer desde la memoria no confirmada, ya que el nodo nunca se eliminará mientras el Subproceso B hace referencia a él. Hay formas de evitar esto, como los indicadores de peligro o la captura de excepciones SEH en Windows, pero estos pueden perjudicar el rendimiento de manera significativa. GC tiende a ser la solución más óptima aquí.
La recolección de basura y RAII son útiles en diferentes contextos. La presencia de GC no debería afectar su uso de RAII. Como RAII es bien conocido, doy dos ejemplos donde GC es útil.
La recolección de basura sería de gran ayuda para implementar estructuras de datos sin bloqueo.
[...] resulta que la liberación determinística de la memoria es un problema bastante fundamental en las estructuras de datos sin bloqueo. (from Lock-Free Data Structures Por Andrei Alexandrescu)
Básicamente, el problema es que debes asegurarte de no desasignar la memoria mientras un hilo la está leyendo. Ahí es donde GC se vuelve práctico: puede mirar los hilos y solo desasignar cuando es seguro. Por favor, lea el artículo para más detalles.
Para que quede claro aquí, no significa que TODO EL MUNDO debería recogerse basura como en Java; solo los datos relevantes deben ser recogidos basura con precisión.
En una de sus presentaciones, Bjarne Stroustrup también dio un buen ejemplo válido en el que GC se vuelve útil. Imagine una aplicación escrita en C / C ++, 10M SLOC en tamaño. La aplicación funciona razonablemente bien (bastante libre de errores) pero tiene fugas. No tienes los recursos (horas hombre) ni el conocimiento funcional para arreglar esto. El código fuente es un código heredado algo desordenado. ¿Qué haces? Estoy de acuerdo en que es quizás la forma más fácil y económica de resolver el problema bajo la alfombra con GC.
Como ha sido señalado por sasha.sochka , el recolector de basura será opcional .
Mi preocupación personal es que las personas comenzarían a usar GC como si fuera usado en Java y escribirían código descuidado y la basura recolectaría todo. (Tengo la impresión de que shared_ptr
ya se ha convertido en el predeterminado ''go to'' incluso en los casos en que unique_ptr
o, infierno, la asignación de la pila lo haría).
Ninguna de las respuestas hasta el momento toca el beneficio más importante de agregar recolección de basura a un idioma: en ausencia de recolección de basura con soporte de lenguaje, es casi imposible garantizar que ningún objeto se destruirá mientras que las referencias a él existan. Peor aún, si tal cosa sucede, es casi imposible garantizar que un intento posterior de usar la referencia no termine manipulando algún otro objeto aleatorio.
Aunque hay muchos tipos de objetos cuya vida útil puede ser mucho mejor administrada por RAII que por un recolector de basura, hay un valor considerable en que el GC administre casi todos los objetos, incluidos aquellos cuya vida útil está controlada por RAII . El destructor de un objeto debería matar al objeto y hacerlo inútil, pero dejar el cadáver para el GC. Cualquier referencia al objeto se convertirá así en una referencia al cadáver, y seguirá siendo uno hasta que (la referencia) deje de existir por completo. Solo cuando todas las referencias al cadáver hayan dejado de existir lo hará el cadáver.
Si bien hay formas de implementar recolectores de basura sin soporte de lenguaje inherente, tales implementaciones requieren que se informe al GC en cualquier momento que las referencias se creen o destruyan (agregando un considerable inconveniente y sobrecarga), o se corre el riesgo de que el GC no se conozca sobre podría existir a un objeto que de otra manera no se referencia. El soporte del compilador para GC elimina ambos problemas.
No hay, porque no hay uno. Las únicas características que C ++ alguna vez tuvo para GC se introdujeron en C ++ 11 y solo marcan la memoria, no se requiere un recopilador. Tampoco habrá en C ++ 14.
No hay forma de que un coleccionista pueda aprobar el Comité, es mi opinión.
Definiciones:
RCB GC: GC basado en el recuento de referencias.
MSB GC: GC basado en el barrido de marcas.
Respuesta rápida:
MSB GC debe agregarse al estándar C ++, porque es más útil que RCB GC en ciertos casos.
Dos ejemplos ilustrativos:
Considere un búfer global cuyo tamaño inicial es pequeño, y cualquier subproceso puede ampliar dinámicamente su tamaño y mantener el contenido anterior accesible para otros subprocesos.
Implementación 1 (MSB GC Version):
int* g_buf = 0;
size_t g_current_buf_size = 1024;
void InitializeGlobalBuffer()
{
g_buf = gcnew int[g_current_buf_size];
}
int GetValueFromGlobalBuffer(size_t index)
{
return g_buf[index];
}
void EnlargeGlobalBufferSize(size_t new_size)
{
if (new_size > g_current_buf_size)
{
auto tmp_buf = gcnew int[new_size];
memcpy(tmp_buf, g_buf, g_current_buf_size * sizeof(int));
std::swap(tmp_buf, g_buf);
}
}
Implementación 2 (RCB GC Version):
std::shared_ptr<int> g_buf;
size_t g_current_buf_size = 1024;
std::shared_ptr<int> NewBuffer(size_t size)
{
return std::shared_ptr<int>(new int[size], []( int *p ) { delete[] p; });
}
void InitializeGlobalBuffer()
{
g_buf = NewBuffer(g_current_buf_size);
}
int GetValueFromGlobalBuffer(size_t index)
{
return g_buf[index];
}
void EnlargeGlobalBufferSize(size_t new_size)
{
if (new_size > g_current_buf_size)
{
auto tmp_buf = NewBuffer(new_size);
memcpy(tmp_buf, g_buf, g_current_buf_size * sizeof(int));
std::swap(tmp_buf, g_buf);
//
// Now tmp_buf owns the old g_buf, when tmp_buf is destructed,
// the old g_buf will also be deleted.
//
}
}
TENGA EN CUENTA:
Después de llamar a std::swap(tmp_buf, g_buf);
, tmp_buf
posee el antiguo g_buf
. Cuando tmp_buf
es destruido, el viejo g_buf
también será eliminado.
Si otro hilo llama a GetValueFromGlobalBuffer(index);
para recuperar el valor del antiguo g_buf
, entonces se presentará un peligro racial.
Entonces, aunque la implementación 2 parece tan elegante como la implementación 1, ¡no funciona!
Si queremos que la implementación 2 funcione correctamente, debemos agregar algún tipo de mecanismo de bloqueo; entonces no solo será más lento, sino menos elegante que la implementación 1.
Conclusión:
Es bueno tomar MSB GC en el estándar C ++ como característica opcional.