JavaScript中最快的模幂运算

12
我的问题是如何在JavaScript中快速计算(g^x) mod p,其中^是指数运算,mod是模运算。所有输入都是非负整数,x大约有256位,p是一个2048位的质数,g可能有多达2048位。
我找到的大多数能够在JavaScript中完成此操作的软件似乎都使用JavaScript BigInt库(http://www.leemon.com/crypto/BigInt.html)。使用此库进行单个如此大的指数运算需要大约9秒钟,在我的慢浏览器(Firefox 3.0与SpiderMonkey)上。我正在寻找至少快10倍的解决方案。明显的想法是使用平方乘(通过平方进行指数运算,http://en.wikipedia.org/wiki/Exponentiation_by_squaring),但对于2048位的数字来说太慢了:它需要多达4096次乘法。
升级浏览器不是一个选项。使用另一种编程语言也不是一个选项。将数字发送到Web服务也不是一个选项。
是否有更快的替代方法实现?
更新:按照outis下面回答中提到的文章http://www.ccrwest.org/gordon/fast.pdf建议,通过进行一些额外的准备工作(即预先计算几百个幂),可以仅使用最多354次模乘法来进行2048位模指数运算。(传统的平方-乘法方法速度较慢:最多使用4096次模乘法。)这样做可以在Firefox 3.0中将模指数运算加速6倍,在Google Chrome中加速4倍。之所以没有获得完整的4096/354加速比,是因为BigInt的模指数算法已经比平方-乘法更快,因为它使用Montgomery约简算法(http://en.wikipedia.org/wiki/Montgomery_reduction)。
更新:从BigInt的代码开始,似乎值得进行两级手动优化(和内联)Karatsuba乘法(http://en.wikipedia.org/wiki/Karatsuba_algorithm),然后再转换为BigInt中实现的基于32768的O(n^2)乘法。这将使2048位整数的乘法加速2.25倍。不幸的是,模操作并没有变得更快。
更新:使用在http://www.lirmm.fr/arith18/papers/hasenplaugh-FastModularReduction.pdf中定义的修改过的Barrett约简和Karatsuba乘法以及预先计算幂(如http://www.ccrwest.org/gordon/fast.pdf中所定义的),我可以将单次乘法所需的时间从73秒降至12.3秒,在Firefox 3.0中。这似乎是我能做到的最好,但仍然太慢了。
更新:Flash Player中的ActionScript 2(AS2)解释器不值得使用,因为它似乎比Firefox 3.0中的JavaScript解释器更慢:对于Flash Player 9,它似乎比后者慢4.2倍,对于Flash Player 10,它似乎比后者慢2.35倍。有人知道ActionScript2和ActionScript3(AS3)在数字处理方面的速度差异吗?
更新:Flash Player 9中的ActionScript 3(AS3)解释器不值得使用,因为它的速度几乎与Firefox 3.0中的JavaScript相同。
更新:Flash Player 10中的ActionScript 3(AS3)解释器,如果使用int而不是Number,并且使用Vector.而不是Array,则比Firefox 3.0中的JavaScript解释器快高达6.5倍。至少在2048位大整数乘法方面,速度提高了2.41倍。因此,如果可以,在AS3中执行模指数运算并在Flash Player 10中执行可能是值得的。请注意,这仍然比Google Chrome的JavaScript解释器V8慢。有关各种编程语言和JavaScript实现的速度比较,请参见http://ptspts.blogspot.com/2009/10/javascript-and-actionscript-performance.html
更新:有一个非常快的Java解决方案,如果安装了Java插件,则可以从浏览器的JavaScript中调用它。以下解决方案比使用BigInt的纯JavaScript实现快约310倍。
<body>hi0
<script type="text/javascript">
document.body.innerHTML += '<br>hi1';
if ('object'==typeof java) {
  var x = new java.math.BigInteger("123456789123456789", 10);
  var p = new java.math.BigInteger("234567891234567891", 10);
  var g = new java.math.BigInteger("3", 10);
  var v = x.modPow(x, p);
  document.body.innerHTML += '<br>' + v.toString();
  document.body.innerHTML += '<br>' + v.toString(16);
} else {
  document.body.innerHTML += '<br>java plugin not installed';
}
</script></body>

有人能将这段代码翻译成Silverlight(C#)吗?


1
这个问题最初是针对哪个CPU系列的?“Firefox 3.0”是单线程的,缺乏任何本地加速的BigInt支持;我会假设相关的机器是英特尔Cedar Mill / AMD Windsor或类似的处理器?我很想看到这个问题最初提出的解决方案:SpiderMonkey 1.8是否有可能在该硬件上及时评估该数学问题。 - JamesTheAwesomeDude
@JamesTheAwesomeDude:我记不起来这些细节了。也许最好查一下Firefox 3.0源代码,看看它的JavaScript引擎如何进行32位整数运算。可能加速效果不是很明显。 - pts
5个回答

4

我使用 "%" 表示模运算 (mod),使用 "/" 表示整数除法。函数 f(p,g,x,r) 的作用是在满足 r<p 和 g<p 的条件下计算 (r*g^x)%p。f() 可以实现为:

bigint_t f(p,g,x,r) {
  bigint_t i, z = g, y;
  for (i = 1; i < x; ++i) {
    y = z; z *= g;
    if (z > p) break;
  }
  if (i >= x - 1) return r*z%p; // g^x*r%p = g^x*r
  else return f(p,y,x/i,g^(x%i)*r%p); // reduce to (r*g^(x%i)%p)*(g^i)^(x/i)%p
}

这个例程需要进行更多的计算,但每个整数都小于4096位,通常比g^x要小得多。我认为这可能比直接计算更有效率。还要注意,因为我们已经计算了g^(i+1),所以g^(x%i)可以以更快的方式计算。

编辑:请参见此帖子。Mehrdad给出了正确(且更好)的解决方案。


你的实现对于 f(100, 3, 8, 1) 给出了无限递归,而不是返回 61。你的算法有一个合适的名称吗? - pts
抱歉,那里有一个小错误。我已经更改了它。这个方法只是简单数学的结果,太简单了没有名字。 - user172818
该方法基于观察到的规律:(k*p+g)^x%p = g^x%p。它反复应用这个规则来避免直接计算g^x。 - user172818
当递归调用f时,如果x/i > 2,则f将无限递归,因为y*y > p,所以递归调用中的循环将以i=1退出。尝试f(100,5,8,1)(实际上不要尝试,因为您将不得不关闭浏览器)。还要注意i==floor(log(p)/log(g))。根据指数和对数的实现方式,使用i=floor(log(p)/log(g)); y=g^i可能比循环更快。一旦解决了递归问题,最好情况下f将是对数级别的,但最坏情况下将是x的平方根。通过平方取幂(BigInt使用)在所有情况下都是对数级别的。 - outis

4

如果可从JS调用的其他客户端技术(如Java小程序或Flash电影)可接受,那么这些技术是否可以使用?Leemon的实现(基于蒙哥马利约简)已经相当快了。您可以调整BigInt,也可以尝试不同的算法,但您可能不会得到数量级的提升。


我必须确认 BigInt 已经被优化得非常好了。我曾尝试使用 Karatsuba 算法来实现乘法,但它比 BigInt 的简单 O(n^2) 乘法慢了4倍。 - pts
谢谢您提到这篇论文,它看起来很有前途。 - pts
使用您提供的文章中介绍的技术,结合 BigInt.js 库,我可以在 Firefox 3.0 上将 2048 位整数的模乘法速度提高 6 倍,在 Google Chrome 上提高 4 倍。不幸的是,这对我来说仍然太慢了,因此我必须寻找一个需要更少计算量的不同加密协议。 - pts
我想给这个答案更多的赞,因为我发现链接的文章非常有用。但是我不能把它接受为答案,因为它还不够快。 - pts

2
为什么不使用更适合的语言(例如C)在服务器端创建一个Web服务来执行此操作?这样,所需时间为来回一次(小于9秒),再加上服务���使用本地代码中的BigInt库计算结果的时间。这很可能会更快。

2
您可能不想将您的私钥发送到服务器。 - Steve Gilham
3
谁说过私钥了? - 1800 INFORMATION
使用C语言和GMP库,速度大约快了1042倍。但是在我的问题中,使用不同的编程语言或将数字发送到服务器都不是一个选项。 - pts
那么,我猜测把它们放入Silverlight以进行大量计算也是不可能的了,是吗? - Dan F
我不想依赖Silverlight。但是如果浏览器中有bigint或Java的bigint可用,那么使用它们可能是可行的。但在这个问题中,我需要一个在JavaScript中实现快速模幂运算的解决方案。 - pts

2

1
最初提出的问题已经不适用了,但是对于搜索模块指数算法的JavaScript程序员来说,这个线程仍然在Google上排名靠前。
有多种方法可以实现它,从幼稚的乘-减循环到位运算魔法;这里介绍一种基于所谓的“二进制右到左方法”的方法,它在性能和可读性参考实现之间找到了平衡点。
function bn_powMod(a, e, m) {
  // h/t https://umaranis.com/2018/07/12/calculate-modular-exponentiation-powermod-in-javascript-ap-n/
  if (m === 1n)
    return 0n;
  if (e < 0n)
    return bn_powMod(bn_modInv(a, m), -e, m);
  for (var b = 1n; e; e >>= 1n) {
    if (e % 2n === 1n)
      b = (b * a) % m;
    a = (a * a) % m;
  }
  return b;
}

function bn_modInv(a, m) {
  // h/t https://github.com/python/cpython/blob/v3.8.0/Objects/longobject.c#L4184
  const m0 = m;
  var b = 1n, c = 0n, q, r;
  while (m) {
    [q, r] = [a/m, a%m];
    [a, b, c, m] = [m, c, b - q*c, r];
  }
  if (a !== 1n)
    throw new RangeError("Not invertible");
  if (b < 0n)
    b += m0;
  return b;
}

如果您正在寻找真正高效的解决方案,可以尝试使用斯坦福大学的sjcl.bn.montpowermodnpm的modular-powerPomcor的pjclMontExp;这三种选择中,最后一种不是基于ES6 BigInts设计的,显然是在有限制条件下设计的高性能解决方案,因此即使在性能较弱的旧版浏览器上也可能成为候选方案。
如果您找到了这个帖子,因为您正在编写基于浏览器的RSA加密,请记住存在时间攻击和无限多的实现攻击,并考虑使用更预打包的解决方案

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