2011-10-27 24 views
8

Estoy desarrollando una biblioteca de funciones matemáticas especiales en C. Necesito proporcionar una capacidad para que la biblioteca maneje tanto precisión simple como precisión doble. El punto importante aquí es que las funciones "únicas" deben usar SÓLO aritmética "única" internamente (para las funciones "dobles").¿Diseño adecuado del código C que maneja punto flotante de precisión simple y doble?

Como ilustración, consulte LAPACK (Fortran), que proporciona dos versiones de cada una de sus funciones (SINGLE y DOUBLE). También la biblioteca matemática C (ejemplo, expf y exp).

Para aclarar, yo quiero apoyar algo similar a la (artificial) siguiente ejemplo:

float MyFloatFunc(float x) { 
    return expf(-2.0f * x)*logf(2.75f*x); 
} 

double MyDoubleFunc(double x) { 
    return exp(-2.0 * x)*log(2.75*x); 
} 

He pensado en los siguientes enfoques:

  1. Uso de macros para el nombre de la función . Esto todavía requiere dos bases de código fuente separados:

    #ifdef USE_FLOAT 
    #define MYFUNC MyFloatFunc 
    #else 
    #define MYFUNC MyDoubleFunc 
    #endif 
    
  2. el uso de macros para los tipos de punto flotante. Esto me permite compartir la base de código a través de las dos versiones diferentes:

    #ifdef USE_FLOAT 
    #define NUMBER float 
    #else 
    #define NUMBER double 
    #endif 
    
  3. Sólo el desarrollo de dos bibliotecas independientes, y olvidarse de tratar de ahorrar dolores de cabeza.

¿Alguien tiene una recomendación o sugerencias adicionales?

Respuesta

7

Para aproximaciones polinomiales, interpolaciones y otras funciones matemáticas aproximadas inherentes, no puede compartir código entre una implementación de precisión doble y una de precisión simple sin perder tiempo en la versión de precisión simple o ser más aproximado de lo necesario en el de doble precisión.

Sin embargo, si vas a la ruta de la base de código único, el siguiente debe trabajar para las constantes y funciones de la biblioteca estándar:

#ifdef USE_FLOAT 
#define C(x) x##f 
#else 
#define C(x) x 
#endif 

... C(2.0) ... C(sin) ... 
+0

Sí, gracias por ese excelente punto. El objetivo aquí es compensar la velocidad de ejecución del "sencillo" con la ventaja de precisión del "doble". –

2

La gran pregunta para usted será:

  • ¿Es ¿es más fácil mantener dos árboles fuente no difuminados separados, o uno ofuscado?

Si tiene la codificación común propuesta, tendrá que escribir el código de forma forzada, teniendo mucho cuidado de no escribir constantes no decodificadas o llamadas a función no macro (o cuerpos de función).

Si tiene árboles de código fuente separados, el código será más simple de mantener, ya que cada árbol se verá como código C normal (no ofuscado), pero si hay un error en YourFunctionA en la versión 'flotante', ¿Siempre recordarás hacer el cambio correspondiente en la versión 'doble'?

Creo que esto depende de la complejidad y la volatilidad de las funciones. Mi sospecha es que una vez escrita y depurada la primera vez, rara vez habrá necesidad de volver a ella. Esto realmente significa que no importa mucho el mecanismo que use, ambos serán factibles.Si los cuerpos de función son algo volátiles, o la lista de funciones es volátil, entonces la base de código único puede ser más fácil en general. Si todo es muy estable, la claridad de las dos bases de código separadas puede hacer que sea preferible. Pero es muy subjetivo.

Probablemente vaya con una base de código única y macros de pared a pared. Pero no estoy seguro de que sea lo mejor, y el otro camino también tiene sus ventajas.

5

(Parcialmente inspirado en la respuesta de Pascal Cuoq) Si desea una biblioteca con versiones flotantes y dobles de todo, puede usar recursivas #include s en combinación con macros. Que no da lugar a la más clara de código, pero no le permite utilizar el mismo código para las dos versiones, y la ofuscación es lo suficientemente delgada como es probable que sea manejable:

mylib.h:

#ifndef MYLIB_H_GUARD 
    #ifdef MYLIB_H_PASS2 
    #define MYLIB_H_GUARD 1 
    #undef C 
    #undef FLT 
    #define C(X) X 
    #define FLT double 
    #else 
    /* any #include's needed in the header go here */ 

    #undef C 
    #undef FLT 
    #define C(X) X##f 
    #define FLT float 
    #endif 

    /* All the dual-version stuff goes here */ 
    FLT C(MyFunc)(FLT x); 

    #ifndef MYLIB_H_PASS2 
    /* prepare 2nd pass (for 'double' version) */ 
    #define MYLIB_H_PASS2 1 
    #include "mylib.h" 
    #endif 
#endif /* guard */ 

mylib.c:

#ifdef MYLIB_C_PASS2 
    #undef C 
    #undef FLT 
    #define C(X) X 
    #define FLT double 
#else 
    #include "mylib.h" 
    /* other #include's */ 

    #undef C 
    #undef FLT 
    #define C(X) X##f 
    #define FLT float 
#endif 

/* All the dual-version stuff goes here */ 
FLT C(MyFunc)(FLT x) 
{ 
    return C(exp)(C(-2.0) * x) * C(log)(C(2.75) * x); 
} 

#ifndef MYLIB_C_PASS2 
    /* prepare 2nd pass (for 'double' version) */ 
    #define MYLIB_C_PASS2 1 
    #include "mylib.c" 
#endif 

Cada archivo #include s sí mismo una vez adicional, el uso de diferentes definiciones de macro en la segunda pasada, para generar dos versiones de el código que usa las macros.

Sin embargo, algunas personas pueden oponerse a este enfoque.

+0

Podría usar este enfoque para las implementaciones. Nunca lo usaría para el encabezado. La gente no debería necesitar ver este tipo de detalles de implementación cuando solo quieren usar una biblioteca. También movería todas las definiciones de macro a un archivo de inclusión. –

+0

Muy aterrador, y muy interesante. Nunca supe que un archivo de encabezado podría #incluirse a sí mismo. Me pregunto si este truco hace que el preprocesador C Turing-complete (como las plantillas C++)? –

1

El <tgmath.h> cabecera, estandarizado en C 1999, proporciona llamadas de tipo genérico a las rutinas en <math.h> y <complex.h>. Después de incluir <tgmath.h>, el texto de origen "sin (x)" llamará a sinl si x es largo doble, sin si x es doble y sinf si x es flotante.

Aún necesitará condicionalizar sus constantes, para que use "3.1" o "3.1f" según corresponda. Hay una variedad de técnicas sintácticas para esto, según sus necesidades y lo que le parezca más estético. Para las constantes que están representadas exactamente en la precisión de flotación, puede simplemente usar la forma de flotación. Por ejemplo, "y = .5f * x" convertirá automáticamente .5f a .5 si x es doble. Sin embargo, "sin (.5f)" producirá sinf (.5f), que es menos preciso que sin (.5).

Usted puede ser capaz de reducir la condicionalización a una única definición clara:

#if defined USE_FLOAT 
    typedef float Float; 
#else 
    typedef double Float; 
#endif 

continuación, puede utilizar las constantes en formas como esto:

const Float pi = 3.14159265358979323846233; 
Float y = sin(pi*x); 
Float z = (Float) 2.71828182844 * x; 

que podría no ser completamente satisfactoria porque hay son casos raros en los que un número convertido al doble y luego a flotar es menos preciso que un número convertido directamente en flotación. Por lo tanto, es posible que esté mejor con una macro descrita anteriormente, donde "C (numeral)" agrega un sufijo al número si es necesario.

+0

Gracias por el puntero a tgmath.h. Me complace saber que existe, pero ¿con el tipo de información para elegir qué función se llama? ¿Cómo fue que esa cosa llegó a C99? –

Cuestiones relacionadas