単精度浮動小数点と倍精度浮動小数点の両方を扱うCコードの適切な設計?

Cで特殊目的の数学関数のライブラリを開発しています。単精度と倍精度の両方を扱うライブラリを提供する必要があります。ここでの重要な点は、 "single"関数は内部的に "single"算術のみを使うべきであるということです( "double"関数の場合)。

例として、 LAPACK (Fortran)を見てください。これは、それぞれの機能の2つのバージョンを提供します(シングルとダブル)。 C数学ライブラリ(例: expfexp

わかりやすくするために、私は次のような(意図した)例に似た何かをサポートしたい:

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. Using macros for the function name. This still requires two separate source codebases:

    #ifdef USE_FLOAT
    #define MYFUNC MyFloatFunc
    #else
    #define MYFUNC MyDoubleFunc
    #endif
    
  2. Using macros for the floating point types. This allows me to share the codebase across the two different versions:

    #ifdef USE_FLOAT
    #define NUMBER float
    #else
    #define NUMBER double
    #endif
    
  3. Just developing two separate libraries, and forgetting about trying to save headaches.

誰か推奨または追加の提案がありますか?

8

4 答え

多項式近似、補間、その他の本質的に近似的な数学関数では、単精度バージョンで時間を無駄にしたり、倍精度バージョンで必要以上に近似することなく、倍精度実装と単精度実装の間でコードを共有することはできません。

それにもかかわらず、単一のコードベースのルートを参照すると、以下のコードは定数と標準ライブラリ関数で機能するはずです。

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

... C(2.0) ... C(sin) ...
7
追加された
はい、その素晴らしい点に感謝します。ここでの目標は、 "single"の実行速度を "double"の精度上の利点とトレードオフすることです。
追加された 著者 David H,

(部分的にPascal Cuoqの答えに触発された) すべての浮動小数点バージョンと2つのバージョンのライブラリが必要な場合は、再帰的な #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回目のパスで異なるマクロ定義を使用して1回追加します。

しかし、このアプローチには反対する人もいます。

5
追加された
非常に怖い - 非常に興味深い。ヘッダーファイルが#includeできることは決して知りませんでした。このトリックがCプリプロセッサをTuring-complete(C ++のテンプレートのようなもの)にするのだろうか?
追加された 著者 David H,
私は実装のためにこのアプローチを使用するかもしれません。私は決してヘッダーのためにそれを使用しません。ユーザーはライブラリを使いたいときにこのような実装の詳細を見る必要はありません。また、すべてのマクロ定義をインクルードファイルに移動します。
追加された 著者 Stephen Canon,

あなたにとっての大きな疑問は次のようなものです:

  • 2つの独立したソースツリー、または難読化された1つのソースツリーを維持する方が簡単ですか?

提案されている共通のコーディングがある場合は、未定義の定数や非マクロ関数呼び出し(または関数本体)を記述しないように注意しながら、コードを静的な方法で記述する必要があります。

別々のソースコードツリーを持っているならば、各ツリーが普通の(難読化されていない)Cコードのように見えるという点で、コードはより簡単になりますが、あなたの関数 'Float'バージョンにバグがある場合、 'ダブル'バージョンでのマッチングの変更を行います。

私はこれが機能の複雑さとボラティリティに依存していると思います。私の疑惑は、一度書き込まれて初めてデバッグされた場合、それに戻っていく必要はほとんどありません。これは、実際にどのメカニズムを使用するかは重要ではないことを意味します。どちらも実行可能です。関数本体がやや揮発性であるか、または関数のリストがvolatileである場合、単一のコードベースは全体的に簡単になります。すべてが非常に安定していれば、2つの別々のコードベースの明快さが望ましいかもしれません。しかし、それは非常に主観的です。

私はおそらく、単一のコードベースとwall-to-wallマクロを使うでしょう。しかし、私はそれが最善であるとは確信していませんし、他の方法も利点があります。

2
追加された

The header, standardized in C 1999, provides type-generic calls to the routines in and . After you include , the source text "sin(x)" will call sinl if x is long double, sin if x is double, and sinf if x is float.

あなたはまだ "3.1"または "3.1f"を適切に使うように定数を条件付けする必要があります。あなたのニーズやより美的に見えるものに応じて、さまざまな統語テクニックがあります。 float精度で正確に表現された定数については、単にfloat形式を使用することができます。たとえば、「y = .5f * x」は、xがdoubleの場合、.5fを.5に自動的に変換します。しかし、 "sin(0.5f)"はsinf(0.5f)を生成しますが、これはsin(0.5)よりも正確ではありません。

条件設定を1つの明確な定義に減らすことができます。

#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;

まったく満足できるものではないかもしれません。なぜなら、二重に変換された後、浮動小数点数が浮動小数点に直接変換された数値よりも正確さが低い場合があるからです。したがって、上記のマクロでは、C(数字)が必要に応じて数字に接尾辞を付加する方がよい場合があります。

1
追加された
tgmath.hへのポインタありがとう。それが存在することを知ってうれしいですが、タイプ情報を使ってどの関数を呼び出すかを選択しますか?そのことがどうしてC99に入ったのですか?
追加された 著者 Pascal Cuoq,