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 > yx < 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 支持 NaNInf 值。来自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,但现在它的值可能是01

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&lt;2)0 (false)("+nan"+0&gt;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" &lt; 2)("+nan"+0 &lt; 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

如何检查infnan

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)&lt;2)'awk 'BEGINprintf "%s\n", (("+nan")&lt;2)' 将返回相同的值。我相信字符串(关键字?)nan 具有特殊含义,至少在处理数字时是这样。 @oliv 更新了答案。答案在源代码中找到。 @oliv 现在我确信 awk 中存在错误 这一切都非常不幸。这样一个简单的技术问题,由于遗留和相互冲突的标准而变得复杂。我想知道排序包含 NaN 的数组时的差异是否是由于排序本身不能保证“稳定”。您似乎对 awk 内部结构有深入的了解,您是否愿意建议在没有可靠的 NaN 安全比较运算符的情况下,如何安全地首先确定值是否为数字 NaN在使用典型运算符之前,即function isNaN(x) ... if isNaN(x) &amp;&amp; x &gt; 2.0 ...? @init_js 我检查了源代码并可以确保提供的isnan(x) 将按预期工作。忽略所有可能的 NaN 有效编码。我还找到了有关此事的更多信息并更新了答案。我希望这对您有所帮助。

以上是关于GNU awk (gawk) 中涉及 NaN 的令人惊讶的数值比较结果的主要内容,如果未能解决你的问题,请参考以下文章

Linux文本处理三剑客之GNU awk的使用

在awk中更改FS以匹配不是文件路径的任何内容

awk从入门到入土(18)gawk线上手册

文本三剑客之awk基础操作

awk

awk 命令