Pregunta

  

Editar: El código aquí todavía tiene algunos errores, y podría funcionar mejor en el departamento de rendimiento, pero en lugar de intentar solucionarlo, para el registro, llevé el problema al Los grupos de discusión de Intel obtuvieron una gran cantidad de comentarios, y si todo va bien, se incluirá una versión pulida de Atomic float en una próxima versión de los Threading Building Blocks de Intel

Ok, aquí está uno difícil, quiero un flotador atómico, no para un rendimiento de gráficos súper rápido, sino para usar rutinariamente como miembros de clases de datos. Y no quiero pagar el precio de usar bloqueos en estas clases, porque no proporciona beneficios adicionales para mis necesidades.

Ahora con tbb de Intel y otras bibliotecas atómicas que he visto, se admiten tipos enteros, pero no puntos flotantes. Así que continué e implementé uno, y funciona ... pero no estoy seguro de si realmente FUNCIONA, o simplemente tengo mucha suerte de que funcione.

¿Alguien aquí sabe si esto no es una forma de herejía de subprocesos?

typedef unsigned int uint_32;

  struct AtomicFloat
  {
    private:
    tbb::atomic<uint_32> atomic_value_;

    public:
    template<memory_semantics M>
    float fetch_and_store( float value ) 
    {
        const uint_32 value_ = atomic_value_.tbb::atomic<uint_32>::fetch_and_store<M>((uint_32&)value);
        return reinterpret_cast<const float&>(value_);
    }

    float fetch_and_store( float value ) 
    {
        const uint_32 value_ = atomic_value_.tbb::atomic<uint_32>::fetch_and_store((uint_32&)value);
        return reinterpret_cast<const float&>(value_);
    }

    template<memory_semantics M>
    float compare_and_swap( float value, float comparand ) 
    {
        const uint_32 value_ = atomic_value_.tbb::atomic<uint_32>::compare_and_swap<M>((uint_32&)value,(uint_32&)compare);
        return reinterpret_cast<const float&>(value_);
    }

    float compare_and_swap(float value, float compare)
    {
        const uint_32 value_ = atomic_value_.tbb::atomic<uint_32>::compare_and_swap((uint_32&)value,(uint_32&)compare);
        return reinterpret_cast<const float&>(value_);
    }

    operator float() const volatile // volatile qualifier here for backwards compatibility 
    {
        const uint_32 value_ = atomic_value_;
        return reinterpret_cast<const float&>(value_);
    }

    float operator=(float value)
    {
        const uint_32 value_ = atomic_value_.tbb::atomic<uint_32>::operator =((uint_32&)value);
        return reinterpret_cast<const float&>(value_);
    }

    float operator+=(float value)
    {
        volatile float old_value_, new_value_;
        do
        {
            old_value_ = reinterpret_cast<float&>(atomic_value_);
            new_value_ = old_value_ + value;
        } while(compare_and_swap(new_value_,old_value_) != old_value_);
        return (new_value_);
    }

    float operator*=(float value)
    {
        volatile float old_value_, new_value_;
        do
        {
            old_value_ = reinterpret_cast<float&>(atomic_value_);
            new_value_ = old_value_ * value;
        } while(compare_and_swap(new_value_,old_value_) != old_value_);
        return (new_value_);
    }

    float operator/=(float value)
    {
        volatile float old_value_, new_value_;
        do
        {
            old_value_ = reinterpret_cast<float&>(atomic_value_);
            new_value_ = old_value_ / value;
        } while(compare_and_swap(new_value_,old_value_) != old_value_);
        return (new_value_);
    }

    float operator-=(float value)
    {
        return this->operator+=(-value);
    }

    float operator++() 
    {
        return this->operator+=(1);
    }

    float operator--() 
    {
        return this->operator+=(-1);
    }

    float fetch_and_add( float addend ) 
    {
        return this->operator+=(-addend);
    }

    float fetch_and_increment() 
    {
        return this->operator+=(1);
    }

    float fetch_and_decrement() 
    {
        return this->operator+=(-1);
    }
   };

¡Gracias!

Editar: cambió size_t a uint32_t como sugirió Greg Rogers, de esa manera es más portátil

Editar: listado agregado para toda la cosa, con algunas correcciones.

Más ediciones: En cuanto al rendimiento, usar un flotador bloqueado para 5.000.000 + = operaciones con 100 subprocesos en mi máquina toma 3.6 s, mientras que mi flotador atómico, incluso con su tonto do-while toma 0.2s para hacer el mismo trabajo. Así que el aumento de rendimiento de > 30x significa que vale la pena, (y este es el problema) si es correcto.

Incluso más ediciones: Como Awgn señaló que mis partes fetch_and_xxxx estaban todas mal. Arreglado eso y eliminé partes de la API de las que no estoy seguro (modelos de memoria con plantilla). E implementó otras operaciones en términos de operador + = para evitar la repetición del código

Añadido: operador agregado * = y operador / =, ya que los flotadores no serían flotantes sin ellos. Gracias al comentario de Peterchen de que esto fue notado

Editar: Sigue la última versión del código (aunque dejaré la versión anterior como referencia)

  #include <tbb/atomic.h>
  typedef unsigned int      uint_32;
  typedef __TBB_LONG_LONG       uint_64;

  template<typename FLOATING_POINT,typename MEMORY_BLOCK>
  struct atomic_float_
  {
    /*  CRC Card -----------------------------------------------------
    |   Class:          atmomic float template class
    |
    |   Responsability: handle integral atomic memory as it were a float,
    |                   but partially bypassing FPU, SSE/MMX, so it is
    |                   slower than a true float, but faster and smaller
    |                   than a locked float.
    |                       *Warning* If your float usage is thwarted by
    |                   the A-B-A problem this class isn't for you
    |                       *Warning* Atomic specification says we return,
    |                   values not l-values. So  (i = j) = k doesn't work.
    |
    |   Collaborators:  intel's tbb::atomic handles memory atomicity
    ----------------------------------------------------------------*/
    typedef typename atomic_float_<FLOATING_POINT,MEMORY_BLOCK> self_t;

    tbb::atomic<MEMORY_BLOCK> atomic_value_;

    template<memory_semantics M>
    FLOATING_POINT fetch_and_store( FLOATING_POINT value ) 
    {
        const MEMORY_BLOCK value_ = 
            atomic_value_.tbb::atomic<MEMORY_BLOCK>::fetch_and_store<M>((MEMORY_BLOCK&)value);
        //atomic specification requires returning old value, not new one
        return reinterpret_cast<const FLOATING_POINT&>(value_);
    }

    FLOATING_POINT fetch_and_store( FLOATING_POINT value ) 
    {
        const MEMORY_BLOCK value_ = 
            atomic_value_.tbb::atomic<MEMORY_BLOCK>::fetch_and_store((MEMORY_BLOCK&)value);
        //atomic specification requires returning old value, not new one
        return reinterpret_cast<const FLOATING_POINT&>(value_);
    }

    template<memory_semantics M>
    FLOATING_POINT compare_and_swap( FLOATING_POINT value, FLOATING_POINT comparand ) 
    {
        const MEMORY_BLOCK value_ = 
            atomic_value_.tbb::atomic<MEMORY_BLOCK>::compare_and_swap<M>((MEMORY_BLOCK&)value,(MEMORY_BLOCK&)compare);
        //atomic specification requires returning old value, not new one
        return reinterpret_cast<const FLOATING_POINT&>(value_);
    }

    FLOATING_POINT compare_and_swap(FLOATING_POINT value, FLOATING_POINT compare)
    {
        const MEMORY_BLOCK value_ = 
            atomic_value_.tbb::atomic<MEMORY_BLOCK>::compare_and_swap((MEMORY_BLOCK&)value,(MEMORY_BLOCK&)compare);
        //atomic specification requires returning old value, not new one
        return reinterpret_cast<const FLOATING_POINT&>(value_);
    }

    operator FLOATING_POINT() const volatile // volatile qualifier here for backwards compatibility 
    {
        const MEMORY_BLOCK value_ = atomic_value_;
        return reinterpret_cast<const FLOATING_POINT&>(value_);
    }

    //Note: atomic specification says we return the a copy of the base value not an l-value
    FLOATING_POINT operator=(FLOATING_POINT rhs) 
    {
        const MEMORY_BLOCK value_ = atomic_value_.tbb::atomic<MEMORY_BLOCK>::operator =((MEMORY_BLOCK&)rhs);
        return reinterpret_cast<const FLOATING_POINT&>(value_);
    }

    //Note: atomic specification says we return an l-value when operating among atomics
    self_t& operator=(self_t& rhs) 
    {
        const MEMORY_BLOCK value_ = atomic_value_.tbb::atomic<MEMORY_BLOCK>::operator =((MEMORY_BLOCK&)rhs);
        return *this;
    }

    FLOATING_POINT& _internal_reference() const
    {
        return reinterpret_cast<FLOATING_POINT&>(atomic_value_.tbb::atomic<MEMORY_BLOCK>::_internal_reference());
    }

    FLOATING_POINT operator+=(FLOATING_POINT value)
    {
        FLOATING_POINT old_value_, new_value_;
        do
        {
            old_value_ = reinterpret_cast<FLOATING_POINT&>(atomic_value_);
            new_value_ = old_value_ + value;
        //floating point binary representation is not an issue because
        //we are using our self's compare and swap, thus comparing floats and floats
        } while(self_t::compare_and_swap(new_value_,old_value_) != old_value_);
        return (new_value_); //return resulting value
    }

    FLOATING_POINT operator*=(FLOATING_POINT value)
    {
        FLOATING_POINT old_value_, new_value_;
        do
        {
            old_value_ = reinterpret_cast<FLOATING_POINT&>(atomic_value_);
            new_value_ = old_value_ * value;
        //floating point binary representation is not an issue becaus
        //we are using our self's compare and swap, thus comparing floats and floats
        } while(self_t::compare_and_swap(new_value_,old_value_) != old_value_);
        return (new_value_); //return resulting value
    }

    FLOATING_POINT operator/=(FLOATING_POINT value)
    {
        FLOATING_POINT old_value_, new_value_;
        do
        {
            old_value_ = reinterpret_cast<FLOATING_POINT&>(atomic_value_);
            new_value_ = old_value_ / value;
        //floating point binary representation is not an issue because
        //we are using our self's compare and swap, thus comparing floats and floats
        } while(self_t::compare_and_swap(new_value_,old_value_) != old_value_);
        return (new_value_); //return resulting value
    }

    FLOATING_POINT operator-=(FLOATING_POINT value)
    {
        return this->operator+=(-value); //return resulting value
    }

    //Prefix operator
    FLOATING_POINT operator++()
    {
        return this->operator+=(1); //return resulting value
    }

    //Prefix operator
    FLOATING_POINT operator--() 
    {
        return this->operator+=(-1); //return resulting value
    }

    //Postfix operator
    FLOATING_POINT operator++(int)
    {
        const FLOATING_POINT temp = this;
        this->operator+=(1);
        return temp//return resulting value
    }

    //Postfix operator
    FLOATING_POINT operator--(int) 
    {
        const FLOATING_POINT temp = this;
        this->operator+=(1);
        return temp//return resulting value
    }

    FLOATING_POINT fetch_and_add( FLOATING_POINT addend ) 
    {
        const FLOATING_POINT old_value_ = atomic_value_;
        this->operator+=(addend);
        //atomic specification requires returning old value, not new one as in operator x=
        return old_value_; 
    }

    FLOATING_POINT fetch_and_increment() 
    {
        const FLOATING_POINT old_value_ = atomic_value_;
        this->operator+=(+1);
        //atomic specification requires returning old value, not new one as in operator x=
        return old_value_; 
    }

    FLOATING_POINT fetch_and_decrement() 
    {
        const FLOATING_POINT old_value_ = atomic_value_;
        this->operator+=(-1);
        //atomic specification requires returning old value, not new one as in operator x=
        return old_value_; 
    }
  };

  typedef atomic_float_<float,uint_32> AtomicFloat;
  typedef atomic_float_<double,uint_64> AtomicDouble;
¿Fue útil?

Solución

Yo aconsejaría seriamente en contra de la herencia pública. No sé cómo es la implementación atómica, pero supongo que tiene operadores sobrecargados que lo utilizan como tipo integral, lo que significa que esas promociones se usarán en lugar de su flotador en muchos (¿quizás la mayoría?) Casos.

No veo ninguna razón por la que eso no funcione, pero como tú, tengo una manera de demostrarlo ...

Una nota: su rutina operator float () no tiene semántica de adquisición de carga, ¿y no debería estar marcada como volátil (o definitivamente al menos constante)? ??

EDITAR: Si va a proporcionar el operador - (), debe proporcionar los formularios de prefijo / postfijo.

Otros consejos

Parece que su implementación asume que sizeof (size_t) == sizeof (float) . ¿Será eso siempre cierto para sus plataformas de destino?

Y no diría enhebrar tanto la herejía como lanzar la herejía. :)

Aunque el tamaño de un uint32_t puede ser equivalente al de un flotador en un arco dado, al reinterpretar un molde de uno a otro, implícitamente supones que Los incrementos atómicos, los decrementos y todas las demás operaciones en bits son semánticamente equivalentes en ambos tipos, que no son en realidad. Dudo que funcione como se esperaba.

Dudo mucho que obtengas los valores correctos en fetch_and_add, etc., ya que la adición flotante es diferente de la adición int.

Esto es lo que obtengo de estas aritméticas:

1   + 1    =  1.70141e+038  
100 + 1    = -1.46937e-037  
100 + 0.01 =  1.56743e+038  
23  + 42   = -1.31655e-036  

Así que sí, seguro para hilos pero no lo que esperas.

los algoritmos de bloqueo (operador +, etc.) deberían funcionar en relación con la atomicidad (no he comprobado el algoritmo en sí ..)


Otra solución: Como se trata de todas las adiciones y sustracciones, es posible que pueda darle a cada hilo su propia instancia y luego agregar los resultados de varios hilos.

Este es el estado del código tal como está ahora después de las conversaciones en los tableros de inteligencia, pero aún no se ha verificado que funcione correctamente en todos los escenarios.

  #include <tbb/atomic.h>
  typedef unsigned int      uint_32;
  typedef __TBB_LONG_LONG       uint_64;

  template<typename FLOATING_POINT,typename MEMORY_BLOCK>
  struct atomic_float_
  {
    /*  CRC Card -----------------------------------------------------
    |   Class:          atmomic float template class
    |
    |   Responsability: handle integral atomic memory as it were a float,
    |                   but partially bypassing FPU, SSE/MMX, so it is
    |                   slower than a true float, but faster and smaller
    |                   than a locked float.
    |                       *Warning* If your float usage is thwarted by
    |                   the A-B-A problem this class isn't for you
    |                       *Warning* Atomic specification says we return,
    |                   values not l-values. So  (i = j) = k doesn't work.
    |
    |   Collaborators:  intel's tbb::atomic handles memory atomicity
    ----------------------------------------------------------------*/
    typedef typename atomic_float_<FLOATING_POINT,MEMORY_BLOCK> self_t;

    tbb::atomic<MEMORY_BLOCK> atomic_value_;

    template<memory_semantics M>
    FLOATING_POINT fetch_and_store( FLOATING_POINT value ) 
    {
        const MEMORY_BLOCK value_ = 
            atomic_value_.tbb::atomic<MEMORY_BLOCK>::fetch_and_store<M>((MEMORY_BLOCK&)value);
        //atomic specification requires returning old value, not new one
        return reinterpret_cast<const FLOATING_POINT&>(value_);
    }

    FLOATING_POINT fetch_and_store( FLOATING_POINT value ) 
    {
        const MEMORY_BLOCK value_ = 
            atomic_value_.tbb::atomic<MEMORY_BLOCK>::fetch_and_store((MEMORY_BLOCK&)value);
        //atomic specification requires returning old value, not new one
        return reinterpret_cast<const FLOATING_POINT&>(value_);
    }

    template<memory_semantics M>
    FLOATING_POINT compare_and_swap( FLOATING_POINT value, FLOATING_POINT comparand ) 
    {
        const MEMORY_BLOCK value_ = 
            atomic_value_.tbb::atomic<MEMORY_BLOCK>::compare_and_swap<M>((MEMORY_BLOCK&)value,(MEMORY_BLOCK&)compare);
        //atomic specification requires returning old value, not new one
        return reinterpret_cast<const FLOATING_POINT&>(value_);
    }

    FLOATING_POINT compare_and_swap(FLOATING_POINT value, FLOATING_POINT compare)
    {
        const MEMORY_BLOCK value_ = 
            atomic_value_.tbb::atomic<MEMORY_BLOCK>::compare_and_swap((MEMORY_BLOCK&)value,(MEMORY_BLOCK&)compare);
        //atomic specification requires returning old value, not new one
        return reinterpret_cast<const FLOATING_POINT&>(value_);
    }

    operator FLOATING_POINT() const volatile // volatile qualifier here for backwards compatibility 
    {
        const MEMORY_BLOCK value_ = atomic_value_;
        return reinterpret_cast<const FLOATING_POINT&>(value_);
    }

    //Note: atomic specification says we return the a copy of the base value not an l-value
    FLOATING_POINT operator=(FLOATING_POINT rhs) 
    {
        const MEMORY_BLOCK value_ = atomic_value_.tbb::atomic<MEMORY_BLOCK>::operator =((MEMORY_BLOCK&)rhs);
        return reinterpret_cast<const FLOATING_POINT&>(value_);
    }

    //Note: atomic specification says we return an l-value when operating among atomics
    self_t& operator=(self_t& rhs) 
    {
        const MEMORY_BLOCK value_ = atomic_value_.tbb::atomic<MEMORY_BLOCK>::operator =((MEMORY_BLOCK&)rhs);
        return *this;
    }

    FLOATING_POINT& _internal_reference() const
    {
        return reinterpret_cast<FLOATING_POINT&>(atomic_value_.tbb::atomic<MEMORY_BLOCK>::_internal_reference());
    }

    FLOATING_POINT operator+=(FLOATING_POINT value)
    {
        FLOATING_POINT old_value_, new_value_;
        do
        {
            old_value_ = reinterpret_cast<FLOATING_POINT&>(atomic_value_);
            new_value_ = old_value_ + value;
        //floating point binary representation is not an issue because
        //we are using our self's compare and swap, thus comparing floats and floats
        } while(self_t::compare_and_swap(new_value_,old_value_) != old_value_);
        return (new_value_); //return resulting value
    }

    FLOATING_POINT operator*=(FLOATING_POINT value)
    {
        FLOATING_POINT old_value_, new_value_;
        do
        {
            old_value_ = reinterpret_cast<FLOATING_POINT&>(atomic_value_);
            new_value_ = old_value_ * value;
        //floating point binary representation is not an issue becaus
        //we are using our self's compare and swap, thus comparing floats and floats
        } while(self_t::compare_and_swap(new_value_,old_value_) != old_value_);
        return (new_value_); //return resulting value
    }

    FLOATING_POINT operator/=(FLOATING_POINT value)
    {
        FLOATING_POINT old_value_, new_value_;
        do
        {
            old_value_ = reinterpret_cast<FLOATING_POINT&>(atomic_value_);
            new_value_ = old_value_ / value;
        //floating point binary representation is not an issue because
        //we are using our self's compare and swap, thus comparing floats and floats
        } while(self_t::compare_and_swap(new_value_,old_value_) != old_value_);
        return (new_value_); //return resulting value
    }

    FLOATING_POINT operator-=(FLOATING_POINT value)
    {
        return this->operator+=(-value); //return resulting value
    }

    //Prefix operator
    FLOATING_POINT operator++()
    {
        return this->operator+=(1); //return resulting value
    }

    //Prefix operator
    FLOATING_POINT operator--() 
    {
        return this->operator+=(-1); //return resulting value
    }

    //Postfix operator
    FLOATING_POINT operator++(int)
    {
        const FLOATING_POINT temp = this;
        this->operator+=(1);
        return temp//return resulting value
    }

    //Postfix operator
    FLOATING_POINT operator--(int) 
    {
        const FLOATING_POINT temp = this;
        this->operator+=(1);
        return temp//return resulting value
    }

    FLOATING_POINT fetch_and_add( FLOATING_POINT addend ) 
    {
        const FLOATING_POINT old_value_ = atomic_value_;
        this->operator+=(addend);
        //atomic specification requires returning old value, not new one as in operator x=
        return old_value_; 
    }

    FLOATING_POINT fetch_and_increment() 
    {
        const FLOATING_POINT old_value_ = atomic_value_;
        this->operator+=(+1);
        //atomic specification requires returning old value, not new one as in operator x=
        return old_value_; 
    }

    FLOATING_POINT fetch_and_decrement() 
    {
        const FLOATING_POINT old_value_ = atomic_value_;
        this->operator+=(-1);
        //atomic specification requires returning old value, not new one as in operator x=
        return old_value_; 
    }
  };

  typedef atomic_float_<float,uint_32> AtomicFloat;
  typedef atomic_float_<double,uint_64> AtomicDouble;

Solo una nota sobre esto (quería hacer un comentario, pero aparentemente los usuarios nuevos no pueden comentar): el uso de reinterpret_cast en las referencias produce un código incorrecto con gcc 4.1 -O3. Esto parece estar arreglado en 4.4 porque ahí funciona. Cambiar los reinterpret_casts a los punteros, aunque un poco más feo, funciona en ambos casos.

A partir de la lectura de ese código, estaría realmente enojado con un compilador de tal manera que fuera para ensamblar para esto que no era atómico.

Haz que tu compilador genere el código de ensamblaje y échale un vistazo. Si la operación es más que una única instrucción en lenguaje ensamblador, entonces no es una operación atómica, y requiere que los bloqueos funcionen correctamente en sistemas multiprocesador.

Lamentablemente, no estoy seguro de que lo contrario también sea cierto: se garantiza que las operaciones de instrucción única están siendo atómicas. No conozco los detalles de la programación de multiprocesadores hasta ese nivel. Podría hacer un caso para cualquiera de los resultados. (Si alguien más tiene alguna información definitiva sobre eso, siéntete libre de participar)

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top