2012-01-05 18 views
20

std :: atomic es una nueva característica introducida por C++ 11 pero no puedo encontrar mucho tutorial sobre cómo usarla correctamente. Entonces, ¿la siguiente práctica es común y eficiente?Cómo usar std :: atomic eficientemente

Una práctica utilicé es que tenemos una memoria intermedia y quiero CAS de algunos bytes, así que lo que hice fue:

uint8_t *buf = .... 
auto ptr = reinterpret_cast<std::atomic<uint8_t>*>(&buf[index]); 
uint8_t oldValue, newValue; 
do { 
    oldValue = ptr->load(); 
    // Do some computation and calculate the newValue; 
    newValue = f(oldValue); 
} while (!ptr->compare_exchange_strong(oldValue, newValue)); 

Así que mis preguntas son:

  1. Los usos de código anteriores reinterpret_cast feo y ¿es esta la forma correcta de recuperar el puntero atómico que hace referencia a la ubicación & buf [index]?
  2. ¿El CAS en un solo byte es significativamente más lento que CAS en una palabra de máquina, por lo que debería evitar usarlo? Mi código se verá más complicado si lo cambio para cargar una palabra, extraer el byte, calcular y establecer el byte en el nuevo valor, y hacer CAS. Esto hace que el código sea más complicado y también tengo que ocuparme de la alineación de direcciones.

EDITAR: si esas preguntas dependen del procesador/arquitectura, entonces ¿cuál es la conclusión para los procesadores x86/x64?

+1

C++ Concurrencia en acción [(acceso anticipado)] (http://www.manning.com/williams/), [(amazon)] (http://www.amazon.com/gp/product/1933988770/ ref = as_li_qf_sp_asin_tl?ie = UTF8 & tag = gummadoon-20 & linkCode = as2 & camp = 1789 & creative = 9325 & creativeASIN = 1933988770) es probablemente el mejor libro sobre este tema en este momento, o mejor dicho, lo será. – Cubbi

+1

No hay muchos tutoriales sobre átomo porque, aparte de algunos casos simples como banderas atómicas, es un campo minado. Ver "The Hurt Locker" debería ser un prerrequisito para usar atómico. Use bloqueos! –

Respuesta

23
  1. El reinterpret_cast dará un comportamiento indefinido. La variable es o bien un std::atomic<uint8_t> o una llanura uint8_t; no puedes lanzar entre ellos. Los requisitos de tamaño y alineación pueden ser diferentes, por ejemplo. p.ej. algunas plataformas solo proporcionan operaciones atómicas en palabras, por lo que std::atomic<uint8_t> utilizará una palabra de máquina completa donde el uint8_t solo puede usar un byte. Las operaciones no atómicas también pueden optimizarse en todo tipo de formas, incluida la reorientación significativa con operaciones circundantes, y combinadas con otras operaciones en ubicaciones de memoria adyacentes donde eso puede mejorar el rendimiento.

    Esto significa que si desea operaciones atómicas en algunos datos, debe saberlo con antelación y crear objetos adecuados std::atomic<> en lugar de asignar un búfer genérico. Por supuesto, podría asignar un búfer y luego usar la colocación new para inicializar su variable atómica en ese búfer, pero tendría que asegurarse de que el tamaño y la alineación fueran correctos, y no podría usar operaciones no atómicas en ese objeto

    Si realmente no le importa pedir restricciones en su objeto atómico, entonces use memory_order_relaxed en lo que de otro modo serían las operaciones no atómicas. Sin embargo, tenga en cuenta que esto es altamente especializado y requiere gran cuidado. Por ejemplo, las escrituras en distintas variables pueden ser leídas por otros hilos en un orden diferente al que fueron escritas, y diferentes hilos pueden leer los valores en diferentes órdenes entre sí, incluso dentro de la misma ejecución del programa.

  2. Si CAS es más lento para un byte que una palabra, que puede mejor usar std::atomic<unsigned>, pero esto tendrá una penalización del espacio, y que sin duda no se puede simplemente usar std::atomic<unsigned> acceder a una secuencia de bytes primas --- todas las operaciones en esos datos deben realizarse a través del mismo objeto std::atomic<unsigned>. En general, es mejor escribir código que haga lo que necesita y dejar que el compilador descubra la mejor manera de hacerlo.

para x86/x64, con un std::atomic<unsigned> variables a, a.load(std::memory_order_acquire) y a.store(new_value,std::memory_order_release) no son más caros que las cargas y las tiendas para las variables no atómicas en cuanto a las instrucciones reales van, pero lo hacen limitar las optimizaciones del compilador. Si utiliza el valor predeterminado std::memory_order_seq_cst, una o ambas de estas operaciones incurrirá en el costo de sincronización de una instrucción LOCK ed o una valla (my implementation pone el precio en la tienda, pero otras implementaciones pueden elegir de forma diferente). Sin embargo, las operaciones memory_order_seq_cst son más fáciles de razonar debido a la restricción de "orden total único" que imponen.

En muchos casos es igual de rápido, y mucho menos propenso a errores, para utilizar bloqueos en lugar de operaciones atómicas. Si la sobrecarga de un bloqueo de exclusión mutua es significativa debido a la contención, entonces es posible que deba reconsiderar sus patrones de acceso a los datos; de todos modos, el ping pong de caché puede afectarlo con átomos atómicos.

2

Su reinterpret_cast<std::atomic<uint8_t>*>(...) definitivamente no es la forma correcta de recuperar un atómico y ni siquiera está garantizado para funcionar. Esto se debe a que no se garantiza que std::atomic<T> tenga el mismo tamaño que T.

Para su segunda pregunta acerca de que CAS es más lento para bytes que palabras de máquina: Eso es realmente dependiente de la máquina, podría ser más rápido, podría ser más lento o no existir CAS para bytes en su arquitectura de destino. En el último caso, es muy probable que la implementación necesite utilizar una implementación de bloqueo para el atómico o usar un tipo diferente (más grande) internamente (que es un ejemplo de atómico que no tiene el mismo tamaño que el tipo subyacente).

Por lo que veo realmente no hay manera de obtener un std::atomic en un valor existente, sobre todo porque no se garantiza que sean del mismo tamaño. Por lo tanto, realmente debe hacer directamente buf un std::atomic<uint8_t>*. Además, estoy relativamente seguro de que incluso si ese modelo funcionara, no se garantizaría que el acceso no atómico a la misma dirección funcionara como se esperaba (ya que no se garantiza que este acceso sea atómico ni siquiera para los bytes). Por lo tanto, tener medios no atómicos para acceder a una ubicación de memoria en la que quieras realizar operaciones atómicas no tiene realmente sentido.

Tenga en cuenta que para las arquitecturas comunes, los almacenes y montones de bytes son atómicos, de modo que tiene poca o ninguna sobrecarga de rendimiento para usar átomos atómicos allí, siempre que use un orden de memoria relajado para esas operaciones. Por lo tanto, si realmente no le importa el orden de ejecución en un punto (por ejemplo, porque el programa aún no está multiproceso), simplemente use a.store(0, std::memory_order_relaxed) en lugar de a.store(0).

Por supuesto si solo está hablando de x86 su reinterpret_cast es probable que funcione, pero su pregunta de rendimiento probablemente aún depende del procesador (creo que no he buscado los tiempos de las instrucciones reales para cmpxchg).

+0

Estoy 90% seguro de que el byte atómico será más lento que una palabra, porque necesita realizar una operación en modo bit. Quiero saber cuánto más lento se vería. Otra cosa es que no estoy de acuerdo contigo en que un byte de lectura/escritura sea atómico, al menos en x86. Gracias por su sugerencia de que utiliza una matriz atómica en lugar de una matriz de bytes, que funciona, pero también provocará que la carga de los bytes sea más lenta, lo cual no es lo que quiero. De hecho, en el 99% del tiempo, puedo decir que no hay otro hilo almacenado en la matriz, por lo que la barrera adicional no es necesaria. Solo por un corto período de tiempo necesito hacer lo anterior. –

+0

@icando: Como dije, depende de la plataforma. Pero ya que estás hablando de x86: ¿Por qué una operación atómica sería más lenta en un byte que en una palabra? ¿Qué quieres decir con que necesita hacer algunas operaciones a nivel de bit? X86 puede almacenar bytes de forma nativa y tiene 8bit 'cmpxchg', por lo que no debería importar (bueno, eso no es exactamente, pero no debería tener más impacto que usar bytes en lugar de machinewords de todos modos). Y sobre la barrera adicional: es por eso que sugerí 'memory_order_relaxed', que debería eliminar la mayoría de los costos adicionales, ya que load/store es atómico de todos modos (al menos en x86). – Grizzly

4

Su código es ciertamente incorrecto y obligado a hacer algo gracioso. Si las cosas van realmente mal, podría hacer lo que crees que está destinado a hacer. No llegaría tan lejos como para entender cómo usar correctamente, p.CAS pero se usarían std::atomic<T> algo como esto:

std::atomic<uint8_t> value(0); 
uint8_t oldvalue, newvalue; 
do 
{ 
    oldvalue = value.load(); 
    newvalue = f(oldvalue); 
} 
while (!value.compare_exchange_strong(oldvalue, newvalue)); 

Hasta ahora mi política personal es que se mantenga alejado de cualquiera de estas cosas sin cerradura y dejar en manos de personas que saben lo que están haciendo. Usaría atomic_flag y posiblemente contadores, y eso es todo lo lejos que llegaría. Conceptualmente entiendo cómo este trabajo la materia libre de bloqueo pero también entiendo que hay demasiadas cosas que pueden salir mal si no se es extremadamente cuidadoso.

+1

Diría que es un problema que proviene del caso de uso del mundo real, no de la tarea académica. Yo personalmente seguiría estándar tanto como sea posible, pero en la vida real, a veces simplemente no puedo. –

Cuestiones relacionadas