VisualDSP: использование типов с фиксированной точкой
Добавил(а) microsin
В этом документе (перевод раздела "Using Native Fixed-Point Types" документации [1]) предоставлен обзор поддержки компилятором VisualDSP традиционных типов с фиксированной точкой (native fixed-point types) fract и accum, как это определено в Главе 4 "Extensions to support embedded processors" ISO/IEC draft document Technical Report 18037.
[Поддержка типов с фиксированной точкой]
Тип данных с фиксированной точкой это такой тип, у которого точка основания, разделяющая целую и дробную части, находится в фиксированной позиции внутри бит двоичного числа заданной разрядности. Это включает целые типы (точка находится непосредственно справа от самого младшего значащего бита числа). Однако в здесь используется термин "фиксированная точка" только к тем случаям представления дробных чисел, когда под дробную часть выделено не нулевое количество бит числа, т. е. есть в наличии биты, стоящие справа после точки. Могут быть также биты под целую часть числа, стоящие слева от точки.
У процессора Blackfin имеется встроенная аппаратная поддержка для арифметики на числах с такими типами данных, имеющих фиксированную точку. Например, процессор может выполнять сложение, вычитание и умножение на 16-битных и 32-битных дробных значениях. Однако язык C не предусматривает простого расширения семантики арифметики, которая позволит использовать поддержку нижележащей аппаратуры.
Чтобы упростить использование этой аппаратной возможности и применить расширение языка для алгоритмов DSP, которые манипулируют типами с фиксированной точкой, компилятор поддерживает набор типов native fixed-point, арифметика которых подчиняется семантике чисел с фиксированной точкой. Это упрощает написание высокопроизводительных алгоритмов для манипуляции данными с фиксированной точкой, без необходимости обращаться к встроенным функциям компилятора (compiler built-ins), или встраиваемому коду на языке ассемблера (inline assembly).
Появление стандарта для таких типов с плавающей точкой установлено в Главе 4 "Extensions to support embedded processors" ISO/IEC Technical Report 18037. VisualDSP++ предоставляет всю функциональность, обозначенную в этой главе, и эта глава является полезным справочником, объясняющим тонкости семантики библиотечных функций и арифметических операторов. Однако в следующих секциях дается обзор этих типов данных, семантики арифметики использования этих типов, и руководства о том, как писать высокопроизводительный код с использованием этих типов.
Традиционные типы с плавающей точкой (Native Fixed-Point Types). Два ключевых слова, _Fract и _Accum, используются для декларирования переменных, имеющих тип с фиксированной точкой. Каждое из этих ключевых слова может быть также использовано совместно со спецификаторами типа short и long, а также signed и unsigned. Таким образом, имеется 12 доступных типов с плавающей точкой, хотя некоторые из них являются псевдонимами для типов, имеющих такой же размер и формат.
Путем подключения заголовочного файла stdfix.h можно использовать более удобный синтаксис - fract и accum вместо _Fract и _Accum. Заголовочный файл stdfix.h также предоставляет прототипы многих полезных функций, и весьма рекомендуется подключать его в файлы исходного кода, которые используют типы с фиксированной точкой. Таким образом вся дискуссия, которая пойдет дальше, будет использовать семантику с ключевыми словами fract и accum, как это делает остальная часть документации VisualDSP++.
Форматы типов с фиксированной точкой даны в таблице 1-15. В столбце "Представление" таблицы число после точки показывает количество бит, отведенное под дробную часть, в то время как число слева от точки означает количество бит целой части числа, включая бит знака, который обозначается буквой s. Типы со знаком (signed) используют форму представления чисел с дополнением до 2 (как обычные числа со знаком). Также в этой таблице представлен диапазон значений, которые могут принимать эти числа. Обратите внимание, что нижнее значение диапазона указано включительно (обозначено квадратной скобкой), в то время как верхнее значение диапазона указано исключительно (т. е. не включая, что обозначено круглой скобкой). Таким образом, для положительного верхнего предела значений может быть представлено значение только на вес 1 бита меньше, чем указанное.
Таблица 1-15. Форматы хранения, диапазоны и размеры типов Native Fixed-Point.
Тип
Представление
Диапазон
sizeof
short fract
s1.15
[-1.0,1.0)
2
fract
long fract
s1.31
4
unsigned short fract
0.16
[0.0,1.0)
2
unsigned fract
unsigned long fract
0.32
4
short accum
s9.31
[-256.0,256.0)
8
accum
long accum
unsigned short accum
8.32
[0.0,256.0)
unsigned accum
unsigned long accum
Стандарт (Technical Report 18037) также определяет ключевое слово _Sat для квалификатора типа с фиксированной точкой (альтернативное, более удобное представление sat). Это предусматривает, что вся арифметика над переменной fixed-point с таким квалификатором должна использовать арифметику с насыщением, т. е. в случае переполнения результата арифметических операций число останется в виде самого максимального (или самого минимального) значения, которое может представить тип. Когда квалификатор sat не используется, стандарт говорит, что арифметические операции могут допускать переполнение, которое будет вести себя неопределенным (с точки зрения стандарта) образом, т. е. будет зависеть от платформы, на которой работает код. VisualDSP++ принимает квалификатор sat для совместимости, но будет всегда генерировать код, который будет насыщать результат операции независимо от того, был использован квалификатор sat или нет. Это дает максимальную воспроизводимость результатов и позволяет писать код, не беспокоясь о неожиданных эффектах переполнения.
Традиционные константы с фиксированной точкой (Native Fixed-Point Constants). Константы с фиксированной точкой могут быть заданы в том же формате, что и константы с плавающей точкой, включая любую десятичную или бинарную экспоненту. Для дополнительной информации об этих форматах см. врезку с описанием функции strtofxfx.
Для идентификации типа констант используются суффиксы. Заголовочный файл stdfix.h также декларирует макросы для максимальных и минимальных значений типов с фиксированной точкой. См. таблицу 1-16 для подробного описания суффиксов и максимальных и минимальных значений соответствующих типов с фиксированной точкой.
Таблица 1-16. Суффиксы и макросы для констант типа с фиксированной точкой.
Тип
Суффикс
Пример
Min
Max
short fract
hr
0.5hr
SFRACT_MIN
SFRACT_MAX
fract
r
0.5r
FRACT_MIN
FRACT_MAX
long fract
lr
0.5lr
LFRACT_MIN
LFRACT_MAX
unsigned short fract
uhr
0.5uhr
0.0uhr
USFRACT_MAX
unsigned fract
ur
0.5ur
0.0ur
UFRACT_MAX
unsigned long fract
ulr
0.5ulr
0.0ulr
ULFRACT_MAX
short accum
hk
12.4hk
SACCUM_MIN
SACCUM_MAX
accum
k
12.4k
ACCUM_MIN
ACCUM_MAX
long accum
lk
12.4lk
LACCUM_MIN
LACCUM_MAX
unsigned short accum
uhk
12.4uhk
0.0uhk
USACCUM_MAX
unsigned accum
uk
12.4uk
0.0uk
UACCUM_MAX
unsigned long accum
ulk
12.4ulk
0.0ulk
ULACCUM_MAX
[Мотивирующий пример]
Рассмотрим очень простой пример операции скалярного умножения (dot-product) с фиксированной точкой. Как бы Вы написали подобное с использованием традиционных типов с фиксированной точкой? Алгоритм выполняет умножение каждой пары дробных значений во входных массивах. Тип accum разработан для хранения результатов накопления, что является именно тем, что нужно. Предположим, что данные состоят из векторов 16-битных значений в диапазоне [-1.0,1.0). Тогда естественно написать следующее:
#include < stdfix.h >
accum dot_product(fract *a, fract *b, int n)
{
accum sum =0.0k;
int i;
for (i =0; i < n; i++)
sum += a[i] * b[i];
return sum;
}
Этот показанный выше алгоритм выполняет попарное умножение элементов входных массивов и накапливает результат в переменной, которая получит насыщение при переполнении. Фактически это простое расширение алгоритма скрывает тонкость, связанную с семантикой арифметики, что обсуждается во врезке FX_CONTRACT, но оно показывает, как можно просто расширять алгоритмы, которые манипулируют данными с фиксированной точкой и выполняют насыщение результата при переполнении, без необходимости поиска специальных путей для расширения этих семантик через целочисленную арифметику.
[Семантика арифметики с фиксированной точкой]
Семантики арифметики с фиксированной точкой, работающие в соответствии с вышеупомянутым Technical Report, следующие:
1. Если двоичный оператор имеет один из операндов с плавающей запятой, то другой операнд преобразуется в тип с плавающей запятой, и производится операция над числами с плавающей точкой, давая результат с плавающей точкой.
2. Если в операторе имеется два операнда с фиксированной точкой, но с разным отношением к знаковости (т. е. один операнд signed, другой unsigned), то тогда операнд unsigned переводится в signed без изменения размерности (однако также см. FX_CONTRACT).
3. Выведение типа результата оператора. Результатом типа будет тип операнда с самым высоким рангом. Ранг увеличивается в следующем порядке: short fract, fract, long fract, short accum, accum, long accum (или их unsigned эквиваленты). Оператор, у которого только один операнд типа fixed-point, даст результат типа fixed-point (исключение составляет оператор сравнения, который даст результат boolean).
4. Математический результат, который получается от действия оператора над значениями операндов, преобразуется в тот тип результата, который определен на шаге 3. Другими словами, результат будет как если бы он был вычислен с бесконечной точностью перед тем, как преобразовать его в тип для конечного результата.
Преобразования между различными типами обсуждаются в секции "Преобразования типов данных и типы с фиксированной точкой".
[Преобразования типов данных и типы с фиксированной точкой]
Правила преобразования в тип с фиксированной точкой и из этого типа следующие:
1. Когда происходит преобразование в тип с фиксированной точкой, то если значение операнда может быть представлено типом с фиксированной точкой, то результат получит это значение. Если значение операнда выходит за диапазон типа с фиксированной точкой, то результат будет ближайшее значение операнда для этого типа с фиксированной точкой. Другими словами, преобразование к типу с фиксированной точкой приведет к насыщению математического значения операнда до диапазона значений типа с фиксированной точкой. Если значение операнда находится в диапазоне типа с фиксированной точкой, но не может быть представлено точно, то результат получит ближайшее значение либо к самому большому, либо самому малому значению операнда. Для дополнительной информации см. "Поведение округления".
2. Когда происходит преобразование в целый тип из числа с фиксированной точкой, то результатом будет целая часть от типа с фиксированной точкой. Дробная часть будет отброшена, как при округлении по направлению к 0; (int)(1.9k) даст 1, и (int)(-1.9k) даст -1.
3. Когда происходит преобразование типа с фиксированной точкой к типу с плавающей точкой, то результат получит ближайшее к значению операнда значение с плавающей точкой.
У этих правил есть важные последствия, которые Вы должны учитывать:
1. Преобразование целого числа в дробное полезно только если целое число равно -1, 0 или 1. Все другие целые значения получат насыщение к дробному типу. Так что оператор наподобие:
fract f =0x4000; // попытка присвоить 0.5 дробной переменной f
вовсе не присвоит 0.5 переменной f, но вместо этого f получит значение FRACT_MAX, потому что 0x4000 это целое число, большее 1. Вместо этого используйте:
fract f =0.5r;
или
fract f =0x4000p-15r;
Обратите внимание, что второй рекомендуемый пример использует синтаксис двоичной экспоненты, доступный для констант с фиксированной точкой; т. е. в данном примере значение 0x4000 масштабируется на 2-15.
2. Присваивание дробного значения целому приведет к нулю, кроме случая, когда дробное значение равно -1.0. Присваивание беззнакового дробного значения целому всегда даст в результате 0.
3. Будьте очень осторожны, чтобы случайно не перепутать типы fract16 и fract32 с fract и long fract. Типы fract16 и fract32 это не подлинные дробные типы, это просто typedef-ы от целочисленных типов данных. Так, например:
#include < stdfix.h >
#include < fract.h >
fract16 f16;
fract f;
voidfoo(void)
{
f16 =-0x4000; // сохранит -0.5 в переменной f16
f = f16; // в результате даст f = -1.0
}
По той причине, что f16 это целое число, произойдет насыщение при присвоении его к истинно дробному типу. Компилятор выдаст ошибку, когда он может детектировать, что fract16 или fract32 были сконвертированы в тип fract или long fract (или было обратное преобразование), потому что это почти всегда означает ошибку программирования. Для преобразования между целочисленными typedef-ами и традиционными типами используйте функции преобразования bitsfx и fxbitss.
Компилятор будет выдавать предупреждения, чтобы помочь диагностировать проблемы, где скорее всего преобразования дадут не ожидаемые результаты.
[Функции преобразования полей бит bitsfx и fxbitss]
Заголовочный файл stdfix.h предоставляет функции для преобразования полей бит в тип с фиксированной точкой и обратно. В частности, эти функции полезны для преобразования между традиционными дробными типами (fract, long fract) и целочисленными typedef-ами (fract16, fract32).
Для каждого типа с фиксированной точкой декларирован соответствующий целочисленный тип, который содержит достаточное количество бит, чтобы удержать в себе информацию типа с фиксированной точкой. Это типы int_fx_t, где fx заменяется на одно из сочетаний hr, r, lr, hk, k или lk, и uint_fx_t, где fx заменяется на одно из сочетаний uhr, ur, ulr, uhk, uk или ulk.
Чтобы преобразовать тип с точкой дробного числа в поле бит, используйте семейство функций bitsfx. fx может быть любым из сочетаний hr, r, lr, hk, k, lk, uhr, ur, ulr, uhk, uk или ulk. Например, с использованием прототипа
uint_ur_tbitsur(unsigned fract);
Вы можете написать:
#include < stdfix.h >
unsigned fract f;
uint_ur_t f_bit_pattern;
voidfoo(void)
{
f =0.5ur;
f_bit_pattern = bitsur(f); // дает 0x8000
}
Это хороший способ для преобразования fract к fract16 или long fract к fract32, где это необходимо. Например:
#include < stdfix.h >
#include < fract.h >
fract f;
fract16 f16;
voidfoo(void)
{
f =0.5r;
f16 = bitsr(f); // 0x4000, как и ожидалось
}
Набор функций семейства bitsfx предоставляет побитное преобразование типа с фиксированной точкой в целое число.
#include < stdfix.h >
int_r_tbitsr(fract f);
int_k_tbitsk(accum a);
int_hr_tbitshr(short fract f);
int_hk_tbitshk(short accum a);
int_lr_tbitslr(long fract f);
int_lk_tbitslk(long accum a);
uint_ur_tbitsur(unsigned fract f);
uint_uk_tbitsuk(unsigned accum a);
uint_uhr_tbitsuhr(unsignedshort fract f);
uint_uhk_tbitsuhk(unsignedshort accum a);
uint_ulr_tbitsulr(unsignedlong fract f);
uint_ulk_tbitsulk(unsignedlong accum a);
Указанный операнд в формате с фиксированной точкой функция преобразует в значение, умноженное на 2F, где F это количество бит дробной части типа с фиксированной точкой. Это эквивалентно передачи поля бит значения с фиксированной точкой в значение, которое хранится в целочисленном типе.
Подобным образом, чтобы преобразовать поле бит в тип с фиксированной точкой, используйте семейство функций fxbits. Так например, чтобы преобразовать из fract32 в long fract, используйте:
#include < stdfix.h >
#include < fract.h >
fract32 f32;
long fract lf;
voidfoo(void)
{
f32 =0x40000000; // это 0.5
lf = lrbits(f32); // получение 0.5lr, как и ожидалось
}
Набор функций семейства fxbits предоставляет побитное преобразование целого числа в тип числа с фиксированной точкой.
#include < stdfix.h >
fract rbits(int_r_t b);
accum kbits(int_k_t b);
short fract hrbits(int_hr_t b);
short accum hkbits(int_hk_t b);
long fract lrbits(int_lr_t b);
long accum lkbits(int_lk_t b);
unsignedshort fract uhrbits(uint_uhr_t b);
unsignedshort accum uhkbits(uint_uhk_t b);
unsigned fract urbits(uint_ur_t b);
unsigned accum ukbits(uint_uk_t b);
unsignedlong fract ulrbits(uint_ulr_t b);
unsignedlong accum ulkbits(uint_ulk_t b);
Указанный целочисленный операнд будет преобразован в целое число, поделенное на 2F, где F это количество бит, отведенное под дробную часть числа с фиксированной точкой. Это эквивалентно передаче значений поля бит целого числа, которое хранит тип числа с фиксированной точкой.
[Условия возникновения ошибки]
Семейство функций fxbits никогда не возвратит информацию о возникновении ошибки. Если входное целое число не помещается в количество бит результата с фиксированной точкой, то тогда результат получит насыщение к самому большому или самому малому числу в соответствующем представлении с фиксированной точкой.
[Пример]
#include < stdfix.h >
accum k;
unsignedlong fract ulr;
k = kbits(-0x640000000ll); /* k == -12.5k */
ulr = ulrbits(0x20000000); /* ulr == 0.125ulr */
См. также bitsfx.
[Арифметические операторы для типов с фиксированной точкой]
Вы можете использовать операторы +, -, * и / для типов с фиксированной точкой, и эти операторы будут иметь тот же эффект, как и их эквиваленты для целых чисел, кроме любой семантики переполнения или округления. Как уже обсуждалось выше, операции с фиксированной точкой, которые приводят к переполнению, получат насыщение к самому большому или самому малому представимому типом fixed-point значению. Округление обсуждается в разделе "Поведение округления".
Вы можете использовать операцию << для сдвига значения с фиксированной дочкой на положительное количество позиций бит, которое должно быть меньше, чем размер в битах типа с фиксированной точкой. Это даст тот же результат, что и умножение на число, являющееся степенью двойки, с подключением семантики переполнения:
#include < stdfix.h >
fract f1, f2;
voidfoo1(void)
{
f1 =0.125r;
f2 = f1 <<2; // дает 0.5r
}
voidfoo2(void)
{
f1 =-0.125r;
f2 = f1 <<10; // дает -1.0r
}
Также Вы можете использовать операцию >> для сдвига значения с фиксированной точкой вправо на указанное количество бит в том же диапазоне. Это даст тот же результат, что и деление на число, являющееся степенью двойки, с подключением семантики округления:
#include < stdfix.h >
fract f1, f2;
voidfoo1(void)
{
f1 =0.5r;
f2 = f1 >>2; // даст 0.125r
}
voidfoo2(void)
{
f1 =0x0003p-15r;
f2 = f1 >>2; // даст 0x0000p-15r, когда режим округления отбрасыванием,// и 0x0001p-15r, когда режим стандартного округления// со смещением (biased) или без смещения (unbiased)
}
Любые из этих операций могут быть использованы совместно с присваиванием, например:
#include < stdfix.h >
fract f1, f2;
voidfoo1(void)
{
f1 =0.2r;
f2 =0.3r;
f2 += f1;
}
Кроме того, есть некоторое количество унарных операторов, которые можно использовать с типами фиксированной точки. Вот они:
++ эквивалентно добавлению целого числа 1. -- эквивалентно вычитанию целого числа 1. + унарный плюс, эквивалентно добавлению 0.0 (операция не оказывает эффекта). - унарный минус, означает вычитанию значения из 0.0. ! 1, если значение равно 0.0, иначе 0.
[FX_CONTRACT]
В примере скалярного умножения (см. "Мотивирующий пример" выше) содержится операция накопления:
sum += a[i] * b[i];
где переменная sum была типа accum, и значения из массивов a[i], b[i] были типа fract. Учитывая ранее рассмотренные правила, какой будет результат умножения? Поскольку оба числа a[i] и b[i] типа fract, то результат умножения будет тоже типа fract - другими словами, два операнда s1.15 при умножении друг на друга дадут результат s1.15. Таким образом, правила говорят, что это должно быть эквивалентно следующему коду:
fract tmp = a[i] * b[i];
sum += tmp;
Однако это означает следующее:
• Результат умножения должен быть округлен до s1.15; будут потеряны 15 бит точности. • Результат умножения -1.0r на -1.0r должен быть FRACT_MAX, т. е. точно не 1.0.
С этим возникают две проблемы:
• Возможно, что Вы не хотели бы округлять дополнительные биты точности прежде, чем добавить результат умножения к sum. Выполнение преждевременного округления снизит точность накопления. Кроме того, процессор Blackfin имеет эффективную, одноцикловую инструкцию умножения с накоплением (multiply-accumulate, MAC), но она не отбрасывает дополнительные биты точности в результате умножения перед накоплением. • На процессорах Blackfin инструкция MAC не делает насыщение -1.0r * -1.0r перед добавлением к регистру аккумулятора. И это снова дает эффект повышения точности в результате накопления, но не соответствует семантике типа с фиксированной точкой для примера скалярного умножения.
Чтобы сгенерировать эффективный код без потери точности, Вы должны в действительности написать:
sum += (accum)a[i] * (accum)b[i];
Причина в том, что преобразование в более высокую точность типа accum перед умножением означает, что генерируемый код может удержать промежуточный результат умножения в формате s9.31, что означает отсутствие требований к насыщению результата или округления бит младшего порядка. Это также будет означать использование компилятором аппаратной инструкции MAC.
Для удобства, компилятор может сделать это за Вас, если будете использовать режим, известный как FX_CONTRACT. Имя FX_CONTRACT используется потому, что поведение подобно FP_CONTRACT в C99. Когда режим FX_CONTRACT включен, компилятор может удерживать промежуточные результаты в точности выше, чем это задает Technical Report. Другими словами, можно выбрать не округлять дополнительные биты точности или выполнять насыщение в промежуточном результате, если это нежелательно. Если выразиться точнее, компилятор удерживает промежуточный результат в повышенной точности при следующих обстоятельствах:
• Сохранение промежуточного результата с повышенной точностью будет более эффективным для нижележащего аппаратного обеспечения. • Промежуточный результат не сохраняется обратно в любую именованную переменную. • Нет явного преобразования типа для промежуточного результата.
Другими словами,
sum += a[i] * b[i];
приведет к использованию инструкции MAC. Но для этих двух примеров:
sum += (fract)(a[i] * b[i]);
или
fract tmp = a[i] * b[i];
sum += tmp;
будет происходить приведение результата умножения обратно к типу fract перед применением накопления.
Есть и другие примеры, где FX_CONTRACT может сохранять промежуточные результаты с повышенной точностью:
• Неявное преобразование типа unsigned fixed-point в больший по размеру signed fixed-point тип не будет приводить к первоначальному преобразованию в типа signed fixed-point меньшего размера. • Умножение signed fract и unsigned fract может создать дробное умножение смешанного типа, вместо того, чтобы сначала преобразовать unsigned fract в signed fract.
По умолчанию для компилятора разрешено поведение FX_CONTRACT. Режимом FX_CONTRACT можно управлять прагмой (см. описание #pragma FX_CONTRACT {ON|OFF} в [1]), или с опций помощью командной строки (см. описание опций -fx-contract и –no-fx-contract [3]). Прагма может использоваться в области действия файла исходного кода или внутри функции. Здесь применяются те же правила области действия, что и для прагмы FX_ROUNDING_MODE [1].
[Поведение округления]
Что произойдет, когда long fract будет преобразовано в fract? 16 младших значащих бит не могут быть представлены в результате, поэтому при преобразовании они должны быть отброшены. В случае, когда значение long fract не может быть точно представлено значением типа fract, есть 2 выбора: результат может быть представлен ближайшим fract значением, которое больше исходного значения long fract, либо ближайшим значением, которое меньше чем исходное значение long fract. Это известно под термином поведение округления (rounding behavior).
На некоторые операции с фиксированной точкой также может влиять округление. Например, умножение двух дробных значений для генерации дробного результата того же размера требует отбрасывания определенного количества младших бит от полного результата. Например, умножение s1.15 * s1.15 даст полный результат s2.30. Это приведет к насыщению до s1.30 и 15 младших значащих бит должны быть отброшены, чтобы дать результат s1.15.
По умолчанию любые биты должны быть отброшены усечением. Например:
#include < stdfix.h >
fract f1, f2, prod;
voidfoo(void)
{
f1 =0x3ffp-15r;
f2 =0x1000p-15r;
prod = f1 * f2; // даст 0x007fp-15r, с отбрасыванием// младших значащих бит это даст 0xe000
}
Это эквивалентно всегда округлению вниз до отрицательной бесконечности. Такая тенденция будет приводить к результатам, точность которых будет ухудшаться.
Если вычисления не дают требуемой точности, то Вы можете использовать смещенное (biased) или несмещенное округление к ближайшему значению. Компилятор поддерживает прагмы и опции командной строки для управления режимом округления. В режимах округления biased или unbiased результат будет округлен к ближайшему значению, которое можно представить типом результата, так что конечное значение будет 0x0080p-15r.
Разница между biased и unbiased округлением сказывается, когда значение для округления лежит точно на половине между двумя ближайшими значениями, которые может представить тип результата. В этом случае biased округление всегда округляет в сторону большего из этих двух значений, (с применением насыщения, если округление привело к переполнению), в то время как unbiased округление всегда округляет в сторону меньшего из значений, у которого младший значащий бит равен 0. Например:
#include < stdfix.h >
fract f;
long fract lf;
voidfoo1(void)
{
lf =0x34568000p-31lr;
f = lf; // даст 0x3456p-15r в режиме округления unbiased,// но 0x3457p-15r в режиме округления biased
}
voidfoo2(void)
{
lf =0x34578000p-31lr;
f = lf; // даст 0x3458p-15r в обоих режимах, biased// и unbiased
}
Обычно округление unbiased более дорогое в контексте трат тактов процессора, чем biased, но дает более точный результат, поскольку ошибки округления в половине случаев не имеют того же направления, и нее настолько сильно влияют на конечный результат.
Округление, обсуждаемое здесь, влияет только на операции, которые касаются результата с фиксированной точкой. Операции, касающиеся целочисленного результата, округляются в направлении 0. Есть также несколько исключений из этих правил округления:
• Преобразование значения с плавающей точкой в значение с фиксированной точкой приводит к округлению в сторону 0. • Функции roundfx, strtofxfx и fxdivi всегда выполняют либо biased, либо unbiased округление, в зависимости от текущего состояния бита RND_MOD. Они не поддерживают режим округления отбрасыванием.
Подробнее об установке режима округления рассказано в разделе ниже.
[Установка режима округления]
Как упоминалось в разделе "Поведение округления", имеется 3 режима округления, поддерживаемых арифметикой с фиксированной точкой:
• Обрезка (truncation), это режим округления по умолчанию. • Biased округление к ближайшему большему значению. • Unbiased округление к ближайшему меньшему значению.
Чтобы установить режим округления, Вы можете использовать прагму или опции компилятора.
Следующие опции командной строки компилятора управляют поведением округления [3]:
Указанный опцией режим округления станет режимом по умолчанию для всего исходного компилируемого кода.
Вы также можете использовать прагмы для более гибкого управления округлением. Вот они:
#pragma FX_ROUNDING_MODE TRUNCATION
#pragma FX_ROUNDING_MODE BIASED
#pragma FX_ROUNDING_MODE UNBIASED
Если одна из этих прагм применена на области действия файла, то её влияние будет распространено до конца юнита трансляции, или пока не встретится другая прагма на уровне файла, которая поменяет текущий режим округления.
Если одна из этих прагм была применена в пределах области действия составного оператора (т. е. блока кода, ограниченного фигурными скобками), то влияние прагмы будет распространяться до конца составного оператора. Режим округления будет возвращено к режиму, который действовал до входа в область действия составного оператора.
Пример того, как использовать прагмы для управления округлением, показан в листинге ниже.
#include < stdfix.h >
#pragma FX_ROUNDING_MODE BIASED
fract my_func(void)
{
// в этом месте режим округления biased
{
#pragma FX_ROUNDING_MODE UNBIASED// в этом месте режим округления unbiased
}
// в этом месте режим округления biased
}
#pragma FX_ROUNDING_MODE TRUNCATION
fract my_func2(void)
{
// в этом месте режим округления truncation
}
У процессора Blackfin есть специализированные инструкции для поддержки округления к ближайшему значению. Однако, какое именно будет использоваться округление - biased или unbiased - зависит от текущего состояния бита RND_MOD. Чтобы упростить генерацию эффективного кода, компилятор предполагает, что когда режим округления либо biased, либо unbiased, бит RND_MOD был установлен в тот же тип округления. Это означает, что компилятор может эффективно использовать аппаратную поддержку для этих режимов округления без необходимости установки или очистки этого бита каждый раз при использовании инструкции, зависящей от бита RND_MOD.
Таким образом, в Вашей ответственности гарантировать, что бит RND_MOD установлен корректно. Для упрощения этой задачи предоставляются встроенные функции:
Эти функции возвращают предыдущее состояние бита RND_MOD. Имеется другая встроенная функция (void restore_rnd_mod(int)), которая сбрасывает бит RND_MOD в сохраненное состояние. Например, если Вы напишете:
#include < stdfix.h >#include < builtins.h >
fract my_func(void)
{
#pragma FX_ROUNDING_MODE BIASEDint saved_rnd_mod = set_rnd_mod_biased();
// в этом месте режим округления biased
restore_rnd_mod(saved_rnd_mod);
// в этом месте будет тот же режим округления,// который был на входе в функцию
}
Если Вы используете прагмы для указания режима округления без установки бита RND_MOD, то можете получить смесь biased и unbiased поведения округления. Для дополнительной информации см. "#pragma FX_ROUNDING_MODE {TRUNCATION|BIASED|UNBIASED}" и "Changing the RND_MOD Bit" в руководстве [1].
[Арифметические библиотечные функции]
Заголовочный файл stdfix.h также декларирует некоторое количество функций, которые позволяют выполнять полезные арифметические операции на комбинациях типов чисел с фиксированной точкой и целых типов чисел. Это семейства функций divifx, idivfx, fxdivi, mulifx, absfx, roundfx, countlsfx и strtofxfx.
Функции divifx где fx заменяется на один из суффиксов r, lr, k, lk, ur, ulr, uk или ulk, позволяет выполнить деление целого числа на число с фиксированной точкой, чтобы получить в результате целое число.
Функция принимает целочисленное делимое numer и делитель denom в формате с фиксированной точкой, и функция вычислит частное и вернет ближайший целый результат.
У семейства функций divifx поведение не определено, когда делитель denom равен 0.
[Пример]
#include < stdfix.h >
int quo;
unsignedlongint ulquo;
quo = divik(125, -12.5k); /* quo == -10 */
ulquo = diviulr(125, 0.125ulr); /* ulquo == 1000 */
См. также fxdivi, idivfx.
[Почему следует использовать divifx]
Если Вы напишете:
#include < stdfix.h >
fract f;
int i, quo;
voidfoo(void)
{
// ПЛОХАЯ ИДЕЯ: деление int на fract даст в результате fract, а не int
f =0.5r;
i =2;
quo = i / f;
}
то результат деления будет типа fract целая часть которого будет сохранена в переменной quo. Это означает, что значение quo будет 0, так как деление приведет к переполнению, и выдаст дробный результат, который близок к 1.
Чтобы получить желаемый результат, напишите:
#include < stdfix.h >
fract f;
int i, quo;
voidfoo(void)
{
// ТАК БУДЕТ ПРАВИЛЬНО: если использовать divifx// для получения целого результата
f =0.5r;
i =2;
quo = divir(i, f);
}
Функции idivfx, где fx заменяется на один из суффиксов r, lr, k, lk, ur, ulr, uk или ulk, позволяет выполнять деление числа с фиксированной точкой на число с фиксированной точкой, чтобы получить целочисленный результат.
#include < stdfix.h >
intidivi(fract numer, fract denom);
intidivk(accum numer, accum denom);
longintidivlr(long fract numer, long fract denom);
longintidivlk(long accum numer, long accum denom);
int quo;voidfoo(void)
{
// НЕПРАВИЛЬНО: деление двух чисел fract даст в результате fract, не int
f1 =0.5r;
f2 =0.25r;
quo = f1 / f2;
}
то результат деления будет fract, целая часть которого будет сохранена в переменной quo. Это означает, что значение quo будет 0, поскольку произойдет переполнение при делении, и это даст дробный результат, близкий к 1.
Чтобы получить желаемый результат, напишите:
#include < stdfix.h >
fract f1, f2;
int quo;
voidfoo(void)
{
// ТАК ПРАВИЛЬНО: использование idivfx даст целочисленный результат
f1 =0.5r;
f2 =0.25r;
quo = idivr(f1, f2);
}
тогда результат будет 2, и он будет записан в переменную quo.
Функции fxdivi, где fx заменяется на один из префиксов r, lr, k, lk, ur, ulr, uk или ulk, позволяют выполнить деление целого числа на целое, и в результате получить число с фиксированной точкой.
#include < stdfix.h >
fract rdivi(int numer, int denom);
accum kdivi(int numer, int denom);
Функция принимает два целых числа, делит их друг на друга и возвратит тип с фиксированной точкой, по значению ближайший к результату деления.
У семейства функций fxdivi поведение не определено, когда делитель denom равен 0.
[Пример]
#include < stdfix.h >
accum quo;
unsignedlong fract ulquo;
quo = kdivi(125, -10); /* quo == -12.5k */
ulquo = ulrdivi(1, 8); /* ulquo == 0.125ulr */
См. также divifx, idivfx.
[Почему следует использовать fxdivi]
Если Вы напишете:
#include < stdfix.h >
int i1, i2;
fract quo;
voidfoo(void)
{
// НЕПРАВИЛЬНО: деление int на int даст в результате int, не fract
i1 =5;
i2 =10;
quo = i1 / i2;
}
то результат деления будет целым числом, которое будет преобразовано во fract и присвоено переменной quo. Это означает, что значение переменной quo будет 0, потому что результат деления будет округлен к целому 0, и затем этот 0 будет преобразован в формат fract.
Чтобы получить желаемый результат, напишите:
#include < stdfix.h >
int i1, i2;
fract quo;
voidfoo(void)
{
// ТАК ПРАВИЛЬНО: использование fxdivi даст результат fract
i1 =5;
i2 =10;
quo = rdivi(i1, i2);
}
полученное правильное значение 0.5 будет присвоено переменной quo.
Функции mulifx, где fx заменяется на один из суффиксов r, lr, k, lk, ur, ulr, uk или ulk, позволяют умножить целое число на число с фиксированной точкой, и получить в результате целое число.
#include < stdfix.h >
intmulir(int i, fract f);
intmulik(int i, accum a);
longintmulilr(longint i, long fract f);
longintmulilk(longint i, long accum a);
unsignedintmuliur(unsignedint i, unsigned fract f);
unsignedintmuliuk(unsignedint i, unsigned accum a);
Принимая целое число и число с в формате с фиксированной точкой, семейство функций mulifx вычисляет результат умножения и вернет ближайшее целое к полученному результату.
[Почему следует использовать mulifx]
Если Вы напишете:
#include < stdfix.h >
int i, prod;
fract f;
voidfoo(void)
{
// НЕПРАВИЛЬНО: умножение int на fract даст// результат типа fract, не int
i =50;
f =0.5r;
prod = i * f;
}
то результат умножения будет fract, целая часть которого сохраняется в переменной prod. Это означает, что значение prod будет 0, так как умножение приведет к переполнению, и приведет к дробному числу, близкому к 1.
Чтобы получить правильный результат, напишите:
#include < stdfix.h >
int i, prod;
fract f;
voidfoo(void)
{
// ПРАВИЛЬНО: используется mulifx для получения целого результата
i =50;
f =0.5r;
prod = mulir(i, f);
}
В этом варианте получится значение 25, которое будет присвоено переменной prod.
Функции absfx, где fx заменяется одним из суффиксов hr, r, lr, hk, k или lk, вычисляют абсолютное значение числа с плавающей точкой.
#include < stdfix.h >
fract absr(fract f);
accum absk(accum a);
short fract abshr(short fract f);
short accum abshk(short accum a);
long fract abslr(long fract f);
long accum abslk(long accum a);
Дополнительно к индивидуально именованным функциям Вы также можете использовать универсальный по типу макрос absfx(), где тип операнда может быть любым знаковым (signed) типом с фиксированной точкой. Этот макрос предназначен для работы в режиме C99. Макрос возвратит тот же тип, что и его операнд.
[Пример]
#include < stdfix.h >
accum a;
long fract f;
a = abshk(-12.5k); /* a == 12.5k */
f = abslr(0.75lr); /* f == 0.75lr */
#if defined(_C99)
a = absfx(-12.5k); /* a == 12.5k */
f = absfx(0.75lr); /* f == 0.75lr */
Функции roundfx, где fx заменяется одним из суффиксов hr, r, lr, hk, k, lk, uhr, ur, ulr, uhk, uk или ulk, принимает 2 аргумента. Первый это операнд с фиксированной точкой, тип которого соответствует имени вызываемой функции. Второй параметр дает количество бит дробной части. Первый операнд округляется до указанного вторым параметром количества бит дробной части. Второй операнд должен задавать значение между 0 и количеством бит дробной части в округляемом типе. Округление происходит к ближайшему значению. Однако каким именно будет округление - смещенным (biased) или не смещенным (unbiased) - зависит от состояния бита RND_MOD аппаратуры. Подробнее см. раздел "Поведение округления". Если округленный результат выходит за пределы значений типа результата, то результат получит насыщение к максимуму или минимуму значения для типа результата с фиксированной точкой.
#include < stdfix.h >
fract roundr(fract f, int n);
accum roundk(accum a, int n);
short fract roundhr(short fract f, int n);
short accum roundhk(short accum a, int n);
long fract roundlr(long fract f, int n);
long accum roundlk(long accum a, int n);
unsigned fract roundur(unsigned fract f, int n);
unsigned accum rounduk(unsigned accum a, int n);
unsignedshort fract rounduhr(unsignedshort fract f, int n);
unsignedshort accum rounduhk(unsignedshort accum a, int n);
unsignedlong fract roundulr(unsignedlong fract f, int n);
unsignedlong accum roundulk(unsignedlong accum a, int n);
Дополнительно Вы также можете использовать универсальный по отношению к типу макрос roundfx(), где тип первого операнда может быть любым из типов с фиксированной точкой. Этот макрос определен для использования в режиме C99. Макрос вернет тот же тип, что и тип операнда.
Функции countlsfx, где fx заменяется одним из суффиксов hr, r, lr, hk, k, lk, uhr, ur, ulr, uhk, uk или ulk, возвратит самое большое значение k, которое соответствует операнду, сдвинутому влево на k настолько, что не произошло переполнение. Для нулевого входного операнда результат будет количеством бит в типе операнда.
#include < stdfix.h >
intcountlsr(fract f);
intcountlsk(accum a);
intcountlshr(short fract f);
intcountlshk(short accum a);
intcountlslr(long fract f);
intcountlslk(long accum a);
intcountlsur(unsigned fract f);
intcountlsuk(unsigned accum a);
intcountlsuhr(unsignedshort fract f);
intcountlsuhk(unsignedshort accum a);
intcountlsulr(unsignedlong fract f);
intcountlsulk(unsignedlong accum a);
Дополнительно также можно использовать макрос countlsfx(), где тип операнда может быть любым типом с фиксированной точкой. Этот макрос используется в режиме C99.
Функции strtofxfx, где fx заменяется одним из суффиксов hr, r, lr, hk, k, lk, uhr, ur, ulr, uhk, uk или ulk, анализируют строковое представление числа с фиксированной точкой и возвращают число в формате с фиксированной точкой. Функции ведут себя подобно функции strtod, и принимают входную строку в том же формате.
Подразумевается, что nptr указывает на строку, которая представляет число как десятичное с плавающей точкой или шестнадцатеричное с плавающей точкой. Любой из форм числа может предшествовать последовательность из символов пробела (как это определено функцией isspace), которая будет игнорироваться функцией.
Принимается следующая строка числа в представлении с десятичной плавающей точкой:
[sign] [digits] [.digits] [{e|E} [sign] [digits]]
Поле sign является необязательным, и может быть либо плюсом (+), либо минусом (–). Поле digits может быть одной или большим количеством символов десятичных цифр. Последовательность цифр может содержать десятичную точку (.).
За десятичными цифрами может идти экспонента, которая состоит из вводной буквы (e или E) и опционального целого знакового (signed) числа. Если не появляется ни десятичная точка, ни часть экспоненты, то предполагается, что десятичная точка следует за последней цифрой строки.
Принимается следующая строка числа в представлении с шестнадцатеричной плавающей точкой:
Строка шестнадцатеричного числа с плавающей точкой может начинаться с опционального плюса (+) или минуса (–), за которым идет префикс обозначения шестнадцатеричного числа 0x или 0X. За этой последовательностью символов должен идти один или большее количество шестнадцатеричных символов, последовательность которых может содержать десятичную точку (.).
За шестнадцатеричными символами идет двоичная экспонента, которая состоит из буквы p или P, опционального знака и не пустой последовательности десятичных цифр. Эта экспонента интерпретируется как степень двойки, которая используется для масштабирования дробного числа, представленного полями [hexdigs] [.hexdigs].
Первый символ в последовательности текста, который не удовлетворяет представленным формам числа, остановит сканирование строки. Если endptr не NULL, то по завершении работы функции он укажет на символ, который останавливает сканирование, и место, на котором было остановлено сканирование, будет сохранено в этом указателе.
[Условия возникновения ошибки]
Функции strtofxfx вернут 0, если не было произведено преобразование, и в объекте, на который указывает endptr, будет сохранен указатель на недопустимую строку. Если корректное значение результата привело к переполнению, то будет возвращено максимальное положительное или отрицательное число для заданного значения с фиксированной точкой. Если корректное значение привело к недогрузке (underflow, т. е. число слишком мало, чтобы его можно было представить заданным форматом с фиксированной точкой), то будет возвращен 0. В случае переполнения в errno будет сохранено значение ERANGE.
[Пример]
#include < stdfix.h >
char*rem;
accum k;
unsignedlong fract ulr;
k = strtofxk ("-2345.5E-3 abc",&rem);
/* k = -2.3455k, rem = " abc" */
ulr = strtofxulr ("0x180p-12,123",&rem);
/* ulr = 0x1800p-16ulr, rem = ",123" */
См. также strtod, strtol, strtoul.
[Спецификаторы формата преобразования ввода/вывода]
Семейство функций printf и scanf поддерживают спецификаторы преобразования для типов с фиксированной точкой. Они приведены в таблице 1-17. Обратите внимание, что спецификаторы преобразования для signed-типов, %r и %k, задаются в нижнем регистре, в то время как для unsigned-типов применяются %R и %K в верхнем регистре.
Таблица 1-17. Спецификаторы формата преобразования ввода/вывода для типов с фиксированной точкой.
Тип
Спецификатор формата преобразования
short fract
%hr
fract
%r
long fract
%lr
unsigned short fract
%hR
unsigned fract
%R
unsigned long fract
%lR
short accum
%hk
accum
%k
long accum
%lk
unsigned short accum
%hK
unsigned accum
%K
unsigned long accum
%lK
При использовании с семейством функций scanf эти спецификаторы преобразования принимают тот же формат, который потребляют функции strtofxfx, который тот же самый какой применяется для %f. Для дополнительной информации см. врезку с описанием функций strtofxfx.
При использовании с семейством функций printf значения с фиксированной точкой печатаются следующим образом:
• По умолчанию как шестнадцатеричные значения, или когда используется опция командной строки компилятора -no-full-io. Например:
printf("fract: %r\n", 0.5r); // дробное число напечатается в виде 4000
• Как значение с плавающей точкой, когда использовалась опция командной строки компилятора -fixed-point-io или -full-io. Например:
printf("fract: %r\n", 0.5r); // дробное число напечатается в виде 0.500000
Принимаются опциональные спецификаторы точности, которые управляют количеством печатаемых десятичных цифр, и должна ли печататься завершающая десятичная точка. Однако они не дадут эффекта, за исключением использования опций командной строки -fixed-point-io или -full-io. Подробнее см. врезку с описанием функции fprintf.
Функция fprintf передает свой вывод печати в именованный выходной поток stream. Строка format (так называемая строка форматирования) задает, что будет выведено, и какие произойдут преобразования печатаемых аргументов.
Строка format содержать нулевое или большее количество спецификаторов формата преобразования, каждый из которых начинается символом процента (%). Сам спецификатор формата следует за символом %, и может содержать из одного или большего количества следующих полей:
• Flag – опциональные символы, которые модифицируют поведение преобразования параметра в текст. • Width – опциональное числовое значение (или *), которое задает минимальную ширину поля. • Precision – опциональное числовое значение, которое задает минимальное количество появляющихся цифр (задается точность преобразования). • Length – опциональный модификатор, который указывает размер аргумента. • Type – символ, который задает тип применяемого преобразования.
Символы поля flag могут идти в любом порядке, и их появление не обязательно. Допустимые флаги описаны в следующей таблице.
Флаг
Поле
-
Левое выравнивание результата в пределах поля (по умолчанию результат выравнивается вправо).
+
Всегда начинает преобразование со знаком, где знак плюс или минус. По умолчанию только отрицательные значения указываются со знаком минус.
пробел
Добавляется префикс из пробелов к результату, если если первый символ не знак минуса и также не был указан флаг +.
#
Результат преобразуется в альтернативную форму в зависимости от типа преобразования: o если значение не 0, то оно печатается с предшествием нулей. x если значение не 0, то оно печатается с префиксом 0x. X если значение не 0, то оно печатается с префиксом 0X. a, A, e, E, f, F всегда генерирует появление десятичной точки.
0 (ноль)
Задает альтернативу дополнению нулями. Начальные нули будут использоваться для необходимости дополнения поля до указаной ширины (спецификатором Width), лидирующие нули будут следовать за любым знаком или спецификатором базы счисления. Этот флаг игнорируется, если он появляется после флага '-', или если он используется в спецификации преобразования, которая использует точность и один из типов преобразования a, A, d, i, o, u, x или X. Флаг 0 может использоваться вместе с преобразованиями a, A, d, i, o, u, x, X, e, E, f, g и G.
Если задано поле width, то преобразованное значение дополняется пробелами до указанной ширины, если после преобразования количество символов оказалось меньше указанного значения. Обычно пробелы используются для дополнения поля слева, но будет использоваться дополнение справа, если указан флаг '-'. Флаг '0' может использоваться, чтобы заменить дополнение пробелами на дополнение символом 0 (см. описание флагов в таблице выше). Ширина width также может быть задана как '*', что показывает, что текущий аргумент в вызове fprintf это число int, которое задает значение width. Если значение отрицательное, то оно интерпретируется как флаг '-' и положительное значение поля width.
Опциональное значение precision (точность) начинается с точки (.), за которой идет либо звездочка (*), либо десятичное целое число. Звездочка (*) показывает, что точность задана целочисленным аргументом, предшествующим форматируемуму выводимому аргументу. Если указана только точка, то подразумевается нулевая точность. Значение precision дает разные эффекты, в зависимости от используемого спецификатора преобразования:
• Для A точность указывает количество цифр после десятичной точки. Если точность нулевая, и не указан флаг #, то не будет сгенерирована десятичная точка. • Для d, i, o, u, x, X задается минимальное количество появляющихся цифр, по умолчанию 1. • Для f, F, E, e, k, K, r, R задается количество цифр после десятичной точки, по умолчанию 6. Если спецификатор # присутствует с нулевой точностью, то не будет сгенерирована десятичная точка. • Для g, G задается максимальное количество значащих цифр. • Для s, указывается максимальное количество записываемых символов.
Модификатор length может опционально использоваться для указания размерности аргумента. Модификаторы длины должны предшествовать только спецификаторам преобразования d, i, o, u, x, X, k, K, r, R или n, если не детализованы другие спецификаторы преобразования.
Length
Действие
h
Аргумент должен быть представлен типом short int, short fract или short accum.
hh
Аргумент должен интерпретироваться как тип char.
j
Аргумент должен быть представлен типом intmax_t или uintmax_t.
l
Аргумент должен быть представлен типом long int, long fract или long accum.
ll
Аргумент должен интерпретироваться как тип long long int.
L
Аргумент должен интерпретироваться как long double. Этот модификатор длины должен предшествовать спецификаторам преобразования a, A, e, E, f, F, g или G.
t
Аргумент должен интерпретироваться как ptrdiff_t.
z
Аргумент должен интерпретироваться как size_t.
Обратите внимание, что спецификаторы размера hh, j, t и z, как это описано в стандарте C99 (ISO/IEC 9899:1999), доступны толкьо в случае выбора опции компилятора -full-io.
Следующая таблица содержит определения допустимых спецификаторов преобразования, которые задают один из применияемых типов преобразования:
Спецификатор
Преобразование
a, A
Число с плавающей точкой.
c
Символ.
d, i
Десятичное целое со знаком.
e, E
Научная нотация (мантисса/экспонента).
f, F
Десятичное число плавающей точкой.
g, G
Преобразование работает как e, E или f, F.
k
Знаковый тип accum.
K
Тип accum без знака.
n
Указатель на число со знаком, к которому число символов, записанное до сих пор, будет сохранено без другого вывода.
o
Беззнаковое восьмеричное.
p
Указатель на void.
r
Дробное (fract) со знаком.
R
Дробное (fract) без знака.
s
Строка символов.
u
Целое число без знака.
x, X
Число без знака в шестнадцатеричной нотации.
%
Позволяет вывести на печать символ % без интерпретации его как начала спецификатора преобразования.
Спецификатор преобразования a|A преобразует число с плавающей точкой стиля [-]0xh.hhhhp±d, где до точки стоит одна из шестнадцатеричных цифр. Спецификатор преобразования a|A всегда содержит минимум одну цифру для экспоненты.
Спецификатор преобразования e|E преобразует число с плавающей точкой стиля [-]d.ddde±dd. Экспонента всегда содержит как минимум две цифры. Случай, когда e педшествует экспоненте, соответствует спецификатору преобразования.
Спецификатор преобразования f|F конвертирует десятичную строку вида [-]d.ddd.
Спецификатор преобразования g|G конвертирует как спецификаторы e|E или f|F, в зависимости от преобразуемого значения. Если экспонента значения преобразована меньше чем -4, или больше или равна precision, то используются преобразования e|E, иначе f|F.
Все спецификаторы преобразования a, A, e, E, f, F, g и G, когда аргумент представляет бесконечность, выведут inf или INF в том же регистре, в каком был задан спецификатор. Для всех спецификаторов a, A, e, E, f, F, g и G аргумент, который представляет результат NaN, выводится как nan или NAN в том же регистре, в каком был задан спецификатор.
Спецификаторы преобразования k|K and r|R преобразуют значение с фиксированной точкой в десятичную нотацию [-]d.ddd, когда Ваше приложение собрано с опцией командной строки компилятора -full-io или -fixed-point-io. Иначе спецификаторы k|K и r|R будут преобразованивать значение с фиксированной точкой как шестнадцатеричное.
Функция fprintf возвращает количество напечатанных в поток символов.
[Условия возникновения ошибки]
Если функция fprintf завершилась неудачно, то она вернет отрицательное значение.
См. также printf, snprintf, vfprintf, vprintf, vsnprintf, vsprintf.
[Портирование кода, написанного с использованием fract16 и fract32]
Если у Вас есть код, использующий типы fract16 и fract32 вместе со встроенными функциями и вызовами библиотечных функций, то возможно Вы захотите переписать свой код для использования новых традиционных типов с фиксированной точкой (native fixed-point). В этом разделе содержится несколько советов, как это сделать проще всего.
Поскольку fract являются 16-разрядным типом, и long fract является 32-разрядным типом, то базовая стратегия состоит в замене использования переменных fract16 на fract и переменных fract32 на long fract.
Во-первых, код написанный с использованием fract16 и fract32 часто содержит константы. Если они написаны с использованием суффиксов r16 и r32, то Вы можете просто поменять суффикс для создания типа native fixed-point.
Например:
fract16 f1 =0.5r16;
fract32 f2 =0.75r32;
станет
fract f1 =0.5r;long fract f2 =0.75lr;
Если Ваш код содержит шестнадцатеричные константы, то удобно использовать синтаксис двоичной экспоненты для преобразования констант:
fract16 f1=0x1234;
fract32 f2 =0x12345678;
станет
fract f1 =0x1234p-15r;long fract f2 =0x12345678p-31lr;
Множество встроенных функций (built-ins) больше не нужны, поскольку Вы перешли к использованию традиционных типов с фиксированной точкой – вместо этого можно использовать обычную арифметику. Соответствие между встроенными функциями для fract16 и fract32 и традиционными арифметическими действиями показано в таблице 1-18.
Таблица 1-18. Соответствие между встроенными функциями fract16 и строенными функциями fract32 и обычной арифметикой чисел с фиксированной точкой (Native Fixed-Point Arithmetic).
Встроенные функции fract16 или fract32
Традиционная арифметика с фиксированной точкой
fract16 f1, f2; fract16 f3 = add_fr1x16(f1, f2);
fract f1, f2; fract f3 = f1 + f2;
fract16 f1, f2; fract16 f3 = sub_fr1x16(f1, f2);
fract f1, f2; fract f3 = f1 - f2;
fract16 f1, f2; fract16 f3 = mult_fr1x16(f1, f2);
fract f1, f2; fract f3 = f1 * f2; // в режиме округления // с отбрасыванием
fract f1; long fract f2; float f3; f2 = f1; f1 = f2; f3 = f1; f3 = f2; f1 = f3; f2 = f3;
Для удобства встроенные функции также предоставлены для того же функционала, что дают традиционные типы с фиксированной точкой, и нужно просто заменить в имени встроенной функции "fr" на "fx".
Имеется некоторое количество встроенных функций (built-ins), которые не могут напрямую отобразиться на арифметику с фиксированной точкой, но аналогичная функциональность все же доступна (подробнее см. таблицу 1-19). Эти встроенные функции выполняют дробное умножение 1.31 с округлением результата. Однако результат может не быть двоично идентичным результату, который был получен умножением традиционного long fract, даже в режиме округления к ближайшему значению, поскольку округление, выполняемое традиционными типами более точны, чем округление, предоставленное встроенными функциями. Рекомендуется использовать традиционную арифметику с фиксированной точкой, за исключением случаев, когда нужно побитно точно воспроизвести результаты Вашей предыдущей реализации. В таком случае Вы можете использовать побитно точный эквивалент встроенным функциям: mult_fx1x32x32, mult_fx1x32x32NS и multr_fx1x32x32.
Таблица 1-19. Встроенные функции fract16 и fract32 и традиционная арифметика с фиксированной точкой, имеющие подобную семантику.
long fract f1, f2; long fract f3 = f1* f2; // режим округления // biased/unbiased
Имеется множество библиотечных функций, которые используют типы fract16 и fract32. Как общее правило, Вы можете просто заменить "fr" на "fx", чтобы получить библиотечную функцию, которая примет вместо типов fract16 и fract32 и/или вместо типов fract16 и fract32 возвратит традиционные типы с фиксированной точкой. Однако нет аналогичной версии с фиксированной точкой для типа вектора fract2x16 или для комплексных дробных типов complex_fract16 и complex_fract32, поэтому специальные усилия должны быть предприняты, когда используется смесь традиционных типов с фиксированной точкой и векторных или комплексных дробных типов. Типы fract2x16, complex_fract16 и complex_fract32 могут быть использованы с традиционными типами, пока правильно осуществляется доступ к данным членов класса в функциях конструктора и аксессора, приведенных в таблице 1-20.
Таблица 1-20. Функции Constructor и Accessor для использования с традиционными типами фиксированной точки (Native Fixed-Point) и с комплексными и векторными дробными типами (Complex and Vector Fractional).
Создает значение типа complex_fract16 из типов fract вещественной и дробной части.
fract real_fx_fr16 (complex_fract16 c);
Распаковывает вещественную часть типа fract из комплексного значения типа complex_fract16.
fract imag_fx_fr16 (complex_fract16 c);
Распаковывает мнимую часть типа fract из комплексного значения типа complex_fract16.
complex_fract32 ccompose_fx_fr32(long fract real, long fract imag);
Создает значение complex_fract32 из типов long fract вещественной и реальной части комплексного числа.
long fract real_fx_fr32 (complex_fract32 c);
Распаковывает вещественную часть числа типа long fract из комплексного значения типа complex_fract32.
long fract imag_fx_fr32 (complex_fract32 c);
Распаковывает мнимую часть числа типа long fract из комплексного значения типа complex_fract32.
fract2x16 compose_fx_fr2x16 (fract x, fract y);
Создает значение типа fract2x16 из двух величин типа fract.
fract low_of_fx_fr2x16 (fract2x16 vec);
Распаковывает младшую часть типа fract из значения типа fract2x16.
fract high_of_fx_fx2x16 (fract2x16 vec);
Распаковывает старшую часть типа fract из значения типа fract2x16.
Система именования библиотечных функций, которые берут смесь типов фиксированной точки и типов fract2x16, complex_fract16 или complex_fract32, добавляют префикс "fx_" перед "fr2x16", "fr16", или "fr32" в имени функции. Вы можете проверить имя, когда консультируетесь со страницей документации для библиотечной функции. Обратите внимание, что не требуется изменять имена функций, которые не используют типы fract16 или fract32.
В этой врезке рассматривается пример программы для вычисления вариантности массива 16-битных дробных чисел.
Вариантность (variance) массива значений samples[] вычисляется по формуле:
где n количество выборок в массиве.
Как это перекладывается на типы с фиксированной точкой? Выборки в массиве samples составляют дробные (fract) значения, так что для вычисления суммы всех значений выборок нужен тип с размерностью, который больше чем этот дробный тип. Если имеется меньше 256 выборок, то определенно сумма уложится в тип accum без появления насыщения. Тот же аргумент прикладывается к сумме квадратов выборок.
Однако выше приведенная формула также нуждается в вычислении промежуточного результата sample_length * sum(samples[i] * samples[i]). Умножение на sample_length означает, что не обязательно результат умножения уложится в диапазон типа accum.
Эквивалентная формула для вариантности будет следующая:
Это альтернативное определение формулы означает, что промежуточные результаты можно вычислить с помощью типа accum. Возможная реализация вычислений вариантности массива 16-битных дробных чисел показана в листинге ниже.
#include < stdfix.h >
#include < builtins.h >
// FX_CONTRACT ON гарантирует, что компилятор распознает
// идиомы accum += fract * fract
#pragma FX_CONTRACT ON
fract fract_variance(const fract *samples, int sample_length)
{
fract variance =0.0r;
if (sample_length >1)
{
#pragma FX_ROUNDING_MODE UNBIASEDint i, saved_rnd_mod = set_rnd_mod_unbiased();
accum diff, sum_of_samples =0.0k, sum_of_squares =0.0k;
long fract mean;
// это гарантирует, что не будет насыщения, пока// sample_length < = 255for (i =0; i < sample_length; i++)
{
sum_of_samples += samples[i];
sum_of_squares += samples[i] * samples[i];
}
mean = sum_of_samples / sample_length;
diff = sum_of_squares - (mean * sum_of_samples);
variance = diff / (sample_length -1);
restore_rnd_mod(saved_rnd_mod);
}
return variance;
}
Сначала подключается заголовочный файл stdfix.h, чтобы можно было использовать обычный синтаксис fract и accum. Следующее, что Вы можете заметить, это явное использование #pragma FX_CONTRACT ON. Поскольку это поведение по умолчанию для режима FX_CONTRACT, то здесь не строго обязательно использовать такую прагму, но это полезно для того, чтобы четко представлять себе поведение кода в программе.
Функция вычисляет вариантность только если в массиве имеется больше одной выборки, иначе функция просто вернет 0.
Затем функция устанавливает режим округления. Здесь используется unbiased округление, чтобы достичь наибольшей точности результата. Это достигается совместным использованием прагмы FX_ROUNDING_MODE UNBIASED и встроенной функции set_rnd_mod_unbiased, как это обсуждалось ранее в разделе "Установка режима округления".
Цикл вычисляет сумму выборок и сумму квадратов. Поскольку режим FX_CONTRACT включен (ON), то точность не теряется, так как дробные числа перемножаются друг с другом и суммируются в типе accum.
После цикла сумма выборок делится на sample_length, чтобы получить среднее значение. Оно должно быть в диапазоне [-1.0,1.0). Значение сохраняется в переменную типа long fract, чтобы максимально сохранить точность.
Далее функция вычисляет разность между суммой квадратов и результатом среднего значения суммы выборок массива samples. Поскольку абсолютное значение среднего меньше или равно 1, то этот результат умещается в типе accum, и поскольку этот результат и сумма квадратов обе не отрицательные, то разность должна также уместиться в accum.
И наконец, вариантность вычисляется делением этой разницы на число, на единицу меньшее sample_length. Теоретически это значение может быть больше единицы; в нашем случае возвращаемое значение тогда получит насыщение, так что получится FRACT_MAX.