¿Hay un equivalente no atómico de std :: shared_ptr? ¿Y por qué no hay uno en ?

Esta es una pregunta de dos partes, todo sobre la atomicidad de std::shared_ptr :

1. Por lo que puedo decir, std::shared_ptr es el único puntero inteligente en que es atómico. Me pregunto si hay disponible una versión no atómica de std::shared_ptr (no puedo ver nada en , entonces también estoy abierto a sugerencias fuera del estándar, como las de Boost). Sé que boost::shared_ptr también es atómico (si BOOST_SP_DISABLE_THREADS no está definido), pero tal vez haya otra alternativa. Estoy buscando algo que tenga la misma semántica que std::shared_ptr , pero sin la atomicidad.

2. Entiendo por qué std::shared_ptr es atómico; es un poco agradable. Sin embargo, no es bueno para todas las situaciones, y C ++ históricamente ha tenido el mantra de “solo pagar por lo que usa”. Si no estoy usando varios subprocesos, o si estoy usando varios subprocesos pero no comparto la propiedad del puntero entre los subprocesos, un puntero inteligente atómico es excesivo. Mi segunda pregunta es por qué no fue una versión no atómica de std::shared_ptr proporcionada en C ++ 11 ? (suponiendo que haya un por qué ) (si la respuesta es simplemente “una versión no atómica simplemente nunca se consideró” o “nadie pidió una versión no atómica”, está bien!).

Con la pregunta n. ° 2, me pregunto si alguna vez alguien propuso una versión no atómica de shared_ptr (ya sea para Boost o para el comité de estándares) (no para reemplazar la versión atómica de shared_ptr , sino para convivir con ella) y fue derribada por una razón específica.

1. Me pregunto si hay una versión no atómica de std :: shared_ptr disponible

No proporcionado por el estándar. Es posible que haya una proporcionada por una biblioteca de “terceros”. De hecho, antes de C ++ 11, y antes de Boost, parecía que todos escribieron su propio puntero inteligente de referencia contado (incluyéndome a mí).

2. Mi segunda pregunta es ¿por qué no fue una versión no atómica de std :: shared_ptr proporcionada en C ++ 11?

Esta cuestión se discutió en la reunión de Rapperswil en 2010. El tema fue presentado por un comentario del organismo nacional # 20 por Suiza. Hubo fuertes argumentos en ambos lados del debate, incluidos los que usted proporciona en su pregunta. Sin embargo, al final de la discusión, el voto fue abrumadoramente (pero no unánime) en contra de agregar una versión no sincronizada (no atómica) de shared_ptr .

Argumentos en contra:

  • El código escrito con el shared_ptr no sincronizado puede terminar siendo utilizado en el código de subprocesos más adelante, lo que termina causando problemas difíciles de depurar sin previo aviso.

  • Tener un shared_ptr “universal” que sea la “única forma” de traficar en el recuento de referencias tiene beneficios: a partir de la propuesta original :

    Tiene el mismo tipo de objeto independientemente de las características utilizadas, lo que facilita en gran medida la interoperabilidad entre bibliotecas, incluidas las bibliotecas de terceros.

  • El costo de los atómicos, aunque no es cero, no es abrumador. El costo se mitiga mediante el uso de la construcción de movimiento y la asignación de movimiento que no necesitan utilizar operaciones atómicas. Tales operaciones se usan comúnmente en el vector> borrar e insertar.

  • Nada prohíbe a las personas escribir su propio puntero inteligente no contabilizado de referencia atómica si eso es realmente lo que quieren hacer.

La última palabra del LWG en Rapperswil ese día fue:

Rechazar CH 20. No hay consenso para hacer un cambio en este momento.

Howard ya respondió bien la pregunta, y Nicol hizo algunos buenos comentarios sobre los beneficios de tener un solo tipo de puntero compartido estándar, en lugar de muchos incompatibles.

Si bien estoy totalmente de acuerdo con la decisión del comité, creo que hay algún beneficio al usar un tipo similar a shared_ptr no sincronizado en casos especiales , por lo que he investigado el tema varias veces.

Si no estoy usando varios subprocesos, o si estoy usando varios subprocesos pero no comparto la propiedad del puntero entre los subprocesos, un puntero inteligente atómico es excesivo.

Con GCC cuando su progtwig no usa múltiples hilos shared_ptr no usa operaciones atómicas para el recuento. Esto se hace actualizando los recuentos de referencia a través de funciones de envoltura que detectan si el progtwig es multiproceso (en GNU / Linux esto se hace simplemente detectando si el progtwig se vincula a libpthread.so ) y se envía a operaciones atómicas o no atómicas en consecuencia.

Me di cuenta hace muchos años que debido a que shared_ptr GCC se implementa en términos de una clase base __shared_ptr , es posible usar la clase base con la política de locking de un único subproceso incluso en código multiproceso, al usar explícitamente __shared_ptr . Desafortunadamente, debido a que no era un caso de uso previsto, no funcionó de manera óptima antes de GCC 4.9, y algunas operaciones aún utilizaban las funciones de envoltura y se enviaban a operaciones atómicas aunque haya solicitado explícitamente la política _S_single . Vea el punto (2) en http://gcc.gnu.org/ml/libstdc++/2007-10/msg00180.html para obtener más detalles y un parche para GCC para permitir que la implementación no atómica se use incluso en aplicaciones multiproceso. Estuve sentado en ese parche durante años, pero finalmente lo comprometí para GCC 4.9, que le permite usar una plantilla de alias como esta para definir un tipo de puntero compartido que no es seguro para subprocesos, pero es un poco más rápido:

 template using shared_ptr_unsynchronized = std::__shared_ptr; 

Este tipo no sería interoperable con std::shared_ptr y solo sería seguro de usar cuando se garantice que los objetos shared_ptr_unsynchronized nunca se compartirán entre hilos sin una sincronización adicional proporcionada por el usuario.

Esto es, por supuesto, completamente no portátil, pero a veces eso está bien. Con el preprocesador correcto piratea su código aún funcionaría bien con otras implementaciones si shared_ptr_unsynchronized es un alias para shared_ptr , sería un poco más rápido con GCC.

Si está utilizando un GCC anterior a 4.9, puede usarlo agregando las especializaciones explícitas de _Sp_counted_base<_s_single> a su propio código (y asegúrese de que nadie ejemplifique __shared_ptr sin incluir las especializaciones, para evitar violaciones de ODR). tales especializaciones de tipos std están técnicamente definidas, pero funcionarían en la práctica, porque en este caso no hay diferencia entre agregar las especializaciones a GCC o agregarlas a su propio código.

Mi segunda pregunta es por qué no fue una versión atómica de std :: shared_ptr proporcionada en C ++ 11? (suponiendo que hay un por qué)

Uno podría fácilmente preguntar por qué no hay un puntero intrusivo o cualquier cantidad de otras posibles variaciones de punteros compartidos que uno pueda tener.

El diseño de shared_ptr, heredado de Boost, ha sido crear una lingua-franca estándar mínima de punteros inteligentes. Eso, en términos generales, puedes tirar de esto hacia abajo y usarlo. Es algo que se usaría generalmente en una amplia variedad de aplicaciones. Puedes ponerlo en una interfaz, y hay buenas probabilidades de que la gente esté dispuesta a usarlo.

Enhebrar solo va a ser más frecuente en el futuro. De hecho, a medida que pasa el tiempo, el enhebrado generalmente será uno de los principales medios para lograr el rendimiento. Requerir que el puntero inteligente básico haga el mínimo necesario para admitir el enhebrado facilita esta realidad.

Dejar caer media docena de punteros inteligentes con variaciones menores entre ellos en el estándar, o incluso peor un puntero inteligente basado en políticas, habría sido terrible. Todos elegirían el puntero que más les gusta y renunciarán a todos los demás. Nadie podría comunicarse con nadie más. Sería como las situaciones actuales con cadenas de C ++, donde cada uno tiene su propio tipo. Solo que es mucho peor, porque la interoperación con cadenas es mucho más fácil que la interoperación entre clases de punteros inteligentes.

Boost, y por extensión el comité, eligió un puntero inteligente específico para usar. Proporcionó un buen equilibrio de características y fue ampliamente utilizado en la práctica.

std::vector tiene algunas ineficiencias en comparación con las matrices desnudas en algunos casos de esquina también. Tiene algunas limitaciones; algunos usos realmente quieren tener un límite estricto en el tamaño de un vector , sin usar un asignador de lanzamiento. Sin embargo, el comité no diseñó el vector para que sea todo para todos. Fue diseñado para ser un buen valor predeterminado para la mayoría de las aplicaciones. Aquellos para quienes no puede trabajar pueden simplemente escribir una alternativa que satisfaga sus necesidades.

Del mismo modo que puede hacerlo con un puntero inteligente si la atomicidad de shared_ptr es una carga. Por otra parte, uno también podría considerar no copiarlos tanto.

Estoy preparando una charla sobre shared_ptr en el trabajo. He estado utilizando un impulso compartido shared_ptr modificado para evitar malloc por separado (como lo que puede hacer make_shared) y un parámetro de plantilla para la política de locking como shared_ptr_unsynchronized mencionado anteriormente. Estoy usando el progtwig de

http://flyingfrogblog.blogspot.hk/2011/01/boosts-sharedptr-up-to-10-slower-than.html

como prueba, después de limpiar las copias shared_ptr innecesarias. El progtwig usa solo el hilo principal y se muestra el argumento de prueba. El env de prueba es un portátil que ejecuta linuxmint 14. Aquí está el tiempo tomado en segundos:

 configuración de ejecución de prueba boost (1.49) std con make_shared boost modificado
 mt-inseguro (11) 11.9 9 / 11.5 (-pthread on) 8.4  
 atómico (11) 13.6 12.4 13.0  
 mt-inseguro (12) 113.5 85.8 / 108.9 (-pthread on) 81.5  
 atómico (12) 126.0 109.1 123.6  

Solo la versión ‘estándar’ usa -std = cxx11, y la -pthread probablemente cambie a lock_policy en la clase g ++ __shared_ptr.

A partir de estos números, veo el impacto de las instrucciones atómicas en la optimización del código. El caso de prueba no usa ningún contenedor de C ++, pero es probable que el vector> sufra si el objeto no necesita la protección de subprocesos. Boost sufre menos probablemente debido a que el malloc adicional está limitando la cantidad de creación y la optimización del código.

Todavía tengo que encontrar una máquina con suficientes núcleos para probar la escalabilidad de las instrucciones atómicas, pero usar std :: shared_ptr solo cuando sea necesario es probablemente mejor.