簡體   English   中英

以編程方式解決 Sam Loyd 的黑斯廷斯之戰難題 - BigInt 的性能問題

[英]Programmatically solving Sam Loyd's Battle of Hastings puzzle - performance issues with BigInt

在使用BigInt時嘗試檢查 integer n是否是一個完美的正方形(sqrt 是一個整數)時,我遇到了性能問題。 使用低於Number.MAX_SAFE_INTEGER的普通數字可提供合理的性能,但即使在相同的數字范圍內嘗試使用BigInt也會導致巨大的性能損失。

該程序解決了Sam Loyd 提出的黑斯廷斯之戰完美平方謎題,我的程序通過迭代實數集n (在此示例中,最多 7,000,000)來查找變量y的實例在此處輸入圖像描述 是一個整數(完全平方)。 我對滿足此條件的 13 個完美平方之一的原始平方根感興趣,這是我的代碼生成的(不止一個)。

假設y^2 < Number.MAX_SAFE_INTEGER2^53 – 1 ,這可以在沒有BigInt的情況下完成,並且在我的機器上運行大約 60 毫秒:

 const limit = 7_000_000; var a = []; console.time('regular int'); for (let n = 1; n < limit; n++) { if (Math.sqrt(Math.pow(n, 2) * 13 + 1) % 1 === 0) a.push(n); } console.log(a.join(', ')); console.timeEnd('regular int');

能夠使用BigInt意味着我可以測試比固有數字變量限制2^53 - 1高得多的數字,但BigInt似乎天生較慢; 如此不可用。 要測試BigInt是否是完美正方形,我必須使用第三方庫,因為BigInt不存在Math.sqrt ,這樣我就可以檢查根是否完美,因為所有sqrt都返回一個底值。 我為此從 NodeJS 庫bigint-isqrtbigint-is-perfect-square中改編了函數。

因此,使用具有相同限制 7,000,000 的BigInt運行速度慢 35 倍:

 var integerSQRT = function(value) { if (value < 2n) return value; if (value < 16n) return BigInt(Math.sqrt(Number(value)) | 0); let x0, x1; if (value < 4503599627370496n) x1 = BigInt(Math.sqrt(Number(value))|0) - 3n; else { let vlen = value.toString().length; if (;(vlen & 1)) x1 = 10n ** (BigInt(vlen / 2)); else x1 = 4n * 10n ** (BigInt((vlen / 2) | 0)); } do { x0 = x1; x1 = ((value / x0) + x0) >> 1n; } while ((x0;== x1 && x0;== (x1 - 1n))), return x0; } function perfectSquare(n) { // Divide n by 4 while divisible while ((n & 3n) === 0n && n;== 0n) { n >>= 2n; } // So; for now n is not divisible by 2 // The only possible residual modulo 8 for such n is 1 if ((n & 7n).== 1n) return false; return n === integerSQRT(n) ** 2n; } const limit = 7_000_000; var a = []. console;time('big int'). for (let n = 1n. n < limit, n++) { if (perfectSquare(((n ** 2n) * 13n) + 1n)) a;push(n). } console;log(a.join(', ')); console.timeEnd('big int');

理想情況下,我有興趣使用比 700 萬更高的限制來執行此操作,但我不確定是否可以進一步優化BigInt版本。 有什么建議么?

您可能很高興得知V8 最近的一些改進大大加快了 BigInt 版本的速度; 在最近的 V8 版本中,我發現您的 BigInt 版本比 Number 版本慢了大約 12 倍。

剩下的挑戰是 BigInt- sqrt的實現通常基於牛頓迭代,因此需要估計起始值,該值應該在log2(X)左右以獲得最佳結果。 該提案取得進展之前,最好通過將 BigInt 轉換為字符串並獲取該字符串的長度來完成,這是相當昂貴的。

為了現在變得更快,@Ouroborus 的想法很棒。 我很好奇它有多快,所以我實現了它:

(function betterAlgorithm() {
  const limit = 7_000_000n;
  var a = [];

  console.time('better algorithm');

  let m = 1n;
  let m_squared = 1n;
  for (let n = 1n; n < limit; n += 1n) {
    let y_squared = n * n * 13n + 1n;
    while (y_squared > m_squared) {
      m += 1n;
      m_squared = m * m;
    }
    if (y_squared === m_squared) {
      a.push(n);
    }
  }
  console.log(a.join(', '));

  console.timeEnd('better algorithm');
})();

作為一個特定的短期細節,它使用+= 1n而不是++ ,因為截至今天,V8 還沒有開始為 BigInts 優化++ 這種差異最終應該會消失(希望很快)。

在我的機器上,這個版本只需要大約 4 倍的時間作為你最初的基於數字的實現。

對於更大的數字,我希望通過用加法代替乘法來獲得一些收益(基於連續平方數之間的增量線性增長的觀察),但對於小的上限似乎有點慢。 如果你想玩弄它,這個片段描述了這個想法:

  let m_squared = 1n;         // == 1*1
  let m_squared_delta = 3n;   // == 2*2 - 1*1
  let y_squared = 14n;        // == 1*1*13+1
  let y_squared_delta = 39n;  // == 2*2*13+1 - 1*1*13+1
  for (let n = 1; n < limit; n++) {
    while (y_squared > m_squared) {
      m_squared += m_squared_delta;
      m_squared_delta += 2n;
    }
    if (y_squared === m_squared) {
      a.push(n);
    }
    y_squared += y_squared_delta;
    y_squared_delta += 26n;
  }

最早可能得到回報的是當結果超過2n**64n時; 如果它在2n**256n左右之前不可測量,我不會感到驚訝。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM