Skip to content

Latest commit

 

History

History

math

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

Целочисленная и вещественная арифметика

Целочисленные типы данных

Минимально адресуемым размером данных является, какправило, один байт (8 бит). Как правило - это значит, что не всегда, и бывают разные экзотические архитектуры, где "байт" - это 9 бит (PDP-10), или специализированные сигнальные процессоры с минимально адресуемым размером данных 16 бит (TMS32F28xx).

По стандарту языка Си определена константа CHAR_BIT (в заголовочном файле <limits.h>), для которой гарантируется, что CHAR_BIT >= 8.

Тип данных, представляющий один байт, исторически называется "символ" - char, который содержит ровно CHAR_BIT количество бит.

Знаковость типа char по стандарту не определена. Для архитектуры x86 это знаковый тип данных, а, например, для ARM - беззнаковый. Опции компилятора gcc -fsigned-char и -funsigned-char определяют это поведение.

Для остальных целочисленных типов данных: short, int, long, long long, стандарт языка Си определяет минимальную разрядность:

Тип данных Разрядность
short не менее 16 бит
int не менее 16 бит, обычно 32 бит
long не менее 32 бит
long long не менее 64 бит, обычно 64 бит

Таким образом, полагаться на количество разрядов в базовых типах данных нельзя, и это нужно проверять с помощью оператора sizeof, который возвращает "количество байт", то есть, в большинстве случает - сколько блоков размером CHAR_BIT помещается в типе данных.

С особой осторожностью нужно относиться к типу данных long: на 64-разрядной системе Unix он является 64-битным, а, например, на 64-битной Windows - 32-битным. Поэтому, во избежание путаницы, использовать этот тип данных запрещено.

Знаковые и беззнаковые типы данных

Перед целочисленными типами данных могут стоять модификаторы unsigned или signed, которые указывают допустимость отрицательных чисел.

Для знаковых типов, старший бит определяет знак числа: значение 1 является признаком отрицательности.

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

Таким образом, значение -1 представляется как целое число, все биты которого равны единице.

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

Типы данных с фиксированным количеством бит

В заколовочных файлах файле <stdint.h> (для Си99+) и <cstdint> (для C++11 и новее) определены типы данных, для которых гарантируется фиксированное количесвто разрядов: int8_t, int16_t, int32_t, int64_t, - для знаковых, и uint8_t, uint16_t, uint32_t, uint64_t - для беззнаковых.

Переполнение

Ситуация целочисленного переполнения возникает, когда тип данных результата не имеет достаточно разрядов для того, чтобы хранить итоговый результат. Например, при сложении беззнаковых 8-разрядных целых чисел 255 и 1, получается результат, который не может быть представим 8-разрядным значением.

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

Для знаковых типов данных - приводит к ситуации неопределенного поведения (Undefined Behaviour). В корректных программах такие ситуации встречаться не могут.

Пример:

int some_func(int x) {
    return x+1 > x;
}

С точки зрения здравого смысла, такая программа должна всегда возвращать значение 1 (или true), поскольку мы знаем, что x+1 всегда больше, чем x. Компилятор может использовать этот факт для оптимизации кода, и всегда возвращать истинное значение. Таким образом, поведение программы зависит от того, какие опции оптимизации были использованы.

Контроль неопределенного поведения

Свежие версии компиляторов clang и gcc (начиная с 6-й версии) умеют контролировать ситуации неопределенного поведения.

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

Такие инструменты называются ревизорами (sanitizers), предназначенными для разных целей.

Для включения ревизора, контролирующего ситуацию неопределенного поведения, используется опция -fsanitize=undefined.

Контроль переполнения, независимо от знаковости

Целочисленное переполнение означает перенос старшего разряда, и многие процессоры, включая семейство x86, позволяют это диагностировать. Стандартами языков Си и C++ эта возможность не предусмотрена, однако компилятор gcc (начиная с 5-й версии) предоставляет нестандартные встроенные функции для выполнения операций с контролем переполнения.

// Операция сложения
bool __builtin_sadd_overflow (int a, int b, int *res);
bool __builtin_saddll_overflow (long long int a, long long int b, long long int *res);
bool __builtin_uadd_overflow (unsigned int a, unsigned int b, unsigned int *res);
bool __builtin_uaddl_overflow (unsigned long int a, unsigned long int b, unsigned long int *res);
bool __builtin_uaddll_overflow (unsigned long long int a, unsigned long long int b, unsigned long long int *res);

// Операция вычитания
bool __builtin_ssub_overflow (int a, int b, int *res)
bool __builtin_ssubl_overflow (long int a, long int b, long int *res)
bool __builtin_ssubll_overflow (long long int a, long long int b, long long int *res)
bool __builtin_usub_overflow (unsigned int a, unsigned int b, unsigned int *res)
bool __builtin_usubl_overflow (unsigned long int a, unsigned long int b, unsigned long int *res)
bool __builtin_usubll_overflow (unsigned long long int a, unsigned long long int b, unsigned long long int *res)

// Операция умножения
bool __builtin_smul_overflow (int a, int b, int *res)
bool __builtin_smull_overflow (long int a, long int b, long int *res)
bool __builtin_smulll_overflow (long long int a, long long int b, long long int *res)
bool __builtin_umul_overflow (unsigned int a, unsigned int b, unsigned int *res)
bool __builtin_umull_overflow (unsigned long int a, unsigned long int b, unsigned long int *res)
bool __builtin_umulll_overflow (unsigned long long int a, unsigned long long int b, unsigned long long int *res)

Числа с плавающей точкой в формате IEE754

Два основных типа вещественных с плавающей точкой, которые определены стандартом языка Си, - это float (используется 4 байта для хранения) и double (используется 8 байт).

Самый старший бит в представлении числа - это признак отрицательного значения. Далее, по старшинству бит, хранится значения смещенной экспоненциальной части (8 бит для float или 11 бит для double), а затем - значение мантиссы (23 или 52 бит).

Смещение экспоненциальной части необходимо для того, чтобы можно было в таком представлении хранить значения с отрицательной экспонентой. Смещение для типа float равно 127, для типа double - 1023.

Таким образом, итоговое значение может быть получено как:

Value = (-1)^S * 2^(E-B) * ( 1 + M / (2^M_bits - 1) )

где S - бит знака, E - значение смещенной экспоненты, B - смещение (127 или 1023), а M - значение мантиссы, M_bits - количество бит в экспоненте.

Как получить отдельные биты вещественного числа

Поразрядные операции относятся к целочисленной арифметике, и не предусмотрены для типов float и double. Таким образом, нужно сохранить вещественное число в памяти, и затем прочитать его, интерпретируя как целое число. В случае с языком C++ для этого предназначен оператор reinterpret_cast. Для языка Си есть два способа: использовать аналог reinterpret_cast - приведение указателей, либо использовать тип union.

Приведение указателей

// У нас есть некоторое целое вещественное число, которое хранится в памяти
double a = 3.14159;

// Получаем указатель на это число
double* a_ptr_as_double = &a;

// Теряем информацию о типе, приведением его к типу void*
void* a_ptr_as_void = a_ptr_as_void;

// Указатель void* в языке Си можно присваивать любому указателю
uint64_t* a_ptr_as_uint = a_ptr_as_void;

// Ну а дальше просто разыменовываем указатель
uint64_t b = *a_as_uint;

Использование типа union

Тип union - это тип данных, который синтаксически очень похож на тип struct, то есть там можно перечислить несколько именованных полей, но концептуально - это совершенно разные типы данных! Если в структуре или классе, для хранения каждого поля для предусмотрено отдельное место в памяти, то для union этого не происходит, и все поля накладываются друг на друга при размещении в памяти.

Обычно тип union используется в качестве вариантного типа данных (в С++ начиная с 17-го стандарта для этого предусмотрен std::variant), но в качестве побочного эффекта - его удобно использовать приведения типов в стиле reinterpret_cast, не используя при этом указатели.

// У нас есть некоторое целое вещественное число, которое хранится в памяти
double a = 3.14159;

// Используем тип union
typedef union {
    double     real_value;
    uint64_t   uint_value;
} real_or_uint;

real_or_uint u;
u.real_value = a;
uint64_t b = u.uint_value;

Специальные значения в формате IEEE754

  • Бесконечность: E=0xFF...FF, M=0
  • Минус ноль (результат деления 1 на минус бесконечность): S=1, E=0, M=0
  • NaN (Not-a-Number): S - любое, E=0xFF...FF, M <> 0

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

Признаком денормализованного числа является значение смещенной экспоненты E=0. В этом случае, численное значение получается следующим образом:

Value = (-1)^S * ( M / (2^M_bits - 1) )

Значения Not-a-Number

Некоторые процессоры, например Intel x86, различают два вида чисел NaN - невалидное значение.

sNaN - Signaling NaN

Значения sNaN возникают при выполнении операций, которые сигнализируют об ошибке на уровне прерывания процессора. Например, деление на 0. Обычно, чтобы получить такие значения, необходимо собирать программу с опцией -fno-signaling-nans. Более подробно - см. FloatingPointMath - GCC Wiki

Пример битовой маски для типа double на x86_64, определяющей значение sNaN:

Номера битов    6         5         4         3         2         1         0
             3210987654321098765432109876543210987654321098765432109876543210
             ----------------------------------------------------------------
Значения     0111111111110100000000000000000000000000000000000000000000000000
             ----------------------------------------------------------------
Регион       SEEEEEEEEEEEMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM

qNaN - Quiet NaN

В отличии от sNaN, значения Quiet NaN, которые получаются в результате вычислений, не приводят к прерыванию процессора и вызове обработчика исплючительной ситуации.

Примером является попытка сложить +inf и -inf.

Пример битовой маски для типа double на x86_64, определяющей значение qNaN:

Номера битов    6         5         4         3         2         1         0
             3210987654321098765432109876543210987654321098765432109876543210
             ----------------------------------------------------------------
Значения     0111111111111000000000000000000000000000000000000000000000000000
             ----------------------------------------------------------------
Регион       SEEEEEEEEEEEMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM