深入了解gawk:浮点运算、任意精度整数运算及扩展编写
1. 浮点运算中的舍入模式
在进行浮点运算时,舍入模式是一个关键因素。如果系统的C库在使用
printf
处理中间值时未采用IEEE 754的偶数舍入规则,输出结果可能会截然不同。
ROUNDMODE
变量可对程序的舍入模式进行控制。它与IEEE舍入模式的对应关系如下表所示:
| 舍入模式 | IEEE名称 | ROUNDMODE |
| — | — | — |
| 四舍五入,中间值取偶数 | roundTiesToEven | “N” 或 “n” |
| 向正无穷舍入 | roundTowardPositive | “U” 或 “u” |
| 向负无穷舍入 | roundTowardNegative | “D” 或 “d” |
| 向零舍入 | roundTowardZero | “Z” 或 “z” |
| 四舍五入,中间值远离零 | roundTiesToAway | “A” 或 “a” |
ROUNDMODE
的默认值为 “N”,即选择了IEEE 754的
roundTiesToEven
舍入模式。需要注意的是,”A” 对应的
roundTiesToAway
模式仅在MPFR库支持时可用,否则设置无效。
默认的
roundTiesToEven
模式虽最受推荐,但却不太直观。对于大多数数值,它会将其舍入到最接近的数字。例如,将1.132舍入到两位小数得到1.13,将1.157舍入得到1.16。然而,当遇到恰好处于中间的数值时,它会将数字舍入到最接近的偶数。比如,将0.125舍入到两位小数会向下舍入为0.12,而将0.6875舍入到三位小数则会向上舍入为0.688。
以下是一个示例代码:
BEGIN {
x = -4.5
for (i = 1; i < 10; i++) {
x += 1.0
printf("%4.1f => %2.0f\n", x, x)
}
}
在作者的系统上运行该代码,输出如下:
-3.5 => -4
-2.5 => -2
-1.5 => -2
-0.5 => 0
0.5 => 0
1.5 => 2
2.5 => 2
3.5 => 4
4.5 => 4
roundTiesToEven
模式的原理是,它大致均匀地分配了精确中间值的向上和向下舍入,这可能会使累积的舍入误差相互抵消。这也是IEEE 754计算函数和运算符的默认舍入模式。
其他舍入模式很少使用。向正无穷舍入(
roundTowardPositive
)和向负无穷舍入(
roundTowardNegative
)常用于实现区间算术,通过调整舍入模式来计算输出范围的上下界。向零舍入(
roundTowardZero
)模式可用于将浮点数转换为整数。
roundTiesToAway
模式则将结果舍入到最接近的数字,若出现平局则选择绝对值较大的数字。
一些数值分析师认为,舍入方式的选择会对最终结果产生重大影响,建议在最终输出时再进行舍入。实际上,我们可以通过初始设置足够大的精度,使舍入误差的累积不影响最终结果,从而避免舍入误差问题。若怀疑计算结果对舍入误差累积敏感,可以通过改变舍入模式来观察输出的显著差异。
2. gawk的任意精度整数运算
当使用 -M 选项时,gawk 会使用 GMP 任意精度整数进行所有整数运算。在源文件或数据文件中,任何看起来像整数的数字都会被存储为任意精度整数,其大小仅受可用内存的限制。
例如,以下代码计算 5 的 4 的 3 的 2 次方:
$ gawk -M 'BEGIN {
> x = 5^4^3^2
> print "number of digits =", length(x)
> print substr(x, 1, 20), "...", substr(x, length(x) - 19, 20)
> }'
输出结果为:
number of digits = 183231
62060698786608744707 ... 92256259918212890625
若使用任意精度浮点数计算相同的值,根据公式
prec = 3.322 ⋅ dps
,所需精度为
prec = 3.322 ⋅ 183231
,即 608693。
整数和浮点数进行算术运算的结果是一个精度等于工作精度的浮点数。以下程序使用递归计算 Sylvester 序列的第八项:
$ gawk -M 'BEGIN {
> s = 2.0
> for (i = 1; i <= 7; i++)
> s = s * (s - 1) + 1
> print s
> }'
输出结果为:
113423713055421845118910464
该输出与实际数字 113,423,713,055,421,844,361,000,443 不同,原因是默认的 53 位精度不足以精确表示浮点结果。我们可以通过增加精度(在这种情况下 100 位就足够了),或者将浮点常量
2.0
替换为整数,使用整数运算进行所有计算,以获得正确的输出。
有时,gawk 必须将任意精度整数隐式转换为任意精度浮点数,这主要是因为 MPFR 库并不总是提供处理任意精度整数或混合模式数字所需的相关接口。在这种情况下,精度会设置为精确转换所需的最小值,而工作精度不会用于此目的。如果这不是我们所需要的,可以采用一种变通方法,先将整数转换为浮点数,例如:
gawk -M 'BEGIN { n = 13; print (n + 0.0) % 2.0 }'
我们也可以从一开始就将数字指定为浮点数,以避免这个问题:
gawk -M 'BEGIN { n = 13.0; print n % 2.0 }'
对于这个特定的例子,最好直接使用以下代码:
gawk -M 'BEGIN { n = 13; print n % 2 }'
3. 标准与现有实践
历史上,awk 在需要时会将任何非数字字符串转换为数值零。此外,原始的语言定义和 POSIX 标准规定 awk 只识别十进制数(基数为 10),而不识别八进制(基数为 8)或十六进制数(基数为 16)。
2001 年和 2004 年 POSIX 标准语言的变化可以理解为 awk 应支持额外的功能,这些功能包括:
- 解释以十六进制表示法指定的浮点数据值(例如,0xDEADBEEF)。(注意:是数据值,而不是源代码常量。)
- 支持特殊的 IEEE 754 浮点值 “非数字”(NaN)、正无穷(“inf”)和负无穷(“−inf”)。具体来说,这些值的格式由 ISO 1999 C 标准指定,该标准忽略大小写,并且允许在
nan
之后有依赖于实现的额外字符,同时允许使用
inf
或
infinity
。
这带来了两个问题:
- 这两个功能都是对历史实践的明显改变。gawk 维护者认为,支持十六进制浮点值是不美观的,而且最初的设计者从未打算将其纳入语言。允许完全字母的字符串具有有效的数值也是对历史实践的严重背离。
- gawk 维护者认为,这种对标准的解释是经过一定的 “语言推敲” 才得出的,甚至标准开发者也并非有意如此。
认识到这些问题后,为了与早期版本的标准兼容,2008 年 POSIX 标准增加了明确的措辞,允许但不要求 awk 支持十六进制浮点值以及 “非数字” 和无穷大的特殊值。
尽管 gawk 维护者仍然认为提供这些功能是不明智的,但在支持 IEEE 浮点的系统上,提供某种方式支持 NaN 和无穷大值似乎是合理的。gawk 实现的解决方案如下:
- 使用
--posix
命令行选项时,gawk 会 “放手不管”。字符串值会直接传递给系统库的
strtod()
函数,如果该函数成功返回一个数值,就使用该数值。这些结果在不同系统之间是不可移植的,而且有些出人意料。例如:
$ echo nanny | gawk --posix '{ print $1 + 0 }'
nan
$ echo 0xDeadBeef | gawk --posix '{ print $1 + 0 }'
3735928559
-
不使用
--posix时,gawk 会特殊处理四个字符串值+inf、-inf、+nan和-nan,产生相应的特殊数值。前导符号向 gawk(和用户)表明该值确实是数值。不支持十六进制浮点数(除非同时使用--non-decimal-data,但不推荐这样做)。例如:
$ echo nanny | gawk '{ print $1 + 0 }'
0
$ echo +nan | gawk '{ print $1 + 0 }'
nan
$ echo 0xDeadBeef | gawk '{ print $1 + 0 }'
0
gawk 会忽略这四个特殊值的大小写,因此
+nan
和
+NaN
是相同的。
4. 为gawk编写扩展
在支持 C 的
dlopen()
和
dlsym()
函数的系统上,可以使用动态加载库为 gawk 添加用 C 或 C++ 编写的新函数。
4.1 扩展概述
扩展(有时也称为插件)是一段外部编译代码,gawk 可以在运行时加载它,以提供除本书其他部分描述的内置功能之外的额外功能。扩展的好处在于可以扩展 gawk 的功能,例如提供对系统调用(如
chdir()
用于更改目录)和其他有用的 C 库例程的访问。只要能想象到且能用 C 或 C++ 编写的功能,都可以通过编写扩展来实现。
扩展使用 gawk 开发者定义的应用程序编程接口(API)用 C 或 C++ 编写。下面将详细介绍 API 提供的功能以及如何使用它们,并给出一个小的扩展示例。
4.2 扩展许可
每个动态扩展都必须在与 GNU GPL 兼容的许可证下分发。为了让 gawk 知道扩展已正确授权,扩展必须定义全局符号
plugin_is_GPL_compatible
。如果该符号不存在,gawk 在尝试加载扩展时会发出致命错误并退出。该符号的声明类型应为
int
,不需要在任何分配的部分中,只需确保该符号存在于全局作用域即可。示例代码如下:
int plugin_is_GPL_compatible;
4.3 高层工作原理
gawk 和扩展之间的通信是双向的。当扩展被加载时,gawk 会传递一个指向结构体的指针给它,该结构体的字段是函数指针。扩展可以通过这些函数指针在运行时调用 gawk 内部的函数,而无需在链接时访问 gawk 的符号。其中一个函数指针用于 “注册” 新函数。
扩展通过将提供新功能的函数的函数指针传递给 gawk 来注册其新函数,gawk 将函数指针与名称关联起来,然后可以使用定义的调用约定调用它。
do_xxx()
函数则使用 API 结构体中的函数指针来完成其工作,如更新变量或数组、打印消息、设置
ERRNO
等。
方便的宏使通过函数指针的调用看起来像常规的函数调用,因此扩展代码很容易编写和阅读。
此外,API 还提供了对 gawk 的
do_xxx
值的访问,这些值反映了命令行选项,如
do_lint
、
do_profiling
等,但扩展不能影响 gawk 内部这些值。同时,API 还提供了主要和次要版本号,以便扩展可以检查它所加载的 gawk 是否支持它编译时使用的功能。
以下是 gawk 与扩展交互的流程图:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
A([开始]):::startend --> B(加载扩展):::process
B --> C(gawk传递结构体指针给扩展):::process
C --> D(扩展通过指针调用gawk内部函数):::process
D --> E(扩展注册新函数给gawk):::process
E --> F(gawk调用扩展的新函数):::process
F --> G(新函数使用API指针完成工作):::process
G --> H([结束]):::startend
4.4 API 详细描述
扩展的 C 或 C++ 代码必须包含头文件
gawkapi.h
,该文件声明了用于与 gawk 通信的函数并定义了数据类型。API 提供了以下几类操作的函数指针:
- 内存分配、重新分配和释放。
- 注册功能,包括:
- 扩展函数
- 退出回调
- 版本字符串
- 输入解析器
- 输出包装器
- 双向处理器
- 打印致命、警告和 “lint” 警告消息。
- 更新
ERRNO
或取消设置它。
- 访问参数,包括将未定义的参数转换为数组。
- 符号表访问:检索、创建或更改全局变量。
- 创建和释放缓存值,这为多个变量使用值提供了一种高效的方式,可以显著提高性能。
- 数组操作:
- 检索、添加、删除和修改元素
- 获取数组中元素的数量
- 创建新数组
- 清空数组
- 扁平化数组以便于以 C 风格遍历其所有索引和元素
使用 API 时需要注意以下几点:
| C实体 | 头文件 |
| — | — |
| EOF |
|
| errno 值 |
|
| FILE |
|
| NULL |
|
| memcpy() |
|
| memset() |
|
| size_t |
|
| struct stat |
|
由于可移植性问题,特别是对于不完全符合标准的系统,需要在包含
gawkapi.h
之前正确包含相应的标准头文件。
gawkapi.h
文件可以多次包含,但这是不好的编码实践。虽然 API 仅使用 ISO C 90 特性,但 “构造函数” 函数使用
inline
关键字,如果编译器不支持该关键字,应在命令行中添加
-Dinline=''
或使用 GNU Autotools 并在扩展中包含
config.h
文件。
gawk 填充的所有指针都指向 gawk 管理的内存,扩展应将其视为只读。从扩展传递给 gawk 的所有字符串的内存必须通过调用
gawk_malloc()
、
gawk_calloc()
或
gawk_realloc()
获得,此后由 gawk 管理。
API 定义了几个简单的结构体,用于映射从 awk 中看到的值,值可以是双精度浮点数、字符串或数组。字符串值同时维护指针和长度,因为允许嵌入 NUL 字符。在检索值时,扩展可以请求特定类型(数字、字符串、标量、值 cookie、数组或 “未定义”),当请求为 “未定义” 时,返回值将具有实际的底层类型。
5. 编写扩展的注意事项与示例
在编写 gawk 扩展时,除了遵循 API 的规则和要求外,还可以参考一些示例代码来更好地理解如何实现具体的功能。
5.1 示例:文件函数扩展
以
filefuncs.c
这个示例扩展文件为例,它展示了如何使用 API 来实现一些文件相关的功能。虽然没有详细给出该文件的代码,但我们可以推测它可能包含了对文件的打开、读取、关闭等操作。在这个文件中,会使用到前面提到的 API 中的函数指针,比如注册新函数的指针,将自定义的文件操作函数注册到 gawk 中。
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
A([开始]):::startend --> B(包含gawkapi.h):::process
B --> C(定义plugin_is_GPL_compatible):::process
C --> D(实现文件操作函数):::process
D --> E(注册文件操作函数):::process
E --> F(编译为动态库):::process
F --> G(加载到gawk中使用):::process
G --> H([结束]):::startend
5.2 测试 API 的代码示例
testext.c
代码用于测试 API,它可以帮助我们验证扩展与 gawk 之间的通信是否正常,以及 API 提供的各种功能是否能按预期工作。同样,虽然没有给出具体代码,但我们知道它会使用 API 中的函数指针来调用 gawk 内部的函数,检查返回值是否符合预期。
6. 总结与建议
在进行浮点运算、任意精度整数运算以及编写扩展时,有以下几点总结和建议:
6.1 浮点运算
-
舍入模式的选择会对结果产生影响,默认的
roundTiesToEven模式能在一定程度上抵消累积的舍入误差,但在某些情况下,其他舍入模式也有其特定的用途。 - 为避免舍入误差问题,可以初始设置足够大的精度,使舍入误差的累积不影响最终结果。
6.2 任意精度整数运算
-
使用
-M选项可以让 gawk 进行任意精度整数运算,对于超出普通硬件双精度浮点数范围的计算非常有用。 - 当涉及整数和浮点数的混合运算时,要注意精度问题,必要时可以调整精度或使用整数运算来避免误差。
6.3 标准与实践
- 要了解 gawk 在处理浮点数值时与 POSIX 标准的差异,根据实际需求选择合适的命令行选项来处理十六进制浮点值、NaN 和无穷大值。
6.4 扩展编写
-
编写扩展时要遵循 GNU GPL 兼容的许可证,定义
plugin_is_GPL_compatible符号。 - 熟悉 API 的功能和使用方法,注意包含必要的头文件,正确处理内存管理和数据类型。
以下是一个总结表格:
| 运算或操作类型 | 要点 | 建议 |
| — | — | — |
| 浮点运算 | 舍入模式影响结果,默认模式可抵消误差 | 初始设置足够精度,按需选择舍入模式 |
| 任意精度整数运算 | 用
-M
选项处理大整数,注意混合运算精度 | 调整精度或用整数运算避免误差 |
| 标准与实践 | 了解与 POSIX 标准差异 | 根据需求选命令行选项 |
| 扩展编写 | 遵循许可证,熟悉 API | 定义符号,正确处理内存和类型 |
总之,无论是进行复杂的数值计算还是扩展 gawk 的功能,都需要深入理解相关的概念和规则,并根据具体情况做出合适的选择。通过合理运用这些知识和技巧,我们可以更高效地使用 gawk 来完成各种任务。
超级会员免费看
2

被折叠的 条评论
为什么被折叠?



