GNU awk (gawk) 中涉及 NaN 的令人惊讶的数值比较结果
Posted
技术标签:
【中文标题】GNU awk (gawk) 中涉及 NaN 的令人惊讶的数值比较结果【英文标题】:Surprising numeric comparison results involving NaN in GNU awk (gawk) 【发布时间】:2018-12-05 23:57:51 【问题描述】:使用 awk/gawk,我需要执行涉及 NaN 浮点值的数值比较。尽管 gawk 似乎已正确地将我的用户输入转换为数值 NaN(即不是字符串“NaN”),但使用运算符 '' 执行的比较结果与我的预期不符.
期待:
x > y
或 x < y
等比较,其中 x 是 NaN,y 是浮点值(包括 NaN 和 +/-Infinity),应计算为 false。 [需要引用 IEEE 文档(但wikipedia NaN 有表格)]。
实际结果:
NaN 2.0 == 1
下面的 sn-p 获取第一个字段并将0
添加到它以强制转换为整数(如in the gnu awk manual 所述)。然后它使用 printf 显示变量和表达式的类型(我的特定版本的 gawk 没有typeof()
)。
$ echo -e "+nan\n-nan\nfoo\nnanny" | awk \
'x=($1+0); printf "%s: float=%f str=%s x<2==%f x>2==%f\n",$1,x,x,(x<2.0),(x>2.0);'
+nan: float=nan str=nan x<2==0.000000 x>2==1.000000
-nan: float=nan str=nan x<2==0.000000 x>2==1.000000
foo: float=0.000000 str=0 x<2==1.000000 x>2==0.000000
nanny: float=0.000000 str=0 x<2==1.000000 x>2==0.000000
$ echo -e "+nan\n-nan\nfoo\nnanny" | awk --posix \
'x=($1+0); printf "%s: float=%f str=%s x<2==%f x>2==%f\n",$1,x,x,(x<2.0),(x>2.0);'
+nan: float=nan str=nan x<2==0.000000 x>2==1.000000
-nan: float=nan str=nan x<2==0.000000 x>2==1.000000
foo: float=0.000000 str=0 x<2==1.000000 x>2==0.000000
nanny: float=nan str=nan x<2==0.000000 x>2==1.000000
运行 GNU Awk 4.1.3,API:1.1
是否有不同的方式/选项让 NaN 正确传播? 我在standards vs practice 上阅读了有关 NaN 的页面,我认为我的做法是正确的。我觉得 NaN 可能不太适合 awk。例如,我找不到可靠的方法来测试一个值是否为 NaN(通过 printf 除外)。
【问题讨论】:
【参考方案1】:POSIX 有什么要说的?
首先,POSIX 允许但不要求 awk 支持 NaN
或 Inf
值。来自awk IEEE Std 1003.1-2017 POSIX standard:
awk 的历史实现不支持数字字符串中的浮点无穷大和 NaN;例如,
"-INF"
和"NaN"
。但是,如果使用函数的 ISO/IEC 9899:1999 标准版本而不是 ISO/IEC 9899:1990 标准版本,则使用atof()
或strtod()
函数进行转换的实现会支持这些值.由于疏忽,该标准的 2001 至 2004 版本不允许支持无穷大和 NaN,但在此修订版中,允许(但不是必需)支持。这是对 awk 程序行为的无声改变;例如,在 POSIX 语言环境中,表达式:("-INF" + 0 < 0)
以前的值是
0
,因为"-INF"
转换为0
,但现在它的值可能是0
或1
。
GNU awk 如何处理如此神奇的 IEEE 数字?GNU awk manual 声明:
如果没有--posix
,gawk 会特别解释四个字符串值"+inf"
、"-inf"
、"+nan
"和"-nan"
,产生相应的特殊数值。前导符号作为gawk (和用户)该值确实是数字。 使用--posix
命令行选项,gawk 变成“放手”。字符串值直接传递给系统库的strtod()
函数,如果成功返回一个数值,就使用这个。 根据定义,结果不能跨不同系统移植。
因此,简而言之,GNU awk(没有 --posix
选项)仅能够成功转换字符串“+nan”、“-nan”、“+inf”和“-inf " 转换为浮点表示(参见函数is_ieee_magic_val
)。
令人惊讶的是,它不会转换"nan"
和"inf"
,尤其是因为"+nan"+0
的字符串转换是无符号"nan"
$ gawk 'BEGINprint "+nan"+0, "nan"+0'
nan 0
备注: 使用 --posix
时,GNU awk 可能会识别字符串 "nan"
和 "inf"
以及其他字符串,例如 "infinity"
或完全出乎意料的 "nano"
或 @ 987654360@。后者可能是主要原因——当不使用--posix
时——符号是最重要的,只有字符串“+nan”、“-nan”、“+inf”和“-inf”被识别。
GNU awk 如何对这些神奇的 IEEE 数字进行排序?
在挖掘 GNU awk 的源代码时,我们发现例程 cmp_awknums
的以下注释:
/* * This routine is also used to sort numeric array indices or values. * For the purposes of sorting, NaN is considered greater than * any other value, and all NaN values are considered equivalent and equal. * This isn't in compliance with IEEE standard, but compliance w.r.t. NaN * comparison at the awk level is a different issue and needs to be dealt * within the interpreter for each opcode separately. */
这解释了 OP 的原始问题,为什么 NaN 不遵循 IEEE 比较,因此 ("+nan"+0<2)
是 0 (false)
和 ("+nan"+0>2)
是 1 (true)
。 (备注:我们在字符串中添加了一个零以确保数字转换)
这可以用下面的代码来证明(不是--posix
):
BEGIN s = "1.0 +nan 0.0 -1 +inf -0.0 1 1.0 -nan -inf 2.0"; split(s, a)
PROCINFO["sorted_in"] = "@val_num_asc"
for (i in a) printf a[i] OFS; printf "\n"
PROCINFO["sorted_in"] = "@val_num_desc"
for (i in a) printf a[i] OFS; printf "\n"
输出以下顺序:
-inf -1 -0.0 0.0 1 1.0 1.0 2.0 +inf +nan -nan
-nan +nan +inf 2.0 1.0 1.0 1 0.0 -0.0 -1 -inf
如果NaN
遵循 IEEE 约定,则它应该始终出现在列表的开头,而不考虑顺序,但显然情况并非如此。使用--posix
时也是如此:
function arr_sort(arr, x, y, z)
for (x in arr) y = arr[x]; z = x - 1
# force numeric comp
while (z && arr[z]+0 > y+0) arr[z + 1] = arr[z]; z--
arr[z + 1] = y
BEGIN s = "1.0 +nan 0.0 -1 +inf -0.0 1 1.0 -nan -inf 2.0"
s = s" inf nan info -infinity"; split(s, a)
arr_sort(a)
for (i in a) printf a[i] OFS; printf "\n"
-inf -infinity -1 0.0 -0.0 1.0 1 1.0 2.0 +inf inf info +nan -nan nan
请注意,字符串“info”被视为无穷大,而在没有--posix
的情况下,它将被转换为ZERO
("inf"
、"nan"
、...的同上)
("+nan" < 2)
和 ("+nan"+0 < 2)
是什么关系?
在第一种情况下,进行纯字符串比较,而在第二种情况下,字符串被强制为数字并进行数字比较。这类似于("2.0" == 2)
和("2.0"+0 == 2)
。第一个返回false,第二个返回true。这种行为的原因是,在第一种情况下,awk 只知道“2.0”是一个字符串,它不检查它的内容,因此它将2
转换为一个字符串。
BEGIN print ("-nan" < 2) , ("-nan" > 2) , ("+nan" < 2) , ("+nan" > 2)
print ("-nan"+0 < 2), ("-nan"+0 > 2), ("+nan"+0 < 2), ("+nan"+0> 2)
print ("-nan"+0 ) , ("-nan"+0) , ("+nan"+0) , ("+nan"+0)
1 0 1 0
0 1 0 1
nan nan nan nan
如何检查inf
或nan
:
function isnum(x) return x+0 == x
function isnan(x) return (x+0 == "+nan"+0)
function isinf(x) return ! isnan(x) && isnan(x-x)
BEGINinf=log(0.0);nan=sqrt(-1.0);one=1;foo="nano";
print "INF", inf , isnum(inf) , isnan(inf) , isinf(inf)
print "INF", -inf , isnum(-inf) , isnan(-inf) , isinf(-inf)
print "INF", "+inf", isnum("+inf"), isnan("+inf"), isinf("+inf")
print "INF", "-inf", isnum("-inf"), isnan("-inf"), isinf("-inf")
print "NAN", nan , isnum(nan) , isnan(nan) , isinf(nan)
print "NAN", -nan , isnum(-nan) , isnan(-nan) , isinf(-nan)
print "NAN", "+nan", isnum("+nan"), isnan("+nan"), isinf("+nan")
print "NAN", "-nan", isnum("-nan"), isnan("-nan"), isinf("-nan")
print "ONE", one , isnum(one) , isnan(one) , isinf(one)
print "FOO", foo , isnum(foo) , isnan(foo) , isinf(foo)
这会返回:
INF -inf 1 0 1
INF inf 1 0 1
INF +inf 1 0 1
INF -inf 1 0 1
NAN -nan 1 1 0
NAN nan 1 1 0
NAN +nan 1 1 0
NAN -nan 1 1 0
ONE 1 1 0 0
FOO nano 0 0 0
在查看cmp_awknums
的源码时,我们可以确信isnan(x)
函数可以正常工作(添加了一些cmets来解释):
int cmp_awknums(const NODE *t1, const NODE *t2)
// isnan is here the C version
// this ensures that all NANs are equal
if (isnan(t1->numbr))
return ! isnan(t2->numbr);
// this ensures that all NANs are bigger than any other number
if (isnan(t2->numbr))
return -1;
// <snip>
【讨论】:
字符串转换不是这里的(唯一)问题。否则awk 'BEGINprintf "%s\n", (("+nan"+0)<2)'
和 awk 'BEGINprintf "%s\n", (("+nan")<2)'
将返回相同的值。我相信字符串(关键字?)nan
具有特殊含义,至少在处理数字时是这样。
@oliv 更新了答案。答案在源代码中找到。
@oliv 现在我确信 awk 中存在错误
这一切都非常不幸。这样一个简单的技术问题,由于遗留和相互冲突的标准而变得复杂。我想知道排序包含 NaN 的数组时的差异是否是由于排序本身不能保证“稳定”。您似乎对 awk 内部结构有深入的了解,您是否愿意建议在没有可靠的 NaN 安全比较运算符的情况下,如何安全地首先确定值是否为数字 NaN
在使用典型运算符之前,即function isNaN(x) ... if isNaN(x) && x > 2.0 ...
?
@init_js 我检查了源代码并可以确保提供的isnan(x)
将按预期工作。忽略所有可能的 NaN 有效编码。我还找到了有关此事的更多信息并更新了答案。我希望这对您有所帮助。以上是关于GNU awk (gawk) 中涉及 NaN 的令人惊讶的数值比较结果的主要内容,如果未能解决你的问题,请参考以下文章