#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);
}
Я подумал о следующих подходах:
-
Использование макросов для имени функции. Для этого все еще требуются две отдельные базы исходного кода:
#ifdef USE_FLOAT #define MYFUNC MyFloatFunc #else #define MYFUNC MyDoubleFunc #endif
-
Использование макросов для типов с плавающей запятой. Это позволяет мне совместно использовать кодовую базу для двух разных версий:
#ifdef USE_FLOAT #define NUMBER float #else #define NUMBER double #endif
-
Просто разрабатываю две отдельные библиотеки и забываю о попытках избежать головной боли.
У кого-нибудь есть рекомендации или дополнительные предложения?
Ответ №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?