Dav*_*ret 13 javascript generator ecmascript-6
两个功能的故事
我有一个函数填充数组到指定的值:
function getNumberArray(maxValue) {
const a = [];
for (let i = 0; i < maxValue; i++) {
a.push(i);
}
return a;
}
Run Code Online (Sandbox Code Playgroud)
和一个类似的生成器函数,而不是产生每个值:
function* getNumberGenerator(maxValue) {
for (let i = 0; i < maxValue; i++) {
yield i;
}
}
Run Code Online (Sandbox Code Playgroud)
测试跑步者
我已经为这两种情况编写了这个测试:
function runTest(testName, numIterations, funcToTest) {
console.log(`Running ${testName}...`);
let dummyCalculation;
const startTime = Date.now();
const initialMemory = process.memoryUsage();
const iterator = funcToTest(numIterations);
for (let val of iterator) {
dummyCalculation = numIterations - val;
}
const finalMemory = process.memoryUsage();
// note: formatNumbers can be found here: https://jsfiddle.net/onz1ozjq/
console.log(formatNumbers `Total time: ${Date.now() - startTime}ms`);
console.log(formatNumbers `Rss: ${finalMemory.rss - initialMemory.rss}`);
console.log(formatNumbers `Heap Total: ${finalMemory.heapTotal - initialMemory.heapTotal}`);
console.log(formatNumbers `Heap Used: ${finalMemory.heapUsed - initialMemory.heapUsed}`);
}
Run Code Online (Sandbox Code Playgroud)
运行测试
然后在运行这两个时:
const numIterations = 999999; // 999,999
console.log(formatNumbers `Running tests with ${numIterations} iterations...\n`);
runTest("Array test", numIterations, getNumberArray);
console.log("");
runTest("Generator test", numIterations, getNumberGenerator);
Run Code Online (Sandbox Code Playgroud)
我得到的结果与此类似:
Running tests with 999,999 iterations...
Running Array test...
Total time: 105ms
Rss: 31,645,696
Heap Total: 31,386,624
Heap Used: 27,774,632
Running Function generator test...
Total time: 160ms
Rss: 2,818,048
Heap Total: 0
Heap Used: 1,836,616
Run Code Online (Sandbox Code Playgroud)
注意:我在Windows 8.1上的节点v4.1.1上运行这些测试.我没有使用转换器,而是通过操作来运行它node --harmony generator-test.js.
题
显然,期望使用数组增加内存使用量......但为什么我一直在为数组获得更快的结果?是什么导致这里的放缓?产量只是一项昂贵的操作吗?或者也许我正在做的方法来检查这个?
JDB*_*JDB 13
虽然 JS 引擎作者将继续努力提高性能,但有一些潜在的结构现实实际上保证了,对于 OP 的测试用例,构建数组总是比使用迭代器更快。
\n考虑生成器函数返回一个生成器对象。
\n根据定义,生成器对象将具有一个next() function,并且在 Javascript 中调用函数意味着向调用堆栈添加一个条目。虽然这很快,但它可能永远不会像直接属性访问那么快。(至少,直到奇点到来。)
因此,如果要迭代集合中的每个元素,则通过直接属性访问访问元素的简单数组上的 for 循环总是比迭代器上的 for 循环(访问每个元素)更快通过调用该next()函数。
当我在 2022 年 1 月写这篇文章时,运行 Chrome 97,生成器函数比使用 OP 示例的数组函数慢 60%。
\n不难想象发电机会更快的场景。数组函数的主要缺点是,无论您是否需要所有元素,它都必须先构建整个集合,然后代码才能开始迭代元素。
\n考虑一个基本的搜索操作,它平均只能访问集合的一半元素。在这种情况下,数组函数暴露了它的“致命弱点”:它必须构建一个包含所有结果的数组,即使有一半结果永远不会被看到。这就是生成器有可能远远超过数组函数的地方。
\n为了演示这一点,我稍微修改了 OP 的用例。我使数组元素的计算成本稍微高一些(使用一点除法和平方根逻辑),并修改循环以在大约中间标记处终止(以模拟基本搜索)。
\n设置
\nfunction getNumberArray(maxValue) {\n const a = [];\n\n for (let i = 0; i < maxValue; i++) {\n const half = i / 2;\n const double = half * 2;\n const root = Math.sqrt(double);\n const square = Math.round(root * root);\n a.push(square);\n }\n\n return a;\n}\n\nfunction* getNumberGenerator(maxValue) {\n for (let i = 0; i < maxValue; i++) {\n const half = i / 2;\n const double = half * 2;\n const root = Math.sqrt(double);\n const square = Math.round(root * root);\n yield square;\n }\n}\nlet dummyCalculation;\nconst numIterations = 99999;\nconst searchNumber = numIterations / 2;\nRun Code Online (Sandbox Code Playgroud)\n发电机
\nconst iterator = getNumberGenerator(numIterations);\nfor (let val of iterator) {\n dummyCalculation = numIterations - val;\n if (val > searchNumber) break;\n}\nRun Code Online (Sandbox Code Playgroud)\n大批
\nconst iterator = getNumberArray(numIterations);\nfor (let val of iterator) {\n dummyCalculation = numIterations - val;\n if (val > searchNumber) break;\n}\nRun Code Online (Sandbox Code Playgroud)\n有了这段代码,这两种方法就旗鼓相当了。经过反复的测试运行,生成器和数组函数交换了第一和第二的位置。不难想象,如果数组元素的计算成本更高(例如,克隆复杂对象、进行 REST 调用等),那么生成器将轻松获胜。
\n虽然认识到OP的问题具体是关于性能的,但我认为值得指出的是,生成器函数最初并不是作为循环数组的更快替代方案而开发的。
\nOP 已经承认了这一点,但内存效率是生成器相对于构建数组提供的主要优势之一。生成器可以动态构建对象,然后在不再需要时丢弃它们。在最理想的实现中,生成器一次只需在内存中保存一个对象,而数组必须同时保存所有对象。
\n对于内存非常密集的集合,生成器将允许系统根据需要构建对象,然后在调用代码移至下一个元素时回收该内存。
\n生成器不必解析整个集合,这可以释放它们来表示可能不完全存在于内存中的集合。
\n例如,生成器可以表示集合,其中获取“下一个”项目的逻辑是耗时的(例如对数据库查询的结果进行分页,其中项目是批量获取的)或状态相关的(例如迭代对当前项目的操作影响“下一个”项目的集合)甚至无限系列(例如分形函数、随机数生成器或返回 \xcf\x80 所有数字的生成器)。在这些场景中,构建阵列要么不切实际,要么不可能。
\n人们可以想象一个生成器,它根据种子数返回程序生成的游戏关卡数据,甚至代表理论上人工智能的“意识流”(例如,玩单词联想游戏)。这些都是有趣的场景,无法使用标准数组或列表来表示,但循环结构在代码中可能感觉更自然。
\n非常不满意的答案可能是这样的:你的ES5功能依赖于自2008年发布以来已经在V8中使用的功能(除了let和之外const)(可能有一段时间之前,据我所知,成为V8的部分起源于Google的网络抓取工具).另一方面,发电机自2013年以来仅在V8发电.因此,ES5代码不仅需要七年时间才能进行优化,而ES6代码只有两个,几乎没有人(与使用代码的数百万个站点相比,就像你的ES5代码一样)在V8中使用了生成器,这意味着几乎没有机会发现或激励实施优化.
如果你真的想要一个关于为什么Node.js中的生成器相对较慢的技术答案,你可能不得不自己深入研究V8源代码,或者询问编写它的人.
仅供参考,这个问题在互联网术语中很古老,并且生成器已经赶上了(至少在 Chrome 中测试时)https://jsperf.com/generator-vs-loops1