Надлежащий дизайн кода C, который обрабатывает как с плавающей запятой одинарной, так и двойной точности? - PullRequest
8 голосов
/ 27 октября 2011

Я занимаюсь разработкой библиотеки математических функций специального назначения на языке 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 ]

7 голосов
/ 27 октября 2011

Для полиномиальных аппроксимаций, интерполяций и других по своей сути аппроксимирующих математических функций вы не можете совместно использовать код для реализации с двойной точностью и с одинарной точностью, не тратя время в версии с одинарной точностью или будучи более приближенным, чем необходимо в двойной-precision one.

Тем не менее, если вы идете по пути одиночной кодовой базы, для констант и стандартных функций библиотеки должно работать следующее:

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

... C(2.0) ... C(sin) ...
5 голосов
/ 28 октября 2011

(частично вдохновлен ответом Паскаля Куока). Если вам нужна одна библиотека с плавающей и двойной версиями всего, вы можете использовать рекурсивные #include s в сочетании с макросами.Это не приводит к ясному коду, но позволяет использовать один и тот же код для обеих версий, а запутывание достаточно тонкое, вероятно, оно управляемо:

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 голосов
/ 27 октября 2011

Большой вопрос для вас будет:

  • Проще ли поддерживать два отдельных необфокусированных дерева источника или одно обфусцированное дерево?

Если у вас есть предложенное общее кодирование, вам придется писать код в заштрихованном виде, очень осторожно, чтобы не записывать какие-либо недекорированные константы или вызовы немакрофункций (или тел функций).

Если у вас есть отдельные деревья исходного кода, код будет проще поддерживать, так как каждое дерево будет выглядеть как обычный (не обфусцированный) код C, но если в YourFunctionA есть ошибка в версии с плавающей точкой, вы всегда помните, чтобы сделать соответствующие изменения в «двойной» версии.

Я думаю, что это зависит от сложности и изменчивости функций. Я подозреваю, что после первого написания и отладки редко будет необходимость возвращаться к нему. На самом деле это означает, что не имеет большого значения, какой механизм вы используете - оба будут работоспособны. Если тела функций несколько изменчивы или список функций изменчив, тогда единая база кода может быть проще в целом. Если все очень стабильно, ясность двух отдельных баз кода может сделать это предпочтительным. Но это очень субъективно.

Я бы, наверное, пошел с одной базой кода и макросами "от стены к стене". Но я не уверен, что это лучше, а другой способ тоже имеет свои преимущества.

1 голос
/ 29 октября 2011

Заголовок , стандартизированный в C 1999, предоставляет общие вызовы для подпрограмм в и . После того как вы включите , исходный текст «sin (x)» будет вызывать sinl, если x - это long double, sin, если x - double, и sinf, если x - float.

Вам все равно нужно будет обусловить свои константы, чтобы вы могли использовать «3.1» или «3.1f» в зависимости от ситуации. Для этого существует множество синтаксических техник, в зависимости от ваших потребностей и того, что кажется вам более эстетичным. Для констант, которые точно представлены в точности с плавающей точкой, вы можете просто использовать форму с плавающей точкой. Например, "y = .5f * x" автоматически преобразует .5f в .5, если x равен двойному. Однако «грех (.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;

Это может быть не совсем удовлетворительным, поскольку в редких случаях число, преобразованное в двойное, а затем в плавающее, является менее точным, чем число, преобразованное непосредственно в плавающее. Поэтому вам может быть лучше использовать макрос, описанный выше, где «C (цифра)» добавляет суффикс к цифре, если необходимо.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...