正确设计C代码,处理单精度和双精度浮点?

时间:2011-10-27 19:14:33

标签: c floating-point

我正在用C开发一个专用数学函数库。我需要为库提供一个处理单精度和双精度的功能。这里重要的一点是“单个”函数应该在内部使用“单个”算术(分别用于“双重”函数)。

作为示例,请查看LAPACK(Fortran),它提供了每个函数的两个版本(SINGLE和DOUBLE)。还有C数学库(例如, expf exp )。

为了澄清,我想支持类似于以下(人为的)示例的内容:

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);
}

我考虑过以下方法:

  1. 将宏用于函数名称。这仍然需要两个独立的源代码库:

    #ifdef USE_FLOAT
    #define MYFUNC MyFloatFunc
    #else
    #define MYFUNC MyDoubleFunc
    #endif
    
  2. 将宏用于浮点类型。这允许我在两个不同版本之间共享代码库:

    #ifdef USE_FLOAT
    #define NUMBER float
    #else
    #define NUMBER double
    #endif
    
  3. 刚开发两个独立的库,忘记尝试保存头痛。

  4. 有人有推荐或其他建议吗?

4 个答案:

答案 0 :(得分:7)

对于多项式近似,插值和其他固有近似数学函数,您不能在双精度和单精度实现之间共享代码,而不会在单精度版本中浪费时间或在双精度版本中更加近似-precision one。

然而,如果你走单一代码库的路线,下面的代码应该适用于常量和标准库函数:

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

... C(2.0) ... C(sin) ...

答案 1 :(得分:5)

(部分灵感来自Pascal Cuoq的回答) 如果你想要一个具有float和双重版本的库的库,你可以将递归#include与宏结合使用。它不会产生最清晰的代码,但它确实允许您对两个版本使用相同的代码,并且模糊处理足够薄,它可能是可管理的:

<强> 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

每个文件#include本身在第二遍中使用不同的宏定义一次,以生成使用宏的代码的两个版本。

但有些人可能会反对这种做法。

答案 2 :(得分:2)

最重要的问题是:

  • 维护两个单独的未经混淆的源树或一个混淆的源树是否更容易?

如果您有建议的通用编码,则必须以高跷的方式编写代码,小心不要编写任何未修饰的常量或非宏函数调用(或函数体)。

如果您有单独的源代码树,代码将更容易维护,因为每个树看起来都像普通(非混淆)C代码,但如果'浮动'版本中的YourFunctionA中存在错误,则会你总是记得在'双'版本中进行匹配的改变。

我认为这取决于功能的复杂性和波动性。我的怀疑是,一旦第一次编写和调试,很少需要回到它。这实际上意味着你使用哪种机制并不重要 - 两者都可行。如果函数体有些不稳定,或者函数列表是易失性的,则单个代码库可能更容易整体。如果一切都非常稳定,那么两个独立代码库的清晰度可能会更加优越。但这是非常主观的。

我可能会使用单个代码库和从墙到墙的宏。但我不确定那是最好的,另一种方式也有它的优点。

答案 3 :(得分:1)

&lt; tgmath.h&gt;标题,在C 1999中标准化,提供对&lt; math.h&gt;中的例程的类型泛型调用。和&lt; complex.h&gt;。在包含&lt; tgmath.h&gt;之后,如果x是long double,源文本“sin(x)”将调用sinl,如果x是double,则调用sin,如果x是float,则调用sinf。

您仍需要对常量进行条件化,以便根据需要使用“3.1”或“3.1f”。根据您的需要以及对您来说更美观的方法,有各种各样的句法技巧。对于以float精度表示的常量,您可以简单地使用float表单。例如,如果x是double,“y = .5f * x”将自动将.5f转换为.5。然而,“罪(.5f)”将产生sinf(.5f),这不如罪(.5)准确。

您可以将条件化简化为一个明确的定义:

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

然后你可以用这样的方式使用常量:

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

这可能不完全令人满意,因为在极少数情况下,数字转换为double然后浮动不如直接转换为float的数字准确。因此,使用上面描述的宏可能会更好,其中“C(数字)”会在必要时在数字后附加一个后缀。