Правильный дизайн кода на C, который обрабатывает как одинарную, так и двойную точность с плавающей запятой?

#c #floating-point

#c #с плавающей запятой

Вопрос:

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

У кого-нибудь есть рекомендации или дополнительные предложения?

Ответ №1:

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

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

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

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

Комментарии:

1. Да, спасибо за это отличное замечание. Цель здесь — найти компромисс между скоростью выполнения «single» и преимуществом точности «double».

Ответ №2:

(Частично вдохновленный ответом Паскаля Куока) Если вам нужна одна библиотека с плавающей и двойной версиями всего, вы можете использовать рекурсивные #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 повторяется один раз, используя разные определения макросов на втором проходе, чтобы сгенерировать две версии кода, использующего макросы.

Однако некоторые люди могут возражать против такого подхода.

Комментарии:

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

2. Очень страшно — и очень интересно. Никогда не знал, что файл заголовка может #включать себя. Интересно, делает ли этот трюк препроцессор C завершенным по Тьюрингу (например, шаблоны C )?

Ответ №3:

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

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

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

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

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

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

Ответ №4:

<tgmath.h> Заголовок, стандартизированный в C 1999, обеспечивает типовые вызовы подпрограмм в <math.h> и <complex.h> . После включения <tgmath.h> ; исходный текст sin(x) вызовет sinl if x is long double , sin if x is double и sinf if x is float .

Вам все равно нужно будет обусловить ваши константы, чтобы вы использовали 3.1 или 3.1f по мере необходимости. Для этого существует множество синтаксических приемов, в зависимости от ваших потребностей и того, что вам кажется более эстетичным. Для констант, которые точно представлены с float точностью, вы можете просто использовать форму с плавающей запятой. Например, y = .5f * x автоматически преобразуется .5f в double if x is double . Однако sin(.5f) будет выдавать sinf(.5f) , что менее точно, чем sin(.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 , менее точна, чем цифра, преобразованная непосредственно в float . Поэтому вам может быть лучше использовать макрос, описанный выше, где C(numeral) при необходимости добавляется суффикс к цифре.

Комментарии:

1. Спасибо за указатель на tgmath.h. Я рад узнать, что он существует, но использовать информацию о типе для выбора вызываемой функции? Как эта штука попала в C99?