[英]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_INTEGER
是2^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-isqrt和bigint-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.