为什么:Math.floor(2e+21) != ~~(2e+21)

7

我不是位运算符的专家,但我经常看到256k比赛中的程序员使用一种模式。他们使用双重按位非运算符~~而不是使用Math.floor()函数(可能更快?)。

像这样:

Math.floor(2.1); // 2
~~2.1 // 2

搜索发现有更多的模式是以相同的方式使用的:

2.1 | 0  // 2
2.1 >> 0 // 2

在使用开发者控制台时,我注意到了一种行为,我不确定我是否完全理解。

Math.floor(2e+21);  // 2e+21
~~2e+21;    // -1119879168
2e+21 | 0;  // -1119879168

发生了什么?这里指的是在技术内部发生了什么事情。

8
按位运算符将值作为32位整数处理。所有按位运算符的操作数都被转换为以二进制补码格式表示的有符号32位整数。详见:http://www.ecma-international.org/ecma-262/5.1/#sec-11.4.8 - Felix Kling
1
作为一条提示,编程谜题和代码高尔夫SE是您探索为什么使用~~的适当位置。使用~~而不是Math.floor(n)的主要好处是它可以节省11个字符,这在任务是尽可能地编写短程序时是非常大的字节节省量。 - Compass
可能是Javascript中的双波浪线(~~)有什么作用?的重复问题。 - Raymond Chen
2个回答

4

正如Felix King所指出的那样,这些数字正在被转换为32位有符号整数。2e9小于有符号整数的最大正值,因此这是可行的:

~~(2e9)  //2000000000

但是当你到达2e10时,它无法使用所有的位数,所以它只取最低的32位并将其转换为int类型:

~~(2e10) //-1474836480

您可以通过使用另一个按位运算符并确认它正在获取最低的32位来验证此内容:

2e10 & 0xFFFFFFFF // also -1474836480
~~(2e10 & 0xFFFFFFFF) // also -1474836480

Math.floor函数专门用于处理大数值,所以如果需要在大范围内保持精度,则应使用它。

另外需要注意的是:~~符号用于截断小数部分,对于正数而言与Math.floor相同。但是对于负数则无法使用:

Math.floor(-2.1) // -3
~~(-2.1) // -2

2
MDN文档所述,我引用一下:

所有位运算符的操作数都被转换为二进制补码格式的有符号32位整数。

这意味着当您应用位运算符(例如~)到2.1时,它首先会被转换为整数,然后才应用运算符。这实际上实现了正数的向下取整(floor)效果。
至于为什么使用这些运算符,而不是更易理解的Math.floor,原因有两个。首先,这些运算符可能相对较快地实现相同的结果。除了性能外,有些人只想要最短的代码。您提到的所有三个运算符都可以实现相同的效果,但~~恰好是最短的,也可以说是最容易记住的。
鉴于浮点数转整数发生在位运算符被应用之前,让我们看看使用~~会发生什么。为了简洁起见,我将使用8位来表示我们的目标数字(在从2.1转换后为2)。
  2: 0000 0010
 ~2: 1111 1101    (-3)
~~2: 0000 0010

因此,你看,我们应用一个运算符来检索整数部分,但我们不能仅应用一个按位取反,因为它会破坏结果。我们通过应用第二个运算符将其恢复到所需的值。
关于您最后的例子,请注意您正在测试的数字2e + 21是一个相对较大的数字。它是由二十一个零组成的数字2。当您应用位运算符时,它无法适应32位整数(数据类型被转换为32位整数)。只需查看您的数字与32位有符号整数可以表示的内容之间的差异即可。
Max. Integer:             2147483647
       2e+21: 2000000000000000000000

"二进制怎么样?"
Max. Integer:                                        01111111111111111111111111111111
       2e+21: 11011000110101110010011010110111000101110111101010000000000000000000000

相当大,是吧?
实际上,在幕后发生的事情是Javascript将你的大数字 截断 为32位可以表示的数字。
110110001101011100100110101101110001011 10111101010000000000000000000000
^----            Junk             ----^

当我们将被截断的数字转换成十进制数时,我们得到了你所看到的内容。
Bin: 10111101010000000000000000000000
Dec: -1119879168

相反,Math.floor 考虑到大数并避免截断它们,这可能是它较慢但准确的原因之一。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接