Ориентация главы
Том 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 / IsFinite | NaN / бесконечность / конечное — три пути, взаимоисключающие и полные |
| Экспоненты-логарифмы | 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|): абсолютный и относительный допуски, действует больший:
Практическая мерка выбора: абсолютный допуск = порядок величины, при котором вы считаете «физически неотличимо» (координаты, 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 — каждая функция заверена в обоих направлениях против известных констант:
*/ #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; }
Что только что произошло. (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 — все три выбора допуска за один проход:
*/ #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; }
Что только что произошло. (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).
Типичные ошибки
Симптомы: ассерты падают спорадически, выравнивание графики то проходит, то нет, поведение различается по платформам; самая коварная форма — «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(); }
Симптомы: индексы кольцевого буфера и хеш-корзины выходят за границы или разворачиваются на отрицательных входах; частое в коде, мигрировавшем из 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 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки