为什么在这个例子中使用生成器函数比填充和迭代数组慢?

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

在OP的例子中,生成器总是会变慢

\n

虽然 JS 引擎作者将继续努力提高性能,但有一些潜在的结构现实实际上保证了,对于 OP 的测试用例,构建数组总是比使用迭代器更快。

\n

考虑生成器函数返回一个生成器对象

\n

根据定义,生成器对象将具有一个next() function,并且在 Javascript 中调用函数意味着向调用堆栈添加一个条目。虽然这很快,但它可能永远不会像直接属性访问那么快。(至少,直到奇点到来。)

\n

因此,如果要迭代集合中的每个元素,则通过直接属性访问访问元素的简单数组上的 for 循环总是比迭代器上的 for 循环(访问每个元素)更快通过调用该next()函数。

\n

当我在 2022 年 1 月写这篇文章时,运行 Chrome 97,生成器函数比使用 OP 示例的数组函数慢 60%

\n

性能取决于用例

\n

不难想象发电机会更快的场景。数组函数的主要缺点是,无论您是否需要所有元素,它都必须先构建整个集合,然后代码才能开始迭代元素。

\n

考虑一个基本的搜索操作,它平均只能访问集合的一半元素。在这种情况下,数组函数暴露了它的“致命弱点”:它必须构建一个包含所有结果的数组,即使有一半结果永远不会被看到。这就是生成器有可能远远超过数组函数的地方。

\n

为了演示这一点,我稍微修改了 OP 的用例。我使数组元素的计算成本稍微高一些(使用一点除法和平方根逻辑),并修改循环以在大约中间标记处终止(以模拟基本搜索)。

\n

设置

\n
function 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;\n
Run Code Online (Sandbox Code Playgroud)\n

发电机

\n
const iterator = getNumberGenerator(numIterations);\nfor (let val of iterator) {\n    dummyCalculation = numIterations - val;\n    if (val > searchNumber) break;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

大批

\n
const iterator = getNumberArray(numIterations);\nfor (let val of iterator) {\n    dummyCalculation = numIterations - val;\n    if (val > searchNumber) break;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

有了这段代码,这两种方法就旗鼓相当了。经过反复的测试运行,生成器和数组函数交换了第一和第二的位置。不难想象,如果数组元素的计算成本更高(例如,克隆复杂对象、进行 REST 调用等),那么生成器将轻松获胜。

\n

超越性能的考虑

\n

虽然认识到OP的问题具体是关于性能的,但我认为值得指出的是,生成器函数最初并不是作为循环数组的更快替代方案而开发的。

\n

内存效率

\n

OP 已经承认了这一点,但内存效率是生成器相对于构建数组提供的主要优势之一。生成器可以动态构建对象,然后在不再需要时丢弃它们。在最理想的实现中,生成器一次只需在内存中保存一个对象,而数组必须同时保存所有对象。

\n

对于内存非常密集的集合,生成器将允许系统根据需要构建对象,然后在调用代码移至下一个元素时回收该内存。

\n

非静态集合的表示

\n

生成器不必解析整个集合,这可以释放它们来表示可能不完全存在于内存中的集合。

\n

例如,生成器可以表示集合,其中获取“下一个”项目的逻辑是耗时的(例如对数据库查询的结果进行分页,其中项目是批量获取的)或状态相关的(例如迭代对当前项目的操作影响“下一个”项目的集合)甚至无限系列(例如分形函数、随机数生成器或返回 \xcf\x80 所有数字的生成器)。在这些场景中,构建阵列要么不切实际,要么不可能。

\n

人们可以想象一个生成器,它根据种子数返回程序生成的游戏关卡数据,甚至代表理论上人工智能的“意识流”(例如,玩单词联想游戏)。这些都是有趣的场景,无法使用标准数组或列表来表示,但循环结构在代码中可能感觉更自然。

\n


Jor*_*ing 6

非常不满意的答案可能是这样的:你的ES5功能依赖于自2008年发布以来已经在V8中使用的功能(除了let和之外const)(可能有一段时间之前,据我所知,成为V8的部分起源于Google的网络抓取工具).另一方面,发电机自2013年以来仅在V8发电.因此,ES5代码不仅需要七年时间才能进行优化,而ES6代码只有两个,几乎没有人(与使用代码的数百万个站点相比,就像你的ES5代码一样)在V8中使用了生成器,这意味着几乎没有机会发现或激励实施优化.

如果你真的想要一个关于为什么Node.js中的生成器相对较慢的技术答案,你可能不得不自己深入研究V8源代码,或者询问编写它的人.

  • 无法在循环外使用 `let` 重现事物。今天,数组生成和迭代仍然**比简单的生成器快三倍多**(在我在 Windows 7 x64 上使用 Node 10 进行的测试中)。有人把这件事搞砸了。 (2认同)

ins*_*846 5

仅供参考,这个问题在互联网术语中很古老,并且生成器已经赶上了(至少在 Chrome 中测试时)https://jsperf.com/generator-vs-loops1

  • 我的性能测试表明,数组函数的速度仍然是 Chrome 97 中生成器的两倍左右 (https://jsbench.me/8skyaaapbt/1)。这是有道理的,因为生成器函数是更复杂对象的语法糖,它必须在每次迭代时进行函数调用,而数组只有一个初始函数调用。如果循环正在执行提前退出的搜索,则性能概况将发生巨大变化,在这种情况下,生成器可能会优于数组函数,具体取决于构建数组的复杂程度。 (3认同)
  • 即使对于我现在在 Linux 上的 FF 77.0.1 来说,数组也比生成器快约 6.05 倍。 (2认同)