¿Existe un equivalente no atómico de std :: shared_ptr? ¿Y por qué no hay uno en <memoria>?

88

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

1. Por lo que puedo decir, std::shared_ptres el único puntero inteligente <memory>que es atómico. Me pregunto si hay una versión no atómica de std::shared_ptrdisponible (no puedo ver nada <memory>, así que también estoy abierto a sugerencias fuera del estándar, como las de Boost). Sé boost::shared_ptrque también es atómico (si BOOST_SP_DISABLE_THREADSno 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_ptres atómico; es un poco agradable. Sin embargo, no es bueno para todas las situaciones, y C ++ ha tenido históricamente el mantra de "solo paga por lo que usas". Si no estoy usando varios subprocesos, o si estoy usando varios subprocesos pero no comparto la propiedad del puntero entre subprocesos, un puntero inteligente atómico es excesivo. Mi segunda pregunta es ¿por qué no se proporcionó una versión no atómica std::shared_ptren C ++ 11 ? (asumiendo que hay 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 alguien propuso alguna vez una versión no atómica de shared_ptr(ya sea para Boost o el comité de estándares) (no para reemplazar la versión atómica shared_ptr, sino para coexistir con ella) y fue rechazada por un razón específica.

Tallos de maiz
fuente
4
¿Qué "costo" exactamente le preocupa aquí? ¿El costo de incrementar atómicamente un número entero? ¿Es ese realmente un costo que le preocupa para cualquier aplicación real? ¿O simplemente está optimizando prematuramente?
Nicol Bolas
9
@NicolBolas: Es más curiosidad que cualquier otra cosa; No tengo (actualmente) ningún código / proyecto en el que realmente quiera usar un puntero compartido no atómico. Sin embargo, he tenido proyectos (en el pasado) en los que Boost's shared_ptrtuvo una desaceleración significativa debido a su atomicidad, y la definición BOOST_DISABLE_THREADShizo una diferencia notable (no sé si std::shared_ptrhabría tenido el mismo costo que eso boost::shared_ptr).
Cornstalks
13
@Votantes cerrados: ¿qué parte de la pregunta no es constructiva? Si no hay un por qué específico para la segunda pregunta, está bien (un simple "simplemente no se consideró" sería una respuesta suficientemente válida). Tengo curiosidad por saber si existe una razón / fundamento específico. Y la primera pregunta ciertamente es una pregunta válida, diría yo. Si necesito aclarar la pregunta o hacer pequeños ajustes, hágamelo saber. Pero no veo por qué no es constructivo.
Cornstalks
10
@Cornstalks Bueno, probablemente es solo que la gente no reacciona tan bien a preguntas que pueden descartar fácilmente como "optimización prematura" , sin importar cuán válida, bien planteada o relevante sea la pregunta, supongo. Por mi parte, no veo ninguna razón para cerrar esto como no constructivo.
Christian Rau
13
(no puedo escribir una respuesta ahora que está cerrado, así que comentando) Con GCC, cuando su programa no usa múltiples hilos shared_ptr, no usa operaciones atómicas para el refcount. Consulte (2) en gcc.gnu.org/ml/libstdc++/2007-10/msg00180.html para obtener un parche para GCC que permita que la implementación no atómica se use incluso en aplicaciones multiproceso, para shared_ptrobjetos que no se comparten entre hilos. He estado sentado en ese parche durante años, pero estoy considerando finalmente comprometerlo para GCC 4.9
Jonathan Wakely

Respuestas:

104

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

No proporcionado por el estándar. Bien puede haber uno proporcionado por una biblioteca de "terceros". De hecho, antes de C ++ 11, y antes de Boost, parecía que todos escribían su propia referencia contada como puntero inteligente (incluyéndome a mí).

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

Esta cuestión se debatió en la reunión de Rapperswil en 2010. El tema fue presentado por Suiza en un Comentario del organismo nacional nº 20. Hubo argumentos sólidos en ambos lados del debate, incluidos los que proporcionó 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.

Los argumentos en contra incluyeron:

  • El código escrito con shared_ptr no sincronizado puede terminar usándose en código enhebrado en el futuro, causando problemas difíciles de depurar sin advertencia.

  • Tener un shared_ptr "universal" que sea "unidireccional" para realizar el tráfico en el recuento de referencias tiene ventajas: De la propuesta original :

    Tiene el mismo tipo de objeto independientemente de las funciones utilizadas, lo que facilita enormemente la interoperabilidad entre bibliotecas, incluidas las de terceros.

  • El costo de las atómicas, aunque no es cero, no es abrumador. El costo se mitiga mediante el uso de la construcción de mudanzas y la asignación de mudanzas que no necesitan usar operaciones atómicas. Estas operaciones se utilizan comúnmente en vector<shared_ptr<T>>borrar e insertar.

  • Nada prohíbe a las personas escribir su propio puntero inteligente contado por referencias no atómicas si eso es realmente lo que quieren hacer.

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

Rechazar el capítulo 20. No hay consenso para hacer un cambio en este momento.

Howard Hinnant
fuente
7
¡Vaya, perfecto, gracias por la información! Ese es exactamente el tipo de información que esperaba encontrar.
Cornstalks
> Has the same object type regardless of features used, greatly facilitating interoperability between libraries, including third-party libraries. ese es un razonamiento extremadamente extraño. Las bibliotecas de terceros proporcionarán sus propios tipos de todos modos, entonces, ¿por qué importaría si lo proporcionaran bajo la forma de std :: shared_ptr <CustomType>, std :: non_atomic_shared_ptr <CustomType>, etc.? siempre tendrá que adaptar su código a lo que devuelve la biblioteca de todos modos
Jean-Michaël Celerier
Eso es cierto en lo que respecta a los tipos específicos de bibliotecas, pero la idea es que también hay muchos lugares donde los tipos estándar aparecen en API de terceros. Por ejemplo, mi biblioteca podría llevar un archivo std::shared_ptr<std::string>. Si la biblioteca de otra persona también toma ese tipo, las personas que llaman pueden pasarnos las mismas cadenas de caracteres a ambos sin el inconveniente o la sobrecarga de convertir entre diferentes representaciones, y eso es una pequeña victoria para todos.
Jack O'Connor
52

Howard ya ha respondido bien a 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 completamente de acuerdo con la decisión del comité, creo que hay algún beneficio en el uso de un shared_ptrtipo 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 subprocesos, un puntero inteligente atómico es excesivo.

Con GCC, cuando su programa no usa múltiples subprocesos, shared_ptr no usa operaciones atómicas para el refcount. Esto se hace actualizando los recuentos de referencias a través de funciones contenedoras que detectan si el programa es multiproceso (en GNU / Linux, esto se hace simplemente detectando si el programa se enlaza libpthread.so) y se envían a operaciones atómicas o no atómicas en consecuencia.

Hace muchos años me di cuenta de que debido a que GCC shared_ptr<T>se implementa en términos de una __shared_ptr<T, _LockPolicy>clase base , es posible usar la clase base con la política de bloqueo de un solo subproceso incluso en código multiproceso, usando explícitamente __shared_ptr<T, __gnu_cxx::_S_single>. Desafortunadamente, debido a que ese no era un caso de uso previsto, no funcionó de manera óptima antes de GCC 4.9, y algunas operaciones aún usaban las funciones de envoltura y, por lo tanto, se enviaban a operaciones atómicas a pesar de que solicitó explícitamente la _S_singlepolítica. Véase el punto (2) en http://gcc.gnu.org/ml/libstdc++/2007-10/msg00180.htmlpara obtener más detalles y un parche para GCC para permitir que la implementación no atómica se use incluso en aplicaciones multiproceso. Me senté 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<typename T>
  using shared_ptr_unsynchronized = std::__shared_ptr<T, __gnu_cxx::_S_single>;

Este tipo no sería interoperable std::shared_ptr<T>y solo sería seguro de usar cuando se garantiza que los shared_ptr_unsynchronizedobjetos nunca se compartirán entre subprocesos sin una sincronización adicional proporcionada por el usuario.

Esto, por supuesto, es completamente no portátil, pero a veces está bien. Con los hacks de preprocesador adecuados, su código aún funcionaría bien con otras implementaciones si shared_ptr_unsynchronized<T>es un alias shared_ptr<T>, sería un poco más rápido con GCC.


Si está usando un GCC antes de 4.9, podría usarlo agregando las _Sp_counted_base<_S_single>especializaciones explícitas a su propio código (y asegurándose de que nadie cree una instancia __shared_ptr<T, _S_single>sin incluir las especializaciones, para evitar violaciones de ODR). Agregar tales especializaciones de stdtipos no está técnicamente definido, pero funciona en la práctica, porque en este caso no hay diferencia entre que yo agregue las especializaciones a GCC o usted las agregue a su propio código.

Jonathan Wakely
fuente
2
Me pregunto, ¿hay un error tipográfico en su ejemplo del alias de plantilla? Es decir, creo que debería leer shared_ptr_unsynchronized = std :: __ shared_ptr <. Por cierto, probé esto hoy, junto con std :: __ enable_shared_from_this y std :: __ debil_ptr, y parece funcionar bien (gcc 4.9 y gcc 5.2). Lo perfilaré / desensamblaré en breve para ver si efectivamente se omiten las operaciones atómicas.
Carl Cook
Detalles impresionantes! Recientemente me enfrentaba a un problema, como se describe en esta pregunta , que con el tiempo me hizo a mirar en el código fuente de std::shared_ptr, std::__shared_ptr, __default_lock_policyy tal. Esta respuesta confirmó lo que entendí del código.
Nawaz
21

Mi segunda pregunta es ¿por qué no se proporcionó una versión no atómica de std :: shared_ptr en C ++ 11? (asumiendo que hay un por qué).

Uno podría preguntarse con la misma facilidad por qué no hay un puntero intrusivo, o cualquier otra posible variación de punteros compartidos que uno podría tener.

El diseño de shared_ptr, transmitido desde Boost, ha sido crear una lengua franca estándar mínima de punteros inteligentes. Que, en términos generales, puedes quitarlo de la pared y usarlo. Es algo que se usaría de manera general, en una amplia variedad de aplicaciones. Puede ponerlo en una interfaz, y es probable que haya gente dispuesta a usarlo.

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

Lanzar media docena de punteros inteligentes con variaciones menores entre ellos en el estándar, o peor aún, un puntero inteligente basado en políticas, habría sido terrible. Todos elegirían el puntero que más les guste 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 se usó ampliamente y comúnmente en la práctica.

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

Al igual que puede hacerlo con un puntero inteligente shared_ptr, la atomicidad es una carga. Por otra parte, también se podría considerar no copiarlos tanto.

Nicol Bolas
fuente
7
+1 para "también se podría considerar no copiarlos tanto".
Ali
Si alguna vez conectas a un generador de perfiles, eres especial y puedes ignorar argumentos como el anterior. Si no tiene un requisito operativo que sea difícil de cumplir, no debe usar C ++. Discutir como tú es una buena manera de hacer que C ++ sea universalmente vilipendiado por cualquier persona interesada en el alto rendimiento o la baja latencia. Es por eso que los programadores de juegos no usan STL, boost o incluso excepciones.
Hans Malherbe
Para mayor claridad, creo que la cita en la parte superior de su respuesta debería decir "¿por qué no se proporcionó una versión no atómica de std :: shared_ptr en C ++ 11?"
Charles Savoie
4

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

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

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

test run setup boost (1.49) std con make_shared modificado boost
mt-inseguro (11) 11.9 9 / 11.5 (-pthread activado) 8.4  
atómico (11) 13,6 12,4 13,0  
mt-inseguro (12) 113.5 85.8 / 108.9 (-pthread activado) 81.5  
atómico (12) 126,0 109,1 123,6  

Solo la versión 'std' usa -std = cxx11, y el -pthread probablemente cambia 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 C ++, pero vector<shared_ptr<some_small_POD>>es probable que sufra si el objeto no necesita la protección de subprocesos. Boost sufre menos probablemente porque el malloc adicional está limitando la cantidad de optimización de código y de inserción.

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.

russ
fuente
3

Boost proporciona un shared_ptrque no es atómico. Se llama local_shared_ptry se puede encontrar en la biblioteca de punteros inteligentes de boost.

El físico cuántico
fuente
+1 para una respuesta breve y sólida con una buena cita, pero este tipo de puntero parece costoso, en términos de memoria y tiempo de ejecución, debido a un nivel adicional de indirección (local-> shared-> ptr vs shared-> ptr).
Red.Wave
@ Red.Wave ¿Puede explicar qué quiere decir con indirección y cómo afecta al rendimiento? ¿Quieres decir que es shared_ptrcon mostrador de todos modos, aunque sea local? ¿O quiere decir que hay otro problema con eso? Los documentos dicen que la única diferencia es que esto no es atómico.
The Quantum Physicist
Cada ptr local mantiene un recuento y una referencia al ptr original compartido. Por lo tanto, cualquier acceso al puntero final necesita una desreferencia de ptr local a compartido, que luego es desreferencia al puntero. Por lo tanto, hay una indirección más apilada a las indirecciones de ptr compartido. Y eso aumenta los gastos generales.
Red.Wave
@ Red.Wave ¿De dónde obtiene esta información? Esto: "Por lo tanto, cualquier acceso al puntero final necesita una referencia de local a ptr compartido" necesita alguna cita. No pude encontrar eso en los documentos de boost. Nuevamente, lo que vi en los documentos es que dice eso local_shared_ptry shared_ptrson idénticos excepto por atómico. Estoy realmente interesado en saber si lo que dice es cierto porque lo uso local_shared_ptren aplicaciones que requieren un alto rendimiento.
The Quantum Physicist
3
@ Red.Wave Si miras el código fuente real github.com/boostorg/smart_ptr/blob/… verás que no hay doble indirección. Este párrafo de la documentación es solo un modelo mental.
Ilya Popov