为什么第一个函数调用的执行速度比所有其他顺序调用快两倍?

2023-12-20

我有一个自定义 JS 迭代器实现和用于测量后一个实现的性能的代码:

const ITERATION_END = Symbol('ITERATION_END');

const arrayIterator = (array) => {
  let index = 0;

  return {
    hasValue: true,
    next() {
      if (index >= array.length) {
        this.hasValue = false;

        return ITERATION_END;
      }

      return array[index++];
    },
  };
};

const customIterator = (valueGetter) => {
  return {
    hasValue: true,
    next() {
      const nextValue = valueGetter();

      if (nextValue === ITERATION_END) {
        this.hasValue = false;

        return ITERATION_END;
      }

      return nextValue;
    },
  };
};

const map = (iterator, selector) => customIterator(() => {
  const value = iterator.next();

  return value === ITERATION_END ? value : selector(value);
});

const filter = (iterator, predicate) => customIterator(() => {
  if (!iterator.hasValue) {
    return ITERATION_END;
  }

  let currentValue = iterator.next();

  while (iterator.hasValue && currentValue !== ITERATION_END && !predicate(currentValue)) {
    currentValue = iterator.next();
  }

  return currentValue;
});

const toArray = (iterator) => {
  const array = [];

  while (iterator.hasValue) {
    const value = iterator.next();

    if (value !== ITERATION_END) {
      array.push(value);
    }
  }

  return array;
};

const test = (fn, iterations) => {
  const times = [];

  for (let i = 0; i < iterations; i++) {
    const start = performance.now();
    fn();
    times.push(performance.now() - start);
  }

  console.log(times);
  console.log(times.reduce((sum, x) => sum + x, 0) / times.length);
}

const createData = () => Array.from({ length: 9000000 }, (_, i) => i + 1);

const testIterator = (data) => () => toArray(map(filter(arrayIterator(data), x => x % 2 === 0), x => x * 2))

test(testIterator(createData()), 10);

测试函数的输出非常奇怪且出乎意料 - 第一个测试运行的执行速度始终比所有其他运行快两倍。结果之一,其中数组包含所有执行时间,数字是平均值(我在 Node 上运行它):

[
  147.9088459983468,
  396.3472499996424,
  374.82447600364685,
  367.74555300176144,
  363.6300039961934,
  362.44370299577713,
  363.8418449983001,
  390.86111199855804,
  360.23125199973583,
  358.4788999930024
]
348.6312940984964

使用 Deno 运行时可以观察到类似的结果,但是我无法在其他 JS 引擎上重现此行为。 V8 出现这种情况的原因是什么?

环境: 节点 v13.8.0、V8 v7.9.317.25-node.28、 Deno v1.3.3、V8 v8.6.334


(这里是 V8 开发人员。)简而言之:它是内联还是缺乏内联,由引擎启发式决定。

对于优化编译器来说,内联被调用的函数可以带来显着的好处(例如:避免调用开销,有时使常量折叠成为可能,或消除重复计算,有时甚至为额外内联创造新的机会),但需要付出代价:使编译本身变慢,并且由于某些假设不成立而增加了稍后不得不丢弃优化代码(“取消优化”)的风险。内联什么都不会浪费性能,内联所有东西都会浪费性能,内联正确的函数需要能够预测程序的未来行为,这显然是不可能的。因此编译器使用启发式方法。

目前,V8 的优化编译器仅在特定位置调用的函数始终相同时才会对内联函数进行启发式处理。在本例中,这是第一次迭代的情况。随后的迭代会创建新的闭包作为回调,从 V8 的角度来看,它们是新函数,因此它们不会被内联。 (V8 实际上知道一些高级技巧,允许它在某些情况下消除来自同一源的重复函数实例并内联它们;但在这种情况下,这些技巧不适用 [我不确定为什么])。

所以在第一次迭代中,一切(包括x => x % 2 === 0 and x => x * 2) 被内联到toArray。从第二次迭代开始,情况不再如此,而是生成的代码执行实际的函数调用。

这可能没问题;我猜想在大多数实际应用中,差异几乎无法衡量。 (减少测试用例往往会使这种差异更加突出;但是根据小测试的观察结果更改较大应用程序的设计通常并不是最有效的消磨时间的方式,最坏的情况下可能会让事情变得更糟。)

此外,手动优化引擎/编译器的代码是一个困难的平衡。我一般会推荐not这样做(因为引擎会随着时间的推移而改进,而让你的代码变得更快确实是他们的工作);另一方面,显然存在效率更高的代码和效率较低的代码,为了最大程度地提高整体效率,每个参与人员都需要尽自己的一份力量,也就是说,您最好尽可能让引擎的工作变得更简单。

如果您确实想微调其性能,可以通过分离代码和数据来实现,从而确保始终调用相同的函数。例如,您的代码的修改版本如下:

const ITERATION_END = Symbol('ITERATION_END');

class ArrayIterator {
  constructor(array) {
    this.array = array;
    this.index = 0;
  }
  next() {
    if (this.index >= this.array.length) return ITERATION_END;
    return this.array[this.index++];
  }
}
function arrayIterator(array) {
  return new ArrayIterator(array);
}

class MapIterator {
  constructor(source, modifier) {
    this.source = source;
    this.modifier = modifier;
  }
  next() {
    const value = this.source.next();
    return value === ITERATION_END ? value : this.modifier(value);
  }
}
function map(iterator, selector) {
  return new MapIterator(iterator, selector);
}

class FilterIterator {
  constructor(source, predicate) {
    this.source = source;
    this.predicate = predicate;
  }
  next() {
    let value = this.source.next();
    while (value !== ITERATION_END && !this.predicate(value)) {
      value = this.source.next();
    }
    return value;
  }
}
function filter(iterator, predicate) {
  return new FilterIterator(iterator, predicate);
}

function toArray(iterator) {
  const array = [];
  let value;
  while ((value = iterator.next()) !== ITERATION_END) {
    array.push(value);
  }
  return array;
}

function test(fn, iterations) {
  for (let i = 0; i < iterations; i++) {
    const start = performance.now();
    fn();
    console.log(performance.now() - start);
  }
}

function createData() {
  return Array.from({ length: 9000000 }, (_, i) => i + 1);
};

function even(x) { return x % 2 === 0; }
function double(x) { return x * 2; }
function testIterator(data) {
  return function main() {
    return toArray(map(filter(arrayIterator(data), even), double));
  };
}

test(testIterator(createData()), 10);

观察热路径上如何不再动态创建函数以及“公共接口”(即arrayIterator, map, filter, and toArraycompose)与以前完全相同,只是底层细节发生了变化。给所有函数命名的好处是您可以获得更有用的分析输出;-)

精明的读者会注意到,这种修改只会转移问题:如果代码中有多个地方调用map and filter使用不同的修饰符/谓词,那么内联性问题将再次出现。正如我上面所说:微基准往往会产生误导,因为真正的应用程序通常有不同的行为......

(FWIW,这与为什么这个函数调用的执行时间会改变? https://stackoverflow.com/questions/62704854/why-is-the-execution-time-of-this-function-call-changing .)

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

为什么第一个函数调用的执行速度比所有其他顺序调用快两倍? 的相关文章

随机推荐

  • 将日期和时间转换为 Unix 时间戳

    我像这样显示日期和时间 2009 年 11 月 24 日 17 57 35 我想将它转换为 unix 时间戳 这样我就可以轻松地操作它 我需要使用正则表达式来匹配字符串的每个部分 然后从中计算出 unix 时间戳 我对正则表达式很糟糕 但我
  • 使用 Java 读取 HTML+JavaScript

    我可以通过http读取HTML内容 例如 http www foo com http www foo com 使用 Java 使用 URL 和 BufferedReader 类 然而 其中一些包含 JavaScript 我当前的应用程序无法
  • HTML5 拖放行为

    我广泛使用了 HTML5 原生拖放功能 它几乎完全可以正常运行 只有一个小例外 当任何东西被拖过页面时 我试图突出显示我的拖放区 我最初尝试通过将 jQuery 侦听器放在文档正文上来实现此目的 如下所示 body live dragove
  • 使用 python-can 时出现 AttributeError (模块“can”没有属性“interface”)

    运行以下代码时出现错误 import can importing CAN module import time bus1 can interface Bus bustype vector channel 0 bitrate 500000 a
  • 将字符串保存为html文件android

    我的问题很简单 如何将字符串 HTML 保存为内部存储中的 html 文件 请让我知道如何执行此操作 可以说我有的字符串是 string html This is random text 试试这个 private void saveHtml
  • 检查 GPS 经纬度点是否位于 Google 地图中的道路上

    我正在 Android 中开发一个导航应用程序 我想在用户越野时提示他 那么 如果我有 GPS 纬度 经度点 是否可以确定该点是否位于道路上 是否有 API 支持此类检查 一般而言 这是在所有道路上完成的 而不是在具有指定路径 折线的情况下
  • 应用内购买“准备提交”,但不允许我提交

    我有一些应用程序内购买设置 应用内购买都经过测试 并且运行良好 但我无法提交它们进行审核 我提交了应用程序二进制文件以供审核 然后访问了应用内购买部分 所有应用内购买均显示 准备提交 但 提交审核 按钮呈灰色且不可点击 当苹果审查实际应用程
  • 是否有 JNDI 命名空间约定?

    我已经下载了 JBoss EAP 6 1 我将添加一个新的数据源 我必须将数据源绑定到 JNDI 名称 通过读取示例数据源的 JNDI 名称 它是 java jboss datasources ExampleDS 我看到他们用过dataso
  • PHP 执行时间导致响应延迟[关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 我故意在我的服务器上运行一个持续长达 60 秒的 php 脚本 每次运行之间都有延迟 问题是 在脚本执行期间发送到服务器的任何其他请求都会
  • CMake:如何在 CMakeLists.txt 中使用 bash 命令

    我想知道如何在 CMakeLists txt 中使用 bash 命令 我想要的是使用以下命令检索处理器的数量 export variable getconf NPROCESSORS ONLN 并使用类似以下内容将 NB PROCESSOR
  • 具有相同对象名称的两个命名空间

    我有两个命名空间 System Numerics 和 UnityEngine 两者都有类型Vector3 所以现在当我想使用它时我必须在它之前声明哪个名称空间 像这样 protected struct CVN public Complex
  • Angular 6 自定义元素在 IE11 和 Firefox 上失败,出现语法和影子 dom 错误

    我创建了一个新的 angular cli 项目 其中使用自定义元素这些方向 https medium com tomsu building web components with angular elements 746cd2a38d5b
  • Java有using语句吗?

    Java有没有可以在hibernate中打开会话时使用的using语句 在 C 中 它类似于 using var session new Session 因此该对象超出范围并自动关闭 Java 7 推出自动资源块管理 http www in
  • 如何在.net core mvc视图中缩小?

    在我用 net core mvc制作的网站上 当我们打开网站并点击页面源代码视图时 如何将第二张图所示的长代码以缩小的形式进行处理 将 Web Markup Min 添加到 ASP NET Core 应用程序 WebMarkupMin是一个
  • 如何从 NSDate 计算年龄

    我正在开发一个应用程序 需要根据某人的生日查找其年龄 我有一个简单的NSDate但我怎么找到这个NSDateFormatter NSInteger ageFromBirthday NSDate birthdate NSDate today
  • 使用类型参数与抽象类型实现类型类

    继从见证抽象类型实现类型类 https stackoverflow com questions 64399785 witness that an abstract type implements a typeclass 64401748 n
  • 在模板函数返回类型上使用 std::enable_if 来利用 SFINAE - 编译错误

    下面的代码 include
  • iPhone:strace、dtruss、dtrace 或同等工具?

    有谁知道是否有类似的东西strace dtruss or dtrace对于iPhone tester iPhone tmp root apt cache search dtruss tester iPhone tmp root apt ca
  • PHP:检查谁阅读了发送的电子邮件?

    我正在向某些用户发送电子邮件 并想知道谁读过它 这意味着如果有人读过该电子邮件 那么将维护一个日志文件 其中包含该用户的电子邮件地址以及日期 时间 IP 为此 我发送一个带有电子邮件 html 模板 的 javascript 函数 当用户打
  • 为什么第一个函数调用的执行速度比所有其他顺序调用快两倍?

    我有一个自定义 JS 迭代器实现和用于测量后一个实现的性能的代码 const ITERATION END Symbol ITERATION END const arrayIterator array gt let index 0 retur