¿Cuál es la mejor manera de hacer matemáticas de punto fijo?

Necesito acelerar un progtwig para Nintendo DS que no tiene una FPU, así que tengo que cambiar la matemática de coma flotante (que es emulada y lenta) a punto fijo.

La forma en que comencé fue cambiando los flotantes a los enteros y cada vez que necesitaba convertirlos, usaba x >> 8 para convertir la variable de punto fijo x en el número real yx << 8 para convertir a punto fijo. Pronto descubrí que era imposible hacer un seguimiento de lo que debía convertirse y también me di cuenta de que sería difícil cambiar la precisión de los números (8 en este caso).

Mi pregunta es, ¿cómo debería hacer esto más fácil y aún más rápido? ¿Debería hacer una clase FixedPoint, o solo una definición o tipo de FixedPoint8 con algunas funciones / macros para convertirlas, o alguna otra cosa? ¿Debería poner algo en el nombre de la variable para mostrar que es un punto fijo?

Puedes probar mi clase de punto fijo (Latest available @ https://github.com/eteran/cpp-utilities )

// From: https://github.com/eteran/cpp-utilities/edit/master/Fixed.h // See also: http://stackoverflow.com/questions/79677/whats-the-best-way-to-do-fixed-point-math /* * The MIT License (MIT) * * Copyright (c) 2015 Evan Teran * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #ifndef FIXED_H_ #define FIXED_H_ #include  #include  #include  // for size_t #include  #include  #include  namespace numeric { template  class Fixed; namespace detail { // helper templates to make magic with types :) // these allow us to determine resonable types from // a desired size, they also let us infer the next largest type // from a type which is nice for the division op template  struct type_from_size { static const bool is_specialized = false; typedef void value_type; }; #if defined(__GNUC__) && defined(__x86_64__) template <> struct type_from_size<128> { static const bool is_specialized = true; static const size_t size = 128; typedef __int128 value_type; typedef unsigned __int128 unsigned_type; typedef __int128 signed_type; typedef type_from_size<256> next_size; }; #endif template <> struct type_from_size<64> { static const bool is_specialized = true; static const size_t size = 64; typedef int64_t value_type; typedef uint64_t unsigned_type; typedef int64_t signed_type; typedef type_from_size<128> next_size; }; template <> struct type_from_size<32> { static const bool is_specialized = true; static const size_t size = 32; typedef int32_t value_type; typedef uint32_t unsigned_type; typedef int32_t signed_type; typedef type_from_size<64> next_size; }; template <> struct type_from_size<16> { static const bool is_specialized = true; static const size_t size = 16; typedef int16_t value_type; typedef uint16_t unsigned_type; typedef int16_t signed_type; typedef type_from_size<32> next_size; }; template <> struct type_from_size<8> { static const bool is_specialized = true; static const size_t size = 8; typedef int8_t value_type; typedef uint8_t unsigned_type; typedef int8_t signed_type; typedef type_from_size<16> next_size; }; // this is to assist in adding support for non-native base // types (for adding big-int support), this should be fine // unless your bit-int class doesn't nicely support casting template  B next_to_base(const N& rhs) { return static_cast(rhs); } struct divide_by_zero : std::exception { }; template  Fixed divide(const Fixed &numerator, const Fixed &denominator, Fixed &remainder, typename std::enable_if::next_size::is_specialized>::type* = 0) { typedef typename Fixed::next_type next_type; typedef typename Fixed::base_type base_type; static const size_t fractional_bits = Fixed::fractional_bits; next_type t(numerator.to_raw()); t <<= fractional_bits; Fixed quotient; quotient = Fixed::from_base(next_to_base(t / denominator.to_raw())); remainder = Fixed::from_base(next_to_base(t % denominator.to_raw())); return quotient; } template  Fixed divide(Fixed numerator, Fixed denominator, Fixed &remainder, typename std::enable_if::next_size::is_specialized>::type* = 0) { // NOTE(eteran): division is broken for large types :-( // especially when dealing with negative quantities typedef typename Fixed::base_type base_type; typedef typename Fixed::unsigned_type unsigned_type; static const int bits = Fixed::total_bits; if(denominator == 0) { throw divide_by_zero(); } else { int sign = 0; Fixed quotient; if(numerator < 0) { sign ^= 1; numerator = -numerator; } if(denominator < 0) { sign ^= 1; denominator = -denominator; } base_type n = numerator.to_raw(); base_type d = denominator.to_raw(); base_type x = 1; base_type answer = 0; // egyptian division algorithm while((n >= d) && (((d >> (bits - 1)) & 1) == 0)) { x <<= 1; d <<= 1; } while(x != 0) { if(n >= d) { n -= d; answer += x; } x >>= 1; d >>= 1; } unsigned_type l1 = n; unsigned_type l2 = denominator.to_raw(); // calculate the lower bits (needs to be unsigned) // unfortunately for many fractions this overflows the type still :-/ const unsigned_type lo = (static_cast(n) << F) / denominator.to_raw(); quotient = Fixed::from_base((answer << F) | lo); remainder = n; if(sign) { quotient = -quotient; } return quotient; } } // this is the usual implementation of multiplication template  void multiply(const Fixed &lhs, const Fixed &rhs, Fixed &result, typename std::enable_if::next_size::is_specialized>::type* = 0) { typedef typename Fixed::next_type next_type; typedef typename Fixed::base_type base_type; static const size_t fractional_bits = Fixed::fractional_bits; next_type t(static_cast(lhs.to_raw()) * static_cast(rhs.to_raw())); t >>= fractional_bits; result = Fixed::from_base(next_to_base(t)); } // this is the fall back version we use when we don't have a next size // it is slightly slower, but is more robust since it doesn't // require and upgraded type template  void multiply(const Fixed &lhs, const Fixed &rhs, Fixed &result, typename std::enable_if::next_size::is_specialized>::type* = 0) { typedef typename Fixed::base_type base_type; static const size_t fractional_bits = Fixed::fractional_bits; static const size_t integer_mask = Fixed::integer_mask; static const size_t fractional_mask = Fixed::fractional_mask; // more costly but doesn't need a larger type const base_type a_hi = (lhs.to_raw() & integer_mask) >> fractional_bits; const base_type b_hi = (rhs.to_raw() & integer_mask) >> fractional_bits; const base_type a_lo = (lhs.to_raw() & fractional_mask); const base_type b_lo = (rhs.to_raw() & fractional_mask); const base_type x1 = a_hi * b_hi; const base_type x2 = a_hi * b_lo; const base_type x3 = a_lo * b_hi; const base_type x4 = a_lo * b_lo; result = Fixed::from_base((x1 << fractional_bits) + (x3 + x2) + (x4 >> fractional_bits)); } } /* * inheriting from boost::operators enables us to be a drop in replacement for base types * without having to specify all the different versions of operators manually */ template  class Fixed : boost::operators> { static_assert(detail::type_from_size::is_specialized, "invalid combination of sizes"); public: static const size_t fractional_bits = F; static const size_t integer_bits = I; static const size_t total_bits = I + F; typedef detail::type_from_size base_type_info; typedef typename base_type_info::value_type base_type; typedef typename base_type_info::next_size::value_type next_type; typedef typename base_type_info::unsigned_type unsigned_type; public: static const size_t base_size = base_type_info::size; static const base_type fractional_mask = ~((~base_type(0)) << fractional_bits); static const base_type integer_mask = ~fractional_mask; public: static const base_type one = base_type(1) << fractional_bits; public: // constructors Fixed() : data_(0) { } Fixed(long n) : data_(base_type(n) << fractional_bits) { // TODO(eteran): assert in range! } Fixed(unsigned long n) : data_(base_type(n) << fractional_bits) { // TODO(eteran): assert in range! } Fixed(int n) : data_(base_type(n) << fractional_bits) { // TODO(eteran): assert in range! } Fixed(unsigned int n) : data_(base_type(n) << fractional_bits) { // TODO(eteran): assert in range! } Fixed(float n) : data_(static_cast(n * one)) { // TODO(eteran): assert in range! } Fixed(double n) : data_(static_cast(n * one)) { // TODO(eteran): assert in range! } Fixed(const Fixed &o) : data_(o.data_) { } Fixed& operator=(const Fixed &o) { data_ = o.data_; return *this; } private: // this makes it simpler to create a fixed point object from // a native type without scaling // use "Fixed::from_base" in order to perform this. struct NoScale {}; Fixed(base_type n, const NoScale &) : data_(n) { } public: static Fixed from_base(base_type n) { return Fixed(n, NoScale()); } public: // comparison operators bool operator==(const Fixed &o) const { return data_ == o.data_; } bool operator<(const Fixed &o) const { return data_ < o.data_; } public: // unary operators bool operator!() const { return !data_; } Fixed operator~() const { Fixed t(*this); t.data_ = ~t.data_; return t; } Fixed operator-() const { Fixed t(*this); t.data_ = -t.data_; return t; } Fixed operator+() const { return *this; } Fixed& operator++() { data_ += one; return *this; } Fixed& operator--() { data_ -= one; return *this; } public: // basic math operators Fixed& operator+=(const Fixed &n) { data_ += n.data_; return *this; } Fixed& operator-=(const Fixed &n) { data_ -= n.data_; return *this; } Fixed& operator&=(const Fixed &n) { data_ &= n.data_; return *this; } Fixed& operator|=(const Fixed &n) { data_ |= n.data_; return *this; } Fixed& operator^=(const Fixed &n) { data_ ^= n.data_; return *this; } Fixed& operator*=(const Fixed &n) { detail::multiply(*this, n, *this); return *this; } Fixed& operator/=(const Fixed &n) { Fixed temp; *this = detail::divide(*this, n, temp); return *this; } Fixed& operator>>=(const Fixed &n) { data_ >>= n.to_int(); return *this; } Fixed& operator<<=(const Fixed &n) { data_ <<= n.to_int(); return *this; } public: // conversion to basic types int to_int() const { return (data_ & integer_mask) >> fractional_bits; } unsigned int to_uint() const { return (data_ & integer_mask) >> fractional_bits; } float to_float() const { return static_cast(data_) / Fixed::one; } double to_double() const { return static_cast(data_) / Fixed::one; } base_type to_raw() const { return data_; } public: void swap(Fixed &rhs) { using std::swap; swap(data_, rhs.data_); } public: base_type data_; }; // if we have the same fractional portion, but differing integer portions, we trivially upgrade the smaller type template  typename std::conditional= I2, Fixed, Fixed>::type operator+(const Fixed &lhs, const Fixed &rhs) { typedef typename std::conditional< I1 >= I2, Fixed, Fixed >::type T; const T l = T::from_base(lhs.to_raw()); const T r = T::from_base(rhs.to_raw()); return l + r; } template  typename std::conditional= I2, Fixed, Fixed>::type operator-(const Fixed &lhs, const Fixed &rhs) { typedef typename std::conditional< I1 >= I2, Fixed, Fixed >::type T; const T l = T::from_base(lhs.to_raw()); const T r = T::from_base(rhs.to_raw()); return l - r; } template  typename std::conditional= I2, Fixed, Fixed>::type operator*(const Fixed &lhs, const Fixed &rhs) { typedef typename std::conditional< I1 >= I2, Fixed, Fixed >::type T; const T l = T::from_base(lhs.to_raw()); const T r = T::from_base(rhs.to_raw()); return l * r; } template  typename std::conditional= I2, Fixed, Fixed>::type operator/(const Fixed &lhs, const Fixed &rhs) { typedef typename std::conditional< I1 >= I2, Fixed, Fixed >::type T; const T l = T::from_base(lhs.to_raw()); const T r = T::from_base(rhs.to_raw()); return l / r; } template  std::ostream &operator<<(std::ostream &os, const Fixed &f) { os << f.to_double(); return os; } template  const size_t Fixed::fractional_bits; template  const size_t Fixed::integer_bits; template  const size_t Fixed::total_bits; } #endif 

Está diseñado para ser casi un reemplazo en reemplazo de flotadores / dobles y tiene una precisión elegible. Hace uso de boost para agregar todas las sobrecargas necesarias del operador matemático, por lo que también necesitará eso (creo que es solo una dependencia del encabezado, no una dependencia de la biblioteca).

Por cierto, el uso común podría ser algo como esto:

 using namespace numeric; typedef Fixed<16, 16> fixed; fixed f; 

La única regla real es que el número debe sumr un tamaño nativo de su sistema, como 8, 16, 32, 64.

En las implementaciones modernas de C ++, no habrá penalización de rendimiento por usar abstracciones simples y magras, como clases concretas. El cálculo de punto fijo es precisamente el lugar donde usar una clase diseñada adecuadamente lo salvará de muchos errores.

Por lo tanto, debe escribir una clase FixedPoint8 . Pruébalo y depúralo a fondo. Si tiene que convencerse de su rendimiento en comparación con el uso de enteros simples, mida.

Le ahorrará muchos problemas al mover la complejidad del cálculo del punto fijo a un solo lugar.

Si lo desea, puede boost aún más la utilidad de su clase convirtiéndola en una plantilla y reemplazando el antiguo FixedPoint8 con, digamos, typedef FixedPoint FixedPoint8; Pero en su architecture de destino esto probablemente no sea necesario, así que evite la complejidad de las plantillas al principio.

Probablemente haya una buena clase de punto fijo en Internet, comenzaría a buscar en las bibliotecas de Boost .

¿Su código de coma flotante realmente hace uso del punto decimal? Si es así:

En primer lugar, debe leer el artículo de Randy Yates sobre Intro to Fixed Point Math: http://www.digitalsignallabs.com/fp.pdf

Luego necesita hacer un “perfil” en su código de coma flotante para calcular el rango apropiado de valores de punto fijo requeridos en puntos “críticos” en su código, por ejemplo U (5,3) = 5 bits a la izquierda, 3 bits a la derecha, sin firmar.

En este punto, puede aplicar las reglas aritméticas en el documento mencionado anteriormente; las reglas especifican cómo interpretar los bits que resultan de las operaciones aritméticas. Puede escribir macros o funciones para realizar las operaciones.

Es útil mantener la versión de punto flotante para comparar los resultados de punto flotante vs punto fijo.

No usaría ningún punto flotante en una CPU sin hardware especial para manejarlo. Mi consejo es tratar TODOS los números como enteros escalados a un factor específico. Por ejemplo, todos los valores monetarios están en centavos como enteros en lugar de dólares como flotantes. Por ejemplo, 0.72 se representa como el entero 72.

La sum y la resta son entonces una operación entera muy simple tal como (0.72 + 1 se convierte en 72 + 100 se convierte en 172 se convierte en 1.72).

La multiplicación es un poco más compleja ya que necesita una multiplicación de números enteros seguida de una reducción de escala como (0.72 * 2 se convierte en 72 * 200 se convierte en 14400 se convierte en 144 (regresión escalar) se convierte en 1.44).

Eso puede requerir funciones especiales para realizar operaciones matemáticas más complejas (seno, coseno, etc.), pero incluso esas pueden acelerarse mediante el uso de tablas de búsqueda. Ejemplo: ya que está utilizando la representación fija-2, solo hay 100 valores en el rango (0.0,1) (0-99) y sin / cos repetición fuera de este rango, por lo que solo necesita una tabla de búsqueda de 100 enteros.

Saludos, Pax.

El cambio de representaciones de puntos fijos se denomina comúnmente ‘escalado’.

Si puedes hacer esto con una clase sin penalización de rendimiento, entonces ese es el camino a seguir. Depende en gran medida del comstackdor y de cómo se inserta. Si hay una penalización de rendimiento usando clases, entonces necesitas un enfoque más tradicional de estilo C. El enfoque de OOP le proporcionará seguridad de tipo implementada por el comstackdor que la implementación tradicional solo se aproxima.

@cibyr tiene una buena implementación de OOP. Ahora para el más tradicional.

Para mantener un registro de las variables escaladas, debe usar una convención consistente. Haga una anotación al final de cada nombre de variable para indicar si el valor está escalado o no, y escriba las macros SCALE () y UNSCALE () que se expanden a x >> 8 yx << 8.

 #define SCALE(x) (x>>8) #define UNSCALE(x) (x<<8) xPositionUnscaled = UNSCALE(10); xPositionScaled = SCALE(xPositionUnscaled); 

Puede parecer mucho trabajo utilizar tanta notación, pero observe cómo puede ver de un vistazo que cualquier línea es correcta sin mirar otras líneas. Por ejemplo:

 xPositionScaled = SCALE(xPositionScaled); 

obviamente está mal, por inspección.

Esta es una variación de la idea húngara de Apps que Joel menciona en esta publicación .

Cuando me encontré por primera vez con los números de punto fijo encontré el artículo de Joe Lemieux, Fixed-point Math in C , muy útil, y sugiere una forma de representar los valores de punto fijo.

Sin embargo, no terminé usando su representación sindical para números de punto fijo. En su mayoría tengo experiencia con el punto fijo en C, por lo que tampoco he tenido la opción de usar una clase. En su mayor parte, creo que la definición del número de bits de fracción en una macro y el uso de nombres de variables descriptivos hace que sea bastante fácil trabajar con ella. Además, he descubierto que es mejor tener macros o funciones para la multiplicación y especialmente la división, o rápidamente se obtiene un código ilegible.

Por ejemplo, con 24.8 valores:

  #include "stdio.h" /* Declarations for fixed point stuff */ typedef int int_fixed; #define FRACT_BITS 8 #define FIXED_POINT_ONE (1 << FRACT_BITS) #define MAKE_INT_FIXED(x) ((x) << FRACT_BITS) #define MAKE_FLOAT_FIXED(x) ((int_fixed)((x) * FIXED_POINT_ONE)) #define MAKE_FIXED_INT(x) ((x) >> FRACT_BITS) #define MAKE_FIXED_FLOAT(x) (((float)(x)) / FIXED_POINT_ONE) #define FIXED_MULT(x, y) ((x)*(y) >> FRACT_BITS) #define FIXED_DIV(x, y) (((x)< 

Lo cual escribe

 9.0
 4.5

Tenga en cuenta que hay todo tipo de problemas de desbordamiento de enteros con esas macros, solo quería mantener las macros simples. Este es solo un ejemplo rápido y sucio de cómo he hecho esto en C. En C ++ podrías hacer algo mucho más limpio usando la sobrecarga del operador. En realidad, fácilmente podrías hacer ese código C mucho más bonito también ...

Supongo que esta es una forma larga de decir: creo que está bien usar un enfoque typedef y macro. Siempre que tenga claro qué variables contienen valores de punto fijo, no es demasiado difícil de mantener, pero probablemente no será tan bonito como una clase de C ++.

Si estuviera en su posición, trataría de obtener algunos números de perfil para mostrar dónde están los cuellos de botella. Si hay relativamente pocos, vaya con typedef y macros. Sin embargo, si decides que necesitas un reemplazo global de todas las carrozas con matemáticas de punto fijo, entonces probablemente estarás mejor con una clase.

La versión original de Tricks of the Game Programming Gurus tiene un capítulo completo sobre la implementación de matemática de punto fijo.

Cualquiera que sea la forma en que decida ir (me inclinaría hacia un typedef y algunas macros de CPP para la conversión), deberá tener cuidado de convertir hacia adelante y hacia atrás con cierta disciplina.

Puede encontrar que nunca necesita convertir de ida y vuelta. Solo imagine que todo en el sistema completo es x256.

 template  class FixedPoint { private: int val_; public: inline FixedPoint(int val) : val_ (val << precision) {}; inline operator int() { return val_ >> precision; } // Other operators... };