ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

高精度计算入门与immense数据模型精讲(七)

高精度计算入门与immense数据模型精讲(七)

跳转链接

  • 高精度计算入门与immense数据模型精讲(一)
  • 高精度计算入门与immense数据模型精讲(二)
  • 高精度计算入门与immense数据模型精讲(三)
  • 高精度计算入门与immense数据模型精讲(四)
  • 高精度计算入门与immense数据模型精讲(五)
  • 高精度计算入门与immense数据模型精讲(六)
  • 高精度计算入门与immense数据模型精讲(八)
  • 高精度计算入门与immense数据模型精讲(九)
  • 高精度计算入门与immense数据模型精讲(十)
  • 高精度计算入门与immense数据模型精讲(收官)
  • immense大实数数据库

前言:四则运算之上的整数-实数桥梁

上一篇我们完整攻克了高精度除法,完成了加减乘除四则运算的收官。但仅有四则运算还不够——很多高级算法都需要在「整数」和「实数」之间切换:快速幂需要分离整数指数与小数指数,三角函数需要对角度做周期约化,级数迭代需要判断余项是否达到精度阈值。这些操作的底层,都依赖取整函数与取模运算。

取整看起来很简单,不就是丢掉小数部分吗?但真正严谨的取整有四种标准语义:截断取整、地板取整、天花板取整、四舍五入。它们在正数下行为相似,到了负数场景却截然不同,很多初学者都在这里踩过坑。而取模运算更是和取整深度绑定——商的取整方式直接决定了余数的符号和大小。

immense对取整的实现非常有巧思:它没有针对每一位小数写复杂的截断进位逻辑,而是借助Product_10小数点平移函数,用「移位 → 取整 → 复位」的三步通用框架,统一实现了四种取整,还支持任意指定位数(包括十位、百位等整数高位)的取整。

本篇依然遵循「通用原理先行,代码实现对照」的教学节奏:先把四种标准取整的数学定义、区别与适用场景讲透,再讲高精度取整的通用移位法、取模运算的数学定义;最后对照immense的源码,逐函数拆解取整与取模的完整实现。


一、通用原理:取整与取模的核心方法论

1.1 为什么需要标准化取整

很多人对取整的理解停留在「去掉小数部分」,但这只是其中一种取整方式。在数学和工程领域,取整有四种严格定义的标准语义,分别对应不同的应用场景:

取整方式英文数学定义核心方向
截断取整trunc向零取整,直接丢弃小数部分靠近0
地板取整floor不大于原数的最大整数向负无穷
天花板取整ceil不小于原数的最小整数向正无穷
四舍五入round距离最近的整数,0.5向绝对值大的方向入就近取整

正数场景下,trunc、floor、ceil 的区别还不明显;一旦到了负数,四种取整的结果会截然不同。我们用一组经典对比案例建立直观认知:

原值trunc(向零)floor(向负无穷)ceil(向正无穷)round(四舍五入)
1.71122
1.21121
-1.2-1-2-1-1
-1.7-1-2-1-2

教学要点:最容易混淆的是 trunc 和 floor。正数下两者完全一致,负数下完全相反。记住一句话:floor 永远往下走,trunc 永远往零走。比如 -1.2 往下走是 -2,往零走是 -1。

1.2 四种取整的典型应用场景

每种取整都有各自的适用领域,用错了会导致逻辑错误:

  • trunc:最符合直觉的「砍掉小数」,常用于整数除法、取模运算、小数位数截断
  • floor:数学分析、周期函数约化、向下取整计数,是很多数学公式的默认取整
  • ceil:向上取整计数,比如分页计算、资源分配、容量预估
  • round:日常数值展示、统计、财务近似,是最符合人类习惯的取整方式

1.3 高精度取整的通用方法:移位取整法

如果要实现「保留小数点后 Point 位」的取整,直接操作小数部分的第 Point 位会比较麻烦,还要处理 Point 为负数(保留到十位、百位)的情况。

工程上有一种非常优雅的通用方案,叫小数点移位法,核心思想是:

把取整目标位移到整数部分的最低位,这样取整就变成了简单的「丢掉所有小数」,移位还原后就是最终结果。

通用三步框架

对于任意指定位数 Point 的取整:

  1. 移位:把小数点向右移动 Point 位,让目标保留位变成整数部分的最低位
    • Point>0:保留小数位,小数点右移,小数变整数
    • Point<0:保留整数高位,小数点左移,高位变低位
  2. 取整:对移位后的数执行对应语义的取整(截断、地板、天花板、四舍五入)
  3. 复位:把小数点向左移动 Point 位,还原到原来的数量级

举个直观例子:对123.456保留 2 位小数做 floor

  1. 小数点右移2位 →12345.6
  2. 地板取整 →12345
  3. 小数点左移2位 →123.45

再比如:对12345.6保留到百位(Point=-2)做 ceil

  1. 小数点左移2位 →123.456
  2. 天花板取整 →124
  3. 小数点右移2位 →12400

这个方法的最大优势是统一框架:无论保留小数位还是整数高位,无论哪种取整语义,外层移位逻辑完全相同,中间只需要替换一步取整核心即可。几乎所有成熟的高精度实数库都采用这种实现方式。

1.4 取模运算的数学定义

取模(求余)和取整是深度绑定的。数学上,取模运算的通用公式是:

a mod b = a - b × q
其中 q = a / b 的商,q 的取整方式决定了取模的定义

不同的取整方式,对应不同的取模规则:

  • 截断取模(trunc 模):q 取 trunc(a/b),余数符号与被除数相同,C/C++ 等绝大多数语言的整数取模都采用这种
  • 地板取模(floor 模):q 取 floor(a/b),余数符号与除数相同,Python 等语言采用这种

对于正数,两种模的结果完全一致;对于负数,结果会有差异。工程上实现高精度取模,通常会和语言原生行为保持一致,采用 trunc 模。

1.5 高精度取模的通用实现步骤

基于 trunc 取整的高精度取模,通用流程非常简洁:

  1. 计算商 q = a / b
  2. 对商做截断取整:q = trunc(q)
  3. 余数 = a - b × q

这个公式对整数和实数都成立。如果是整数取模,只需要先把两个操作数都截断为整数,再按上述公式计算即可。

1.6 常见坑点盘点

  1. 负数下 floor 和 trunc 混淆

    • 现象:对负数取整结果不符合预期
    • 根源:默认正数思维,忽略了向零和向负无穷的区别
  2. 四舍五入的方向歧义

    • 现象:0.5到底往哪入?银行家舍入还是四舍五入?
    • 常规实现:0.5向绝对值大的方向入,即 1.5→2、-1.5→-2
  3. 取模的符号误解

    • 现象:负数取模结果符号搞反
    • 根源:没搞清楚商的取整方式,trunc 模余数和被除数同号
  4. 实数取模的精度误差

    • 现象:理论上应该为0的余数,实际出现极小的非零值
    • 根源:除法本身的精度截断导致,属于正常的数值误差

二、immense 实现对照:取整取模逐函数精讲

immense的取整体系严格遵循移位取整法的通用框架:floor、ceil、trunc三个函数共享几乎完全相同的移位外壳,只有核心取整逻辑一行之差;round基于floor实现;取模则基于trunc实现。

2.1 公共框架:移位-取整-复位

在拆解单个函数之前,我们先提炼出三个取整函数共用的三段式骨架,后面只需要关注差异部分即可。

通用骨架:

immense 取整函数(constimmense T,intPoint){immense t=T;// 1. Point 边界裁剪if(Point>0&&Point>t.LTGn)Point=t.LTGn;if(Point<0&&(-Point)>t.DCMn)Point=-t.DCMn;intL=0;// 2. 移位:把目标位移到整数最低位for(inti=1;i<=Point;i++)t.Product_10(false),L++;for(inti=Point;i<=-1;i++)t.Product_10(true),L--;// 3. 核心取整逻辑(三个函数唯一不同的地方)...t.DCMp="",t.DCMn=0;// 统一清空小数// 4. 复位:移回原位置for(inti=1;i<=L;i++)t.Product_10(true);for(inti=L;i<=-1;i++)t.Product_10(false);returnt;}

我们先拆解公共部分:

第一步:Point 边界裁剪
if(Point>0&&Point>t.LTGn)Point=t.LTGn;if(Point<0&&(-Point)>t.DCMn)Point=-t.DCMn;

对 Point 做边界保护:

  • Point 为正(保留小数位):不能超过整数位数,避免移位过度
  • Point 为负(保留整数高位):不能超过小数位数,避免移位过度
  • 超出范围就裁剪到边界,保证移位操作合法
第二步:移位与计数
intL=0;for(inti=1;i<=Point;i++)t.Product_10(false),L++;for(inti=Point;i<=-1;i++)t.Product_10(true),L--;
  • Point>0:小数点左移 Point 位(Product_10(false)即除以10),L 记录左移次数
  • Point<0:小数点右移 |Point| 位,L 记录右移次数(负值)

移位完成后,目标保留位刚好落在整数部分的最低位,所有需要丢弃的位都变成了小数部分。

第三步:复位还原
for(inti=1;i<=L;i++)t.Product_10(true);for(inti=L;i<=-1;i++)t.Product_10(false);

用 L 记录的移位次数反向操作,把小数点移回原来的位置,得到最终取整结果。

整个框架完全对应通用原理的「移位→取整→复位」三步法,结构极其规整。

2.2 trunc:截断取整

trunc是最简单的取整,也是三个函数里唯一不需要加减修正的。

完整源码:

immensetrunc(constimmense T,intPoint){immense t=T;if(Point>0&&Point>t.LTGn)Point=t.LTGn;if(Point<0&&(-Point)>t.DCMn)Point=-t.DCMn;intL=0;for(inti=1;i<=Point;i++)t.Product_10(false),L++;for(inti=Point;i<=-1;i++)t.Product_10(true),L--;t.DCMp="",t.DCMn=0;for(inti=1;i<=L;i++)t.Product_10(true);for(inti=L;i<=-1;i++)t.Product_10(false);returnt;}

核心取整只有一行:

t.DCMp="",t.DCMn=0;

直接清空小数部分,相当于向零截断。因为移位后需要保留的位都在整数里,小数部分全部丢掉即可。

截断取整不关心符号,正数负数都是直接丢小数,所以不需要额外的符号判断。

2.3 floor:地板取整

floor向负无穷取整,正数下和 trunc 一致,负数下如果有小数部分要多减1。

完整源码:

immensefloor(constimmense T,intPoint){immense t=T;if(Point>0&&Point>t.LTGn)Point=t.LTGn;if(Point<0&&(-Point)>t.DCMn)Point=-t.DCMn;intL=0;for(inti=1;i<=Point;i++)t.Product_10(false),L++;for(inti=Point;i<=-1;i++)t.Product_10(true),L--;if(!t.symbol&&t.DCMp!="")t=t-immense("1");t.DCMp="",t.DCMn=0;for(inti=1;i<=L;i++)t.Product_10(true);for(inti=L;i<=-1;i++)t.Product_10(false);returnt;}

和 trunc 唯一的区别,就是多了一行核心判断:

if(!t.symbol&&t.DCMp!="")t=t-immense("1");
  • !t.symbol:是负数
  • t.DCMp!="":有小数部分(不是整数)
  • 同时满足时,整数部分再减1

对应地板取整的定义:负数有小数时,要往更小的方向(负无穷)再走一步。比如 -1.2 移位后是 -1.xxx,减1变成 -2.xxx,截断小数后就是 -2,符合 floor(-1.2) = -2。

正数情况下这行判断不触发,行为和 trunc 完全一致。

2.4 ceil:天花板取整

ceil向正无穷取整,和 floor 刚好对称:负数下和 trunc 一致,正数下如果有小数部分要多加1。

完整源码:

immenseceil(constimmense T,intPoint){immense t=T;if(Point>0&&Point>t.LTGn)Point=t.LTGn;if(Point<0&&(-Point)>t.DCMn)Point=-t.DCMn;intL=0;for(inti=1;i<=Point;i++)t.Product_10(false),L++;for(inti=Point;i<=-1;i++)t.Product_10(true),L--;if(t.symbol&&t.DCMp!="")t=t+immense("1");t.DCMp="",t.DCMn=0;for(inti=1;i<=L;i++)t.Product_10(true);for(inti=L;i<=-1;i++)t.Product_10(false);returnt;}

核心判断行:

if(t.symbol&&t.DCMp!="")t=t+immense("1");
  • 正数且有小数部分 → 加1
  • 负数不触发,行为和 trunc 一致

比如 1.2 移位后是 1.xxx,加1变成 2.xxx,截断小数后就是 2,符合 ceil(1.2) = 2。

设计点评:floor 和 ceil 完美对称,一个对负数减1,一个对正数加1。两个函数除了这一行判断,其余代码完全相同,结构极其规整,维护成本极低。

2.5 round:四舍五入

round没有重复实现移位框架,而是直接复用floor,采用经典的「加半取整法」。

完整源码:

inlineimmenseround(constimmense T,constintPoint){immense abs_T=abs(T);immensehalf("0.5");immense res=floor(abs_T+half,Point);if(!T.symbol)res=-res;returnres;}

实现逻辑:

  1. 先取绝对值,统一按正数处理
  2. 加上 0.5 个对应位的单位(这里是加 0.5,移位后对应最低位加0.5)
  3. 调用 floor 取整
  4. 恢复原符号

为什么加0.5再floor就是四舍五入?

  • 小数部分 < 0.5:加0.5后也不到下一个整数,floor后不变,相当于舍
  • 小数部分 ≥ 0.5:加0.5后达到或超过下一个整数,floor后进一位,相当于入

这是四舍五入的标准实现方式,好处是完全复用已有的 floor 函数,不需要再写一遍移位框架。

注意:这个实现的舍入方向是「0.5向绝对值大的方向入」,即 1.5→2、-1.5→-2,符合大多数编程语言的 round 行为。

2.6 取模运算 operator%

immense的取模运算基于 trunc 取整,和 C++ 原生整数取模行为保持一致。

完整源码:

inlineimmense immense::operator%(immense other)const{immense x=trunc(*this),y=trunc(other);returnx-(trunc(x/y)*y);}

实现完全对应通用公式:

  1. 先把两个操作数都截断为整数(trunc),只做整数取模
  2. 计算商:x / y
  3. 商截断取整:trunc(x/y)
  4. 余数 = 被除数 - 除数 × 取整后的商

因为商是 trunc 取整,所以余数的符号和被除数保持一致,和 C++ 原生%运算符行为完全相同,学习成本为零。

拓展说明:当前实现只支持整数取模,实数取模需要去掉开头的两个 trunc。对于大多数应用场景,整数取模已经足够,比如快速幂的指数取模、周期约化等。


三、拓展:取整取模的应用与边界

3.1 Point 参数的灵活用法

immense的取整函数都支持 Point 参数,默认是 0(取整到个位),但它可以正负灵活取值:

  • Point = 2:保留 2 位小数,比如 1.234 → 1.23
  • Point = 0:取整到个位,默认值
  • Point = -2:保留到百位,比如 12345 → 12300

这个设计让取整函数的适用范围大大扩展,不仅能处理小数精度,还能处理大数的高位约化。

3.2 设计权衡点评

  1. 移位法统一框架

    • 优点:代码复用率极高,三个取整函数共享99%的代码,只改一行核心逻辑;支持任意指定位数,扩展性强
    • 缺点:需要多次调用Product_10,有少量函数调用开销
    • 综合来看,是可读性、可维护性远大于性能损耗的优秀设计
  2. round 基于 floor 复用

    • 优点:零冗余代码,逻辑完全依赖已验证的 floor 函数,不易出错
    • 缺点:多一次加法和一次函数调用,开销可忽略不计
  3. 取模限定整数

    • 优点:逻辑简单,行为和原生C++一致,符合用户直觉
    • 缺点:不支持实数取模,需要的话用户可自行基于 trunc 扩展

3.3 自我验证练习

用以下案例推演代码逻辑,检验理解程度:

  1. floor(immense("-1.2"), 0)→ 结果应为 -2
  2. ceil(immense("1.2"), 0)→ 结果应为 2
  3. trunc(immense("-1.7"), 0)→ 结果应为 -1
  4. round(immense("123.45"), 1)→ 结果应为 123.5
  5. immense("10") % immense("3")→ 结果应为 1
  6. immense("-10") % immense("3")→ 结果应为 -1

四、本篇小结与下篇预告

4.1 核心知识点回顾

通用原理部分:

  1. 四种标准取整:trunc向零、floor向负无穷、ceil向正无穷、round四舍五入,负数下行为差异显著
  2. 移位取整法:移位→取整→复位,统一框架处理任意指定位数的取整,是高精度库的通用实现方案
  3. 取模运算公式:a mod b = a - b × trunc(a/b),trunc模下余数与被除数同号

实现对照部分:

  1. floor、ceil、trunc 共享移位-复位框架,仅核心判断一行之差,结构极其规整
  2. floor 负数有小数减1,ceil 正数有小数加1,两者完美对称
  3. round 采用「加半取整法」,完全复用 floor 函数,代码零冗余
  4. 取模基于 trunc 实现,行为与 C++ 原生整数取模一致,仅支持整数取模

4.2 下篇预告

取整取模是高级运算的基石,接下来我们就要进入真正的高级运算——幂与根。

从整数次幂的快速幂算法,到实数次幂的二项式展开,再到平方根、任意次开方,幂运算是高精度计算中非常核心的模块。immense采用了「整数指数快速幂 + 小数指数二项级数」的拆分方案,把任意实数次幂拆解成两部分分别计算,再合并结果。

下一篇我们就从快速幂原理讲起,一步步推导实数次幂的实现思路,再对照pow_I、pow_F、pow、sqrt等函数,完整拆解幂与根的实现逻辑。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进