为什么 AWK 使用 printf 将“0xffffffffbb6002e0”打印为“ffffffffbb600000”?

Shu*_*eng 23 awk

我一直在 AWK ( gawk) 中尝试使用十六进制数字,但有时当我使用 eg 打印它们时printf,它们会在打印时屏蔽掉一些 LSB,如下例所示:

awk 'BEGIN { x=0xffffffffbb6002e0; printf("%x\n", x); }'
ffffffffbb600000
Run Code Online (Sandbox Code Playgroud)

为什么我会遇到这种行为,我该如何纠正?

gawk在 Debian Buster 10 上使用。

Ste*_*itt 38

AWK 中的数字默认为浮点数,您的值超出了可用精度。0xffffffffbb6002e0最终0 10000111110 1111111111111111111111111111111101110110110000000000以 IEEE-754 binary64(双精度)格式表示,表示整数值0xffffffffbb600000。注意低 12 位的变化,四舍五入为零。

转换为任何舍入误差的最小正整数double是 2 53 + 1。数字越大,adouble可以表示的值之间的差距就越大。(步骤 2,然后是 4,然后是 8 等等;这就是为什么您的数字的低十六进制数字四舍五入为零的原因。)


使用 GAWK,如果它是用 MPFR 和 MP 构建的(在 Debian 中就是这种情况),您可以使用以下选项强制使用任意精度-M

$ awk -M 'BEGIN { x=0xffffffffbb6002e0; printf("%x\n", x); }'
ffffffffbb6002e0
Run Code Online (Sandbox Code Playgroud)

对于计算,这将默认为与 IEEE-754 双精度相同的 53 位精度,但该PREC变量可用于控制该精度。有关详细信息,请参阅上面链接的手册。

在处理需要超过默认精度的大整数和浮点值时存在差异,这可能会导致令人惊讶的行为;使用-M其默认设置正确解析大整数(仅后续计算受 影响PREC),而浮点值以解析时定义的精度存储,这意味着PREC需要事先适当设置:

# Default settings, integer value too large to be exactly represented by a binary64
$ awk 'BEGIN { v=1234567890123456789; printf "%.20f\n", v }'
1234567890123456768.00000000000000000000
# Forced arbitrary precision, same integer value stored exactly without rounding
$ awk -M 'BEGIN { v=1234567890123456789; printf "%.20f\n", v }'
1234567890123456789.00000000000000000000
# Default settings, floating-point value requiring too much precision
$ awk 'BEGIN { v=123456789.0123456789; printf "%.20f\n", v }'
123456789.01234567165374755859
# Forced arbitrary precision, floating-point parsing doesn’t change
$ awk -M 'BEGIN { v=123456789.0123456789; printf "%.20f\n", v }'
123456789.01234567165374755859
# Forced arbitrary precision, PREC set in the BEGIN block, no difference
$ awk -M 'BEGIN { PREC=94; v=123456789.0123456789; printf "%.20f\n", v }'
123456789.01234567165374755859
# Forced arbitrary precision, PREC set initially
$ awk -M -vPREC=94 'BEGIN { v=123456789.0123456789; printf "%.20f\n", v }'
123456789.01234567890000000000
Run Code Online (Sandbox Code Playgroud)

在读取输入值时,AWK 只将十进制值识别为数字;要处理非十进制值(八进制或十六进制),应使用GAWK 的strtonum函数处理字段。


ImH*_*ere 9

在 awk 中转换一个字符串(看起来像一个数字):

  1. 它可以作为程序常量分配给变量。
  2. 该函数strtonum()可以转换文本。
  3. 可以使用该选项调用 awk -n(现已弃用)。

一旦转换为数字,在大多数 awk(gawk、mawk、nawk、bawk)中,它存储为 64 位浮点数。这些数字只能包含 53 位尾数。任何附加位都被截断。这允许 53/4 = 13 个十六进制数字(从技术上讲,1 作为整数,点后有 13 个数字)。

您使用的十六进制0xffffffffbb6002e0是二进制:

bc <<<"obase=2;ibase=16;FFFFFFFFBB6002E0"
1111111111111111111111111111111110111011011000000000001011100000
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^<== up to here 53 bits.
Run Code Online (Sandbox Code Playgroud)

awk 中的所有小数和大多数整数都存储为浮点数。GNU awk 的唯一其他选项是使用任意精度,该-M选项。使用该选项意味着所有整数都立即用所需数量和计算机内存允许的位数表示。

$ awk -M 'BEGIN{print 3^4^5}'
373391848741020043532959754184866588225409776783734007750636931722079040617265251229993688938803977220468765065431475158108727054592160858581351336982809187314191748594262580938807019951956404285571818041046681288797402925517668012340617298396574731619152386723046235125934896058590588284654793540505936202376547807442730582144527058988756251452817793413352141920744623027518729185432862375737063985485319476416926263819972887006907013899256524297198527698749274196276811060702333710356481
Run Code Online (Sandbox Code Playgroud)

只要它仅用于与其他整数的计算,就可以毫无问题地使用您的整数。没有分工。

$ awk -M 'BEGIN{x=strtonum(0xffffffffbb6002e0); y=x+234; z=x/77; printf("%x\n%x\n%f\n",x,y,z)}'
ffffffffbb6002e0
ffffffffbb6003ca
239568104838418400.000000
Run Code Online (Sandbox Code Playgroud)

正确的结果x/77应该是239568104838418388.36363636363636363636根据 bc。

如果您需要具有需要超过 53 位的小数部分的数字(即使使用 仍保留精度-M),您需要根据需要使变量PREC大于 53:

$ awk -M -vPREC=200 'BEGIN{x=strtonum(0xffffffffbb6002e0); y=x+234; z=x/77; printf("%x\n%x\n%f\n",x,y,z)}'
ffffffffbb6002e0
ffffffffbb6003ca
239568104838418388.363636
Run Code Online (Sandbox Code Playgroud)

希望这会有所帮助。


所有索赔的代码:

使用 shell 实现可移植性并使用%a更接近浮点数内部表示的 53 位是 13 位:

$ dash -c 'printf "%a\n" 0x1.12345678901234567890123'
0x1.1234567890123p+0
Run Code Online (Sandbox Code Playgroud)

其他 shell(和一些 awk)可能使用 80 位数字和 64 位尾数,最多可以使用 16 位数字:

ksh -c 'printf "%a\n" 0x1.12345678901234567890123'
0x1.1234567890123456000000000000p+0
Run Code Online (Sandbox Code Playgroud)

Awk 仅限于它可以接受的十六​​进制(作为程序常量 ( x=))。

$ awk 'BEGIN { x=0x1fffffffffffff ; y=0x3fffffffffffff; printf("%18s %16x\n%18s %16x\n", x, x+0,y,y+0); }'
  9007199254740991   1fffffffffffff
 18014398509481984   40000000000000

$ mawk -vx=$(printf '%d\n' 0xffffffff) 'BEGIN{y=x*2;printf("%18s %16x\n%18s %16x\n", x, x+0,y,y+0); }'
        4294967295         7fffffff
       8.58993e+09         7fffffff

$ bawk 'BEGIN { x=2147483647 ; y=x*2+1; printf("%18s %16x\n%18s %16x\n", x, x+0,y,y+0); }'
        2147483647         7fffffff
        4294967295         80000000
Run Code Online (Sandbox Code Playgroud)

并且,除非使用-n选项(已弃用)或函数strtonum()(推荐),否则来自文件和/或用户的输入不能接受十六进制数字:

$ awk '{x=$1; printf "%s %x\n",x,x}' <<<0x123
0x123 0

$ awk -n '{x=$1; printf "%s %x\n",x,x}' <<<0x123
0x123 123

$ awk -n '{x=strtonum($1); printf "%s %x\n",$1,x}' <<<0x123
0x123 123

Run Code Online (Sandbox Code Playgroud)

在第一个输入上,awk 只读取第一个0并拒绝后面的所有内容,x因为它看起来像一个单词。它在其他两种情况下正常工作。

所以,我们必须使用十进制数来简化 awk 的事情。如果您的 printf 有限,请使用 bc:

$ val=$(printf "%d" 0x1234567890)
$ awk -vx="$val" 'BEGIN{printf "%s %x\n", x,x}'
78187493520 1234567890

$ val=$(bc <<<'ibase=16;1234567890')
$ awk -vx="$val" 'BEGIN{printf "%s %x\n", x,x}'
78187493520 1234567890
Run Code Online (Sandbox Code Playgroud)

但是,awk 仍然是有限的:

$ val=$(bc <<<'ibase=16; 12345678901234')
$ awk -vx="$val" 'BEGIN{printf "%s %x\n", x,x}'
5124095575331380 12345678901234

$ val=$(bc <<<'ibase=16; 123456789012345')
$ awk -vx="$val" 'BEGIN{printf "%s %x\n", x,x}'
81985529205302085 123456789012340
Run Code Online (Sandbox Code Playgroud)

在这里它削减了最后一个5,因为它不能用 53 位的浮点数表示。

如果使用任意精度的bignum( -M) 选项,处理大数的能力会提高,但仅适用于整数:

$ val=$(bc <<<'ibase=16; 12345678901234567890123456789')" 
$ awk    -vx="$val" 'BEGIN{printf "%s %x\n", x,x}'
5907679980460342222050878921467785 5.90768e+33

$ awk -M -vx="$val" 'BEGIN{printf "%s %x\n", x,x}'
5907679980460342222050878921467785 12345678901234567890123456789
Run Code Online (Sandbox Code Playgroud)

如果你确实需要与大数字和长小数工作,你需要同时改变PREC使用(53默认情况下)。

$ awk -M -vx='12345678901234567890123456789' 'BEGIN{printf "%s \n%f\n", x,x/100}'
12345678901234567890123456789 
123456789012345678152597504.000000

$ awk -M -vPREC=500 -vx='12345678901234567890123456789' 'BEGIN{printf "%s \n%f\n", x,x/100}'
12345678901234567890123456789 
123456789012345678901234567.890000
Run Code Online (Sandbox Code Playgroud)