【译】V8是如何实现BigInt的?

简介: 【译】V8是如何实现BigInt的?

640.jpg

文由我的大学同学 AlecHe 翻译

原文地址:https://v8.dev/blog/bigint

原文标题:Adding BigInts to V8


在过去的几个月中,我们已经实现了在V8中支持BigInt,就像在这个提议中定义的一样,包含在了下一个版本的ECMAScript中。接下来这篇文章讲述了我们这段旅程中发生的故事。


总结来说


作为一名Javascript的程序员,现在在你的工具箱中有了支持任意精度的整数:


const a = 2172141653n;const b = 15346349309n;a * b;// → 33334444555566667777n     // Yay!Number(a) * Number(b);// → 33334444555566670000      // Boo!const such_many = 2n ** 222n;// → 6739986666787659948666753771754907668409286105635143120275902562304n


关于新功能以及如何使用的细节,请参考我们深度解析BigInt的文章。我们期待看见你们用它来创造出美好的东西。


在内存中表示BigInt


通常来说,计算机把整数存储在寄存器中(现在通常是32位或是64位),或者是寄存器大小的内存块中。这也决定了你所熟悉的最小值和最大值。比如,一个32位的有符号整数可以表示的数值是从 -2,147,483,648 到 2,147,483,647。但BigInt并不会有这样的限制。

那么什么东西能够存储一个有成百上千位,甚至百万位的BigInt呢?因为它不能存在寄存器里,所以我们需要在内存中分配一个对象,使它足够大,能够在一系列的内存块中存储所有BigInt的二进制位,我们把它称作“digits” ------ 因为它在概念上类似于我们在表示比9大的数时就需要使用更多的位数,比如“10”;十进制中我们用的digits是从0到9,而我们BigInt的digtis是从 0 到 4,294,967,295 (即2^32 - 1)。这是32位CPU寄存器的范围,不包括符号位,因为符号位是分开存储的。在下面的伪代码中,一个拥有 3*32 = 96 位 的BigInt对象内部是这样的:


{  type: 'BigInt',  sign: 0,  num_digits: 3,  digits: [0x12…, 0x34…, 0x56…],}


回顾课堂上的Knuth


存储在CPU寄存器中的整数运算很简单,比如两个数相乘,软件可以使用一套机器指令来告诉CPU如何把两个寄存器的内容相乘,CPU就会照着做。对于BigInt的运算,我们必须要找到自己的解决方案。万幸的是,这是一个基本上每个孩子都会在某个时间点学着解决的问题:记得你在学校计算 345 * 678 的结果(并且不能使用计算器)的时候吗?


345 * 678---------     30    //   5 * 6+   24     //  4  * 6+  18      // 3   * 6+     35   //   5 *  7+    28    //  4  *  7+   21     // 3   *  7+      40  //   5 *   8+     32   //  4  *   8+    24    // 3   *   8=========   233910

这完全就是V8如何相乘BigInt的方法:每次一位,把所有的中间结果相加。这个方法适用于0到9的digits,同样也适用于BigInt中更大的digits。

1969年,Donald Knuth曾在他的著作《计算机程序设计艺术》的第二卷中发表了专门给由较小块组成的大数的乘除法实现。V8的实现也遵循了这本书,从而说明这一成果并不受时间推移的影响。


”少一点去糖化“ === 更多糖?


也许令人惊讶的是,我们不得不花很多精力去实现一元运算,比如 -x。到目前为止,-xx * (-1)做的事情是一样的。为了简化,V8在处理 JavasScript 的时候会尽早地完成这个替换,这个替换过程完成于语法解析阶段。这个方法被称作“去糖化”,因为它把像 -x 一样的表达式当作 x * (-1)的“糖衣语法”。系统的其他部分(解释器,编译器,和整个运行时系统)并不知道什么是一元运算,因为它们只看见了乘法,这个它们当然必须支持的运算。

然而有了BigInt,这样的实现突然变得不合理了,因为把BigInt和一个数(如-1)相乘肯定会抛出一个TypedError。如果x是一个BigInt,语法解析器不得不将-x去糖为x *(-1n),但是它并不知道x会被解析成什么。所以我们必须停止依赖这个尽早去糖化的过程,取而代之的是,我们应该在系统各处都添加对Number和BigInt一元运算的适当支持。


有意思的位运算


如今大部分的计算机系统都采用了一种巧妙的方法“2的补码”来存储有符号整数,它的一个很棒的特质是用第一位来表示符号,并且加上1之后总能保证所表示的数值也增加1,它可以自动地处理符号位的变化,例如对于一个8-bit的整数:


  • 10000000 是 -128,能表示的最小的数,
  • 10000001 是 -127,
  • 11111111 是 -1,
  • 00000000 是 0,
  • 00000001 是 1,
  • 01111111 是 127,能表示的最大的数。


这套编码系统被广泛使用,许多程序员都期待并依赖使用它。传统BigInt的定义也预示着,如果这里新定义的BigInt遵照的这个定义,那么它必须要使用2的补码。但根据之前的内容,V8的BigInt并没有这样做!


为了根据定义来执行二进制运算,我们的BigInt必须假装在后台使用着2的补码。对于正数来说,这没有什么区别,但对于负数,我们必须做额外的工作去达到这样的效果。这有点像 a&b 一样神奇的效果。如果 a 和 b 都是负的 BigInt,实际上是需要执行以下四个步骤(与之相反两个数都是正数的情况下只需要一个步骤):


  1. 把 a 转化成模拟的2的补码
  2. 把 b 转化成模拟的2的补码
  3. 执行运算
  4. 把计算结果再做模拟2的补码转化为得到实际的结果


你或许会问为什么要反复做转换?因为这样会使所有非二进制运算简单一些。


两种新的类型化数组


BigInt提议中包括了两种类型化数组:BigInt64ArrayBigUint64Array。因为BigInt提供了读和写的方式,我们现在可以定义64位的类型化数组,然而如果有人试图用Number来表示其中的元素,那么就会导致有些位的丢失。这就是为什么这两种新的类型化数组与已经存在的 8/16/32 位整数的类型化数组有所不同:前者使用BigInt来访问元素,而使用Number访问元素会抛出异常。


> const big_array = new BigInt64Array(1);> big_array[0] = 123n;  // OK> big_array[0]123n> big_array[0] = 456;TypeError: Cannot convert 456 to a BigInt> big_array[0] = BigInt(456);  // OK


就像JavaScirpt代码,在表示和处理这两种新的类型化数组与传统的类型化数组代码时有所不同,我们必须泛化类型化数组的实现,让它能够对这两个新的类型化数组有不用的处理方式。


优化的考虑


现在,我们正在提交BigInt的基本功能。它已经具备完整的功能并有稳定的性能(比目前的userland库稍微快一些),但它还没有经过特别的优化过。这么做的理由是,我们的目标是优先完成实际应用的功能支持,其次才是那些人为创建的性能测试。我们首先想看看大家是怎么使用BigInt的,这样我们可以根据这些用例去准确地优化大家真正关心的性能!


比如,如果我们看见相对比较小的BigInt(最多64位)才是重要的实际用例,我们可以采用一个特殊的表示方式来提高它的空间效率:


{  type: 'BigInt-Int64',  value: 0x12…,}


其中的一个细节是,我们仍需要考虑是否应该使用"int64"的取值范围,还是"uint64",亦或是两者都支持 ------ 但请注意,如果支持更少这样的"捷径"我就可以更早地发布,而且讽刺的是,每增加一条"捷径"就会使其他的执行路径慢一点,因为涉及的操作总是需要去检查当前的情况是否符合某条”捷径“的要求。


另一个故事是关于对于BigInt在编译器中的优化支持。对于在64位硬件上运行操作着64位数据的计算密集型应用来说,把这些数值存储在寄存器中会比分配给它们一个对象存储在堆空间上(我们现在的方式)快得多。我们已经有了该怎么去支持这一优化的方案,但我们首先需要弄清这个是否是大家最关心的点,我们是否应该把时间花在解决其他问题上。


相关文章
|
存储 算法 NoSQL
6 种常见分布式唯一ID生成策略及它们的优缺点对比
全局唯一的 ID 几乎是所有系统都会遇到的刚需。这个 id 在搜索, 存储数据, 加快检索速度 等等很多方面都有着重要的意义
6 种常见分布式唯一ID生成策略及它们的优缺点对比
|
5月前
|
存储 关系型数据库 MySQL
|
5月前
|
存储 SQL NoSQL
面试题:char和varchar的区别?
字节面试题:char和varchar的区别?
100 0
|
存储 缓存 NoSQL
分布式ID彻底把它搞懂
《分布式》系列
522 0
分布式ID彻底把它搞懂
|
算法
雪花算法(分布式自增长ID)
雪花算法(分布式自增长ID)
76 0
|
存储 算法 安全
场景应用:id全局唯一且自增,如何实现?
场景应用:id全局唯一且自增,如何实现?
183 0
|
监控 算法 NoSQL
分布式ID生成策略
分布式ID生成策略
|
存储 算法 安全
全局唯一ID(自增ID、UUID、雪花算法)
一、介绍 系统唯一id是我们在设计阶段常常遇到的问题。在复杂的分布式系统中,几乎都需要对大量的数据和消息进行唯一标识。在设计初期,我们需要考虑日后数据量的级别,如果可能会对数据进行分库分表,那么就需要有一个全局唯一id来标识一条数据或记录。生成唯一id的策略有多种,但是每种策略都有它的适用场景、优点以及局限性。
|
存储
int与bigint的区别
int与bigint的区别
207 0
|
存储 算法 NoSQL
说起分布式自增ID只知道UUID?SnowFlake(雪花)算法了解一下(Python3实现)
但凡说起分布式系统,我们肯定会对一些海量级的业务进行分拆,比如:用户表,订单表。因为数据量巨大一张表完全无法支撑,就会对其进行分库分表。但是一旦涉及到分库分表,就会引申出分布式系统中唯一主键ID的生成问题,当我们使用mysql的自增长主键(auto\_increment)时,充分感受到了它的好处:整个系统ID唯一,ID是数字类型,而且是趋势递增的,ID简短,查询效率快,在分布式系统中显然由于单点问题无法使用mysql自增长了,此时需要别的解决方案来支撑分布式业务。
说起分布式自增ID只知道UUID?SnowFlake(雪花)算法了解一下(Python3实现)