JavaScript创建百万级对象性能优化:Monty Hall游戏模拟问询
嘿,这是个挺有意思的性能优化问题!针对Monty Hall模拟程序的超大次数运行限制,我来分享几个实用的优化思路,应该能帮你突破500万的瓶颈:
1. 放弃存储所有游戏对象,直接统计结果(优先级最高)
你的原代码会把每一局游戏的详细数据都存在数组里,这对内存来说是巨大的负担——500万对象的内存占用轻松就能达到几百MB甚至GB级别,直接导致卡顿或崩溃。而实际上,模拟Monty Hall游戏的核心需求应该是统计切换/不切换策略的胜率,而非保留每一局的细节。
我们可以简化逻辑,直接在循环里计算每局的胜负结果,累加统计值,完全不用保存任何游戏对象:
const simulateGames = (totalCount, shouldSwitch) => { let winCount = 0; for (let i = 0; i < totalCount; i++) { const correctDoor = Math.floor(Math.random() * 3); const playerPick = Math.floor(Math.random() * 3); // 利用Monty Hall的数学逻辑简化判断: // 切换获胜的条件是初始选择错误,不切换则是初始选择正确 winCount += shouldSwitch ? (playerPick !== correctDoor) : (playerPick === correctDoor); } return { total: totalCount, wins: winCount, winRate: winCount / totalCount }; };
这种方式的内存占用几乎可以忽略不计,别说500万,就算模拟1亿次也不会爆内存。
2. 替换Math.random()为更快的伪随机生成器(PRNG)
Math.random()虽然方便,但底层调用有一定开销,在超大循环里重复调用会累积性能损耗。我们可以自己实现一个轻量的伪随机生成器,比如线性同余生成器(LCG),速度会快很多:
class FastPRNG { constructor(seed = Date.now()) { // 初始化种子,保证在合法范围内 this.seed = seed % 0x7FFFFFFF; if (this.seed <= 0) this.seed += 0x7FFFFFFF; } // 生成[0, max)范围内的整数 nextInt(max) { this.seed = (this.seed * 16807) % 0x7FFFFFFF; return Math.floor(((this.seed - 1) / 0x7FFFFFFF) * max); } } // 修改后的模拟函数 const simulateGames = (totalCount, shouldSwitch) => { const prng = new FastPRNG(); let winCount = 0; for (let i = 0; i < totalCount; i++) { const correctDoor = prng.nextInt(3); const playerPick = prng.nextInt(3); winCount += shouldSwitch ? (playerPick !== correctDoor) : (playerPick === correctDoor); } return { total: totalCount, wins: winCount, winRate: winCount / totalCount }; };
这个PRNG的nextInt方法比Math.floor(Math.random() * 3)快不少,在超大循环里能节省可观的时间。
3. 分块处理+多线程/Worker避免主线程阻塞
如果是在浏览器环境,一次性跑几百万次循环会阻塞事件循环,导致页面卡死;在Node.js里,单线程也会被长时间占用,无法处理其他任务。解决办法是分块处理,并利用多线程(Node.js的worker_threads)或Web Worker来并行计算:
浏览器端用Web Worker分块示例:
// worker.js(单独的文件) class FastPRNG { constructor(seed) { this.seed = seed % 0x7FFFFFFF; if (this.seed <= 0) this.seed += 0x7FFFFFFF; } nextInt(max) { this.seed = (this.seed * 16807) % 0x7FFFFFFF; return Math.floor(((this.seed - 1) / 0x7FFFFFFF) * max); } } self.onmessage = (e) => { const { count, shouldSwitch, seed } = e.data; const prng = new FastPRNG(seed); let wins = 0; for (let i = 0; i < count; i++) { const correctDoor = prng.nextInt(3); const playerPick = prng.nextInt(3); wins += shouldSwitch ? (playerPick !== correctDoor) : (playerPick === correctDoor); } self.postMessage({ wins }); };
// 主线程代码 const runLargeSimulation = async (totalCount, shouldSwitch) => { const chunkSize = 100000; // 每块处理10万次 const chunks = Math.ceil(totalCount / chunkSize); let totalWins = 0; for (let i = 0; i < chunks; i++) { const currentChunk = Math.min(chunkSize, totalCount - i * chunkSize); const worker = new Worker('worker.js'); await new Promise(resolve => { worker.onmessage = (e) => { totalWins += e.data.wins; worker.terminate(); resolve(); }; // 给每个worker分配不同的种子,保证随机性 worker.postMessage({ count: currentChunk, shouldSwitch, seed: Date.now() + i }); }); // 可以在这里更新页面进度 console.log(`完成 ${((i+1)/chunks)*100}%`); } return { total: totalCount, wins: totalWins, winRate: totalWins / totalCount }; };
Node.js端用worker_threads示例:
类似浏览器的Worker逻辑,通过多线程并行处理不同的块,充分利用CPU多核性能,大幅提升模拟速度。
4. 若必须保留游戏数据,用流式写入替代内存存储
如果你的需求确实需要保留每一局的详细数据,绝对不能把所有对象存在数组里再序列化。应该用流式写入的方式,每生成一局数据就写入文件,避免内存堆积:
// Node.js环境下的流式写入示例 const fs = require('fs'); const { createWriteStream } = fs; const simulateAndStream = (totalCount, outputPath) => { const stream = createWriteStream(outputPath, { flags: 'w' }); for (let i = 0; i < totalCount; i++) { const correctDoor = Math.floor(Math.random() * 3); const playerPick = Math.floor(Math.random() * 3); // 计算被主持人排除的门 let eliminatedDoor; for (let j = 0; j < 3; j++) { if (j !== correctDoor && j !== playerPick) { eliminatedDoor = j; break; } } // 构造游戏数据 const gameData = { 0: correctDoor === 0 ? 1 : null, 1: correctDoor === 1 ? 1 : null, 2: correctDoor === 2 ? 1 : null, pick: playerPick, correctpick: correctDoor, eliminated: eliminatedDoor }; // 写入流(每行一个JSON对象,即JSON Lines格式) stream.write(JSON.stringify(gameData) + '\n'); // 每10万次刷新一次缓冲区,避免内存占用过高 if (i % 100000 === 0) { stream.flush(); } } stream.end(() => { console.log('所有数据已写入文件'); }); };
这种方式的内存占用始终保持在很低的水平,不管模拟多少次都不会崩溃。
内容的提问来源于stack exchange,提问作者simon

