Correct ontwerp van de C-code die zowel drijvende komma met enkele als met dubbele precisie behandelt?

Ik ontwikkel een bibliotheek met speciale wiskundige functies in C. Ik moet de bibliotheek de mogelijkheid bieden om zowel single-precision als double-precision te gebruiken. Het belangrijke punt hier is dat de "enkele" functies ALLEEN "enkele" rekenkunde intern moeten gebruiken (of voor de "dubbele" functies).

Ter illustratie, kijk eens naar LAPACK (Fortran), die twee versies van elk van zijn functies biedt ( ENKEL en DUBBEL). Ook de C-wiskundebibliotheek (bijvoorbeeld expf en exp ).

Ter verduidelijking wil ik iets ondersteunen dat lijkt op het volgende (gekunstelde) voorbeeld:

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

Ik heb nagedacht over de volgende benaderingen:

  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.

Heeft iemand een aanbeveling of aanvullende suggesties?

8

4 antwoord

Voor polynoombenaderingen, interpolaties en andere inherent approximatieve wiskundige functies, kunt u geen code delen tussen een precisie met dubbele precisie en een implementatie met slechts één precisie zonder tijd te verspillen in de versie met enkele precisie of meer approximatief dan noodzakelijk in de dubbele precisie-versie. .

Niettemin, als je de route van de enkele codebasis volgt, zou het volgende moeten werken voor constanten en standaard bibliotheekfuncties:

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

... C(2.0) ... C(sin) ...
7
toegevoegd
Ja, bedankt voor dat uitstekende punt. Het doel hier is om de uitvoeringssnelheid van de "single" af te zetten tegen het precisievoordeel van de "dubbele".
toegevoegd de auteur David H, de bron

(Gedeeltelijk geïnspireerd door het antwoord van Pascal Cuoq) Als u een bibliotheek met zwevende en dubbele versies van alles wilt, kunt u recursieve # include s gebruiken in combinatie met macro's. Het resulteert niet in de duidelijkste code, maar het laat je dezelfde code gebruiken voor beide versies, en de verduistering is dun genoeg, het is waarschijnlijk hanteerbaar:

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

Elk bestand # omvat is zelf een extra tijd, gebruikmakend van verschillende macrodefinities op de tweede passage, om twee versies van de code te genereren die de macro's gebruikt.

Sommige mensen hebben echter bezwaar tegen deze benadering.

5
toegevoegd
Heel eng - en heel interessant. Nooit geweten dat een header-bestand # zichzelf kan bevatten. Ik vraag me af of deze truc C preprocessor Turing-compleet maakt (zoals de C ++ -sjablonen)?
toegevoegd de auteur David H, de bron
Ik zou deze benadering kunnen gebruiken voor de implementaties. Ik zou het nooit voor de kop gebruiken. Mensen zouden dit soort implementatiedetail niet hoeven te zien wanneer ze gewoon een bibliotheek willen gebruiken. Ik zou ook alle macrodefinities verplaatsen naar een include-bestand.
toegevoegd de auteur Stephen Canon, de bron

De grote vraag voor jou zal zijn:

  • Is het gemakkelijker om twee afzonderlijke ongeobsellde bronbomen of één versluierde bron te onderhouden?

Als u de voorgestelde algemene codering hebt, moet u de code op een hoogdravende manier schrijven, waarbij u erop let dat u geen onversierde constanten of niet-macro-functieaanroepen (of functie-instanties) schrijft.

Als je aparte broncodebomen hebt, is de code eenvoudiger te onderhouden omdat elke boom eruitziet als normale (niet-versluierde) C-code, maar als er een fout in YourFunctionA in de 'float'-versie voorkomt, zul je altijd onthouden om de aanpassing in de 'dubbele' versie te maken.

Ik denk dat dit afhangt van de complexiteit en volatiliteit van de functies. Mijn vermoeden is dat er, eenmaal geschreven en debugged de eerste keer, zelden zal moeten worden teruggegaan. Dit betekent eigenlijk dat het niet veel uitmaakt welk mechanisme je gebruikt - beide zullen werkbaar zijn. Als de functielichamen enigszins vluchtig zijn of als de lijst met functies vluchtig is, kan de basis van de enkele code over het algemeen eenvoudiger zijn. Als alles erg stabiel is, kan de duidelijkheid van de twee afzonderlijke codebases dit de voorkeur geven. Maar het is erg subjectief.

Ik zou waarschijnlijk met een enkele codebasis en macro's van muur tot muur gaan. Maar ik weet niet zeker of dat het beste is, en de andere manier heeft ook voordelen.

2
toegevoegd

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.

U moet nog steeds uw constanten conditioneren, zodat u "3.1" of "3.1f" gebruikt, indien van toepassing. Hiervoor zijn verschillende syntactische technieken beschikbaar, afhankelijk van uw behoeften en wat voor u meer esthetisch lijkt. Voor constanten die precies worden weergegeven in drijvende precisie, kunt u eenvoudig de vlottervorm gebruiken. Bijvoorbeeld: "y = .5f * x" zal automatisch .5f naar .5 converteren als x dubbel is. Echter, "sin (.5f)" zal sinf (.5f) produceren, wat minder accuraat is dan sin (.5).

Mogelijk kunt u de conditionalisatie terugbrengen tot één duidelijke definitie:

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

Dan kunt u constanten op manieren zoals deze gebruiken:

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

Dat is misschien niet helemaal bevredigend, omdat er zeldzame gevallen zijn waarbij een cijfer omgezet in dubbel en vervolgens zwevend minder nauwkeurig is dan een cijfer dat direct in zwevend is omgezet. U bent dus misschien beter af met een hierboven beschreven macro, waarbij "C (cijfer)" een achtervoegsel aan het cijfer toevoegt, indien nodig.

1
toegevoegd
Bedankt voor de aanwijzer naar tmmath.h. Ik ben blij om te weten dat het bestaat, maar met behulp van type-informatie om te kiezen welke functie wordt genoemd? Hoe kwam dat ding in C99?
toegevoegd de auteur Pascal Cuoq, de bron