Ориентация главы

Том 2 начинается с математики. Семейство xrtMath закрывает «инженерные прорехи» стандартной C-библиотеки: обобщённые операции отношений вроде Min/Max/Sign, трёхчастную классификацию NaN/Inf/конечное, численно стабильные версии вроде Log1p/Expm1 и повсеместно употребимое суждение «близко» для плавающей точки xrtMathNear. Помимо прохода по семейству функций глава досконально разбирает две самые частые инженерские ловушки: почему плавающую точку нельзя сравнивать через == и семантику остатка на отрицательных — это фундамент всех последующих глав о числах, сэмплировании и сравнении.

10.1引入

Три фрагмента кода — три воспоминания. Первый: ассерт 0.1 + 0.2 == 0.3 падает — погрешность представления плавающей точки общеизвестна, но каждый проект наступает на неё заново. Второй: в графическом коде fmod(-7, 3) возвращает -1 вместо ожидаемого 2 — остаток в C следует знаку делимого (усечённая семантика), в отличие от Python и других (семантика округления вниз); межъязыковые команды писали здесь настоящие ошибки направления. Третий: желая судить «совпадают ли две точки» на координатах порядка 1e9, с абсолютным допуском 1e-9 никогда не совпасть, а относительный допуск у нуля полностью отказывает — единый допуск неугоден обоим концам.

Три воспоминания — три стержневых дизайна xrtMath: комбинированный допуск Near, явная усечённая семантика Mod (записана в документации и пришпилена тестами — не угадывается) и трёхчастная полнота семейства классификации (NaN/Inf/конечное взаимоисключающи и полны). Математическая библиотека мала, но каждая деталь отвечает реальной инженерной ране.

Одна легко упускаемая ценность: эти функции всплывут в каждом следующем томе. Сравнение с допуском — база утверждений проверки распределений главы 11 и бенчмарков тома 12; трёхчастная классификация — первый шлюз разбора данных (том 4), вычищающий нелегальные плавающие числа; Log2/Exp2 — математический фундамент роста ёмкости контейнеров и индексации битовых карт. Освоив Near и семейство классификации, вы будете читать любое числовое утверждение без второго прохода.

10.2概念

Карта семейств функций

СемействоФункцииОдной фразой
ОтношенияxrtMathMin / Max / Sign / Trunc / Mod / RadМинимум-максимум, трёхзначный знак, усечение, усечённый остаток, градусы в радианы
КлассификацияxrtMathIsNaN / IsInf / IsFiniteNaN / бесконечность / конечное — три пути, взаимоисключающие и полные
Экспоненты-логарифмыxrtMathLog2 / Exp2 / Log1p / Expm1Логарифм и экспонента по базе 2; Log1p/Expm1 численно стабильны у нуля
КорниxrtMathCbrtКубический корень, корректен на отрицательных (pow(x, 1/3) там ловушка)
БлизостьxrtMathNear / xrtMathIntNearСравнение плавающих с комбинированным допуском; целочисленное суждение «близко»

Sign возвращает три значения -1/0/1 (не булево) — он служит сортировке и суждениям о направлении, где ноль имеет собственную семантику. Log1p(x) вычисляет ln(1+x): при крошечном x прямое вычисление log(1+x) округляет 1+x в 1 и даёт нуль — Log1p специально обходит эту ловушку точности; Expm1 — обратная операция. Парочка — штатное оснащение процентных ставок, вероятностей и инкрементальной статистики.

Rad переводит градусы в радианы — тригонометрия ест радианы, а физика и конфигурация несут градусы, и именно это одно преобразование легче всего перепутать (умножить на π/180 или делить). Trunc усекает к нулю (2.7 → 2, -2.7 → -2); заметьте отличие от «округления вниз» (-2.7 → -3) — когда нужна семантика вниз, комбинируйте сами; не делайте из Trunc универсальное округление.

Сравнение плавающей точки: комбинированный допуск

Суждение xrtMathNear(a, b, abs, rel) — это |a-b| ≤ max(abs, rel×|a|): абсолютный и относительный допуски, действует больший:

1Около нуляотносительный допуск отказывает (0 и 1e-10 различаются в десять раз, но оба должны считаться нулём) -> передавайте абсолютный
2Большие значенияабсолютный допуск отказывает (разница 0.05 при 1e9 по точности бессмысленна) -> передавайте относительный
3Комбинацияпередавайте оба, Near берёт max — обе группы сценариев прикрыты каждая своим
4ЦелыеIntNear с единым допуском (у целых нет погрешности представления)

Практическая мерка выбора: абсолютный допуск = порядок величины, при котором вы считаете «физически неотличимо» (координаты, 1e-9 метра); относительный допуск = приемлемая доля округления (тысячная — передавайте 0.001). Передавать оба — всегда самая надёжная запись.

Один сводный пример: выравнивание координат

Сшивая инструменты главы в одно реальное суждение: «координаты двух лазерных замеров указывают в ту же точку?» Пусть порядок величин метровый, точность прибора 0.01 миллиметра, между замерами 3 миллисекунды расхождения часов. Верное суждение — три шага: сперва IsFinite отфильтровывает NaN/Inf от неудавшихся замеров (классификация вперёд — иначе все последующие сравнения бессмысленны); разность координат — Near(dx, 0, 1e-5, 1e-9) у нуля и Near(dx, x, 0, 1e-6) при больших значениях — комбинированный допуск открыт с обеих сторон; разность времени — IntNear(t1, t2, 5000) с миллисекундным допуском. Этот трёхшаговый шаблон (классификация → комбинированный допуск → целочисленный допуск) повторяется в слиянии сенсоров, выравнивании выборок и утверждениях регрессионных тестов — стоит заучить до мышечной памяти.

Остаток и семантика знака

xrtMathMod(7, 3) = 1, xrtMathMod(-7, 3) = -1 — результат следует знаку делимого (усечённая семантика C fmod). Это отличается от интуиции «остаток — циклическое отображение» (-7 mod 3 = 2, семантика вниз). Почему усечение? Потому что оно согласно со стандартом C, с аппаратными инструкциями, и Mod(a,b) удовлетворяет тождеству Trunc(a/b)*b + Mod(a,b) == a — семантика самосогласованна и выводима. Для «всегда неотрицательного» циклического индекса явно выражайте идиому ((a % n) + n) % n — не заставляйте библиотеку угадывать за вас.

Связь с главой 11: фундамент числовой определённости

Семейство xrtMath разделяет с системой случайностей главы 11 фундамент «детерминированной воспроизводимости»: утверждения воспроизводимости случайного модуля опираются на Near этой главы при числовом сверении. Вместе две главы образуют основание «числовой определённости» тома 2 — контейнеры (том 3) и разбор (том 4) в тестах будут возвращаться к этим двум инструментам.

Кроссплатформенная согласованность

Другой слой ценности xrtMath — один и тот же вход даёт сравнимые результаты под разными компиляторами: классификация, отношения и Near — точная семантика; семейство экспонент-логарифмов держит единую политику точности на ходовых платформах. Снапшоты тестов (скажем, фиксированные последовательности случайных чисел главы 11) поэтому можно сверять на разных CI-машинах.

10.3示例

Полная программа: двусторонние ассерты по семейству

Из примера репозитория examples/math/tour/main.c — каждая функция заверена в обоих направлениях против известных констант:

examples/math/tour/main.c源码 ↗



















 */

#include <stdio.h>
#include <math.h>
#include <xrt.h>

/* 浮点近似比较(容差 1e-9)。 */
static bool exampleNear(double fLeft, double fRight)
{
	return xrtMathNear(fLeft, fRight, 1e-9, 0.0);
}

int main(void)
{
	int iResult = 1;

	/* ---- 关系族 ---- */
	if ( (xrtMathMin(2.0, 3.0) != 2.0) ||
		(xrtMathMin(3.0, 2.0) != 2.0) ||
		(xrtMathMax(2.0, 3.0) != 3.0) ||
		(xrtMathMax(3.0, 2.0) != 3.0) ||
		(xrtMathMin(-1.0, 1.0) != -1.0) ) {
		goto Cleanup;
	}
	if ( (xrtMathSign(-5.0) != -1) ||
		(xrtMathSign(5.0) != 1) ||
		(xrtMathSign(0.0) != 0) ) {
		goto Cleanup;
	}
	if ( (xrtMathTrunc(2.7) != 2.0) ||
		(xrtMathTrunc(-2.7) != -2.0) ) {
		goto Cleanup;
	}
	if ( !exampleNear(xrtMathMod(7.0, 3.0), 1.0) ||
		!exampleNear(xrtMathMod(-7.0, 3.0), -1.0) ) {
		goto Cleanup;  /* 取模随被除数符号(C 的 fmod 截断语义) */
	}
	if ( !exampleNear(xrtMathRad(180.0), 3.14159265358979) ) {
		goto Cleanup;
	}
	printf("math: min/max/sign/trunc/mod/rad ok\n");

	/* ---- 分类族:NaN / Inf / 有限三分 ---- */
	{
		double fNaN = 0.0 / 0.0;
		double fInf = 1.0 / 0.0;

		if ( !xrtMathIsNaN(fNaN) ||
			xrtMathIsNaN(1.0) ||
			xrtMathIsNaN(fInf) ||
			!xrtMathIsInf(fInf) ||
			xrtMathIsInf(1.0) ||
			xrtMathIsInf(fNaN) ||
			!xrtMathIsFinite(1.5) ||
			xrtMathIsFinite(fNaN) ||
			xrtMathIsFinite(fInf) ) {
			goto Cleanup;
		}
	}
	printf("math: isnan/isinf/isfinite = 1/1/1\n");

	/* ---- 指对数族 ---- */
	if ( !exampleNear(xrtMathLog2(8.0), 3.0) ||
		!exampleNear(xrtMathLog2(1.0), 0.0) ||
		!exampleNear(xrtMathExp2(10.0), 1024.0) ||
		!exampleNear(xrtMathLog1p(0.0), 0.0) ||
		!exampleNear(xrtMathExpm1(0.0), 0.0) ||
		!exampleNear(xrtMathLog1p(1.0), 0.693147180559945) ||
		!exampleNear(xrtMathExpm1(1.0), 1.718281828459045) ) {
		goto Cleanup;
	}
	printf("math: log2/exp2/log1p/expm1 ok\n");

	/* ---- 立方根:负数域 ---- */
	if ( !exampleNear(xrtMathCbrt(27.0), 3.0) ||
		!exampleNear(xrtMathCbrt(-27.0), -3.0) ||
		!exampleNear(xrtMathCbrt(0.0), 0.0) ) {
		goto Cleanup;
	}
	printf("math: cbrt(-27) = %g\n", xrtMathCbrt(-27.0));
	iResult = 0;

Cleanup:
	return iResult;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/math/tour/main.c -lws2_32 -liphlpapimath: min/max/sign/trunc/mod/rad okmath: isnan/isinf/isfinite = 1/1/1math: log2/exp2/log1p/expm1 okmath: cbrt(-27) = -3

Что только что произошло. (1) Семейство отношений проверено в обе стороны: Min(2,3) и Min(3,2) дают 2 — порядок аргументов не влияет на семантику; такие «симметричные ассерты» — стандартная проба функций отношений. (2) Mod(-7,3) зафиксирован ассертом как -1, пришпиливая усечённую семантику в тест — если ваш код зависит от другой семантики, он разоблачается на ассерте, а не становится багом направления после релиза. (3) Три классифицирующие функции проверены взаимоисключающе на трёх образцах 0.0/0.0 (NaN), 1.0/0.0 (Inf) и 1.5 (конечное) — NaN не «равен даже самому себе»; классифицирующие функции — единственное надёжное распознавание. (4) Cbrt(-27) = -3: кубический корень определён и корректен на отрицательных — ключевые отличия от pow.

Полная программа: три сценария комбинированного допуска

Из examples/math/near/main.c — все три выбора допуска за один проход:

examples/math/near/main.c源码 ↗



















 */

#include <stdio.h>

#include <xrt.h>



int main(void)
{
	/* 相对容差:100 vs 100.05,差 0.05 ≤ 0.001×100 → near。 */
	printf("relative: %s\n",
		xrtMathNear(100.0, 100.05, 0.0, 0.001) ? "near" : "different");

	/* 绝对容差:零附近,1e-10 ≤ 1e-9 → near(相对容差在此失效)。 */
	printf("absolute: %s\n",
		xrtMathNear(0.0, 1e-10, 1e-9, 0.0) ? "near" : "different");

	/* 整数版:|1000-1003| = 3 ≤ 5 → near(采样抖动判断等场景)。 */
	printf("integer : %s\n",
		xrtMathIntNear(1000, 1003, 5) ? "near" : "different");
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/math/near/main.c -lws2_32 -liphlpapirelative: nearabsolute: nearinteger : near

Что только что произошло. (1) 100 и 100.05 признаны близкими с относительным допуском 0.001: разница 0.05 ≤ 0.1, мимо — большие значения требуют относительного. (2) 0 и 1e-10 признаны близкими с абсолютным допуском 1e-9 — относительный здесь потребовал бы нулевой разницы (любое кратное нуля — ноль), полностью отказав. (3) Целочисленная версия IntNear(1000, 1003, 5): суждения вида «несколько единиц дрожания или ухода часов не имеют значения» не нуждаются в плавающем допуске. Заметьте: два вызова Near в примере передают по одному нулевому допуску — комбинированный допуск позволяет открыть одну сторону, но в инженерии чаще дают обе: абсолютная сторона подхватывает околонулевую зону, относительная — большие значения, одна строка суждения покрывает весь диапазон.

Ставить Near в макрос ассерта — стандартный ход многих команд: ASSERT_NEAR(got, want) разворачивается в вызов Near, и тесты не дают ложных срабатываний от погрешности представления и не пропускают от чрезмерно широкого допуска. Пиша тест, сперва ясно подумайте «в каком порядке величин работает этот ассерт», потом выбирайте допуск — это важнее заученных чисел.

10.4契约

  • Комбинация Near: |a-b| ≤ max(abs, rel×|a|); нулевой допуск отключает свою сторону; обе стороны — надёжнейше; макросы ассертов стоит строить прямо на Near.
  • Усечение Mod: результат следует знаку делимого; тождество Trunc(a/b)*b + Mod(a,b) == a выполняется; неотрицательные индексы нормализуйте сами.
  • Trunc к нулю: -2.7 → -2; отличается от округления вниз — две семантики не смешивать.
  • Sign в трёх состояниях: -1/0/1; ноль — не положительное и не отрицательное.
  • Трёхчастная классификация: NaN/Inf/конечное взаимоисключающи и полны; судить NaN можно только через IsNaN — все операторы сравнения на NaN ненадёжны.
  • Направление Rad: градусы → радианы, умножение на π/180; перепутаете — вся тригонометрия врёт, стоит пришпилить ассертом.
  • Cbrt на отрицательных: кубический корень корректен; не заменяйте на pow(x, 1.0/3.0).

Типичные ошибки

Ловушка 1: сравнение плавающей точки через ==:Ловушка 1: сравнение плавающей точки через ==

Симптомы: ассерты падают спорадически, выравнивание графики то проходит, то нет, поведение различается по платформам; самая коварная форма — «debug-сборка проходит, release падает».

Причина: путь округления плавающих операций зависит от оптимизаций компилятора и промежуточной точности; двоичный результат 0.1+0.2 — просто другое число, чем 0.3.

错误写法
double fSum = 0.1 + 0.2;
if ( fSum == 0.3 ) {          /* всегда ложь: разные двоичные представления */
	accept();
}
正确写法
double fSum = 0.1 + 0.2;
if ( xrtMathNear(fSum, 0.3, 1e-12, 1e-9) ) {   /* комбинированный допуск */
	accept();
}
Ловушка 2: предположение, что остаток всегда неотрицателен:Ловушка 2: предположение, что остаток всегда неотрицателен

Симптомы: индексы кольцевого буфера и хеш-корзины выходят за границы или разворачиваются на отрицательных входах; частое в коде, мигрировавшем из Python/Go.

Причина: % в C и xrtMathMod — усечённая семантика, отрицательное делимое даёт отрицательный результат; «остаток неотрицателен» — семантика других языков.

错误写法
int iIndex = (int)xrtMathMod(iOffset, 3.0);   /* iOffset=-7 -> -1 */
Touch(Buffer[iIndex]);                        /* за границей: отрицательный индекс */
正确写法
int iIndex = ((int)iOffset % 3 + 3) % 3;      /* явная нормализация в [0,3) */
Touch(Buffer[iIndex]);

Практические задания

Базовое: три образца классификации

Напишите программу, конструирующую NaN (0.0/0.0), Inf (1.0/0.0) и конечное (1.5), печатающую трёхчастный результат каждого значения (три строки, девять булевых), проверяя взаимоисключаемость и полноту; затем сравните NaN с самим собой через ==, напечатайте результат и объясните, почему классификация не может полагаться на сравнение.

Продвинутое: зонд границы допуска

Для Near(100.0, x, 0.0, 0.001) бинарным поиском найдите границу x близости и сравните с ручным расчётом 100×(1±0.001). Подсказка: на один ulp (минимальная единица представления) за границей суждение должно перевернуться в «не близко»; проверьте обе границы, убедившись, что относительный допуск действует симметрично.

Вызов: сравнение числовой стабильности

Для десяти точек x от 1e-15 до 1e-1 вычислите погрешность log(1+x) и xrtMathLog1p(x) против высокоточного эталона (приближает версия на long double), сведя таблицу сравнения. Критерий приёмки: таблица показывает взрыв погрешности log(1+x) при малом x и стабильность Log1p, с собственным объяснением причины (подсказка: округление 1+x).

Итоги

ТемаКраткая справка
СравнениеNear(a,b,abs,rel) берёт `max(abs, rel×a); для целых IntNear`
Остатокусечённая семантика по знаку делимого; неотрицательный индекс сами через ((x%n)+n)%n
ЗнакSign три состояния -1/0/1
КлассификацияIsNaN / IsInf / IsFinite взаимоисключающи и полны; NaN ненадёжен во всех сравнениях
Стабильные функцииу нуля — Log1p / Expm1, никогда log(1+x)
Корни из отрицательныхкубический корень через Cbrt; pow(x,1/3) на отрицательных непригоден

📖 Справочники API в этой главе:math (20 записей)

Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки