【他山之石】优化 JavaScript 的乐趣与价值(下)

news2024/11/15 18:28:13

前言
继本文的 上篇 发表之后,没想到反响还挺好,看来大家在 JS 优化的问题上越来越注重“与国际接轨”了。一起来看本文的下篇,也是干货满满。

文章目录

    • 6. Avoid large objects
        • What the eff should I do about this?
    • 7. Use eval
    • 8. Use strings, carefully
        • What the eff should I do about this?
            • On strings complexity
    • 9. Use specialization
            • Branch prediction and branchless code
    • 10. Data structures
    • 11. Benchmarking
      • 11.0 Start with the top
      • 11.1 Avoid micro-benchmarks
      • 11.2 Doubt your results
      • 11.3 Pick your target
    • 12. Profiling & tools
      • 12.1 Browser gotchas
      • 12.2 Sample vs structural profiling
      • 12.3 The tools of the trade
    • Final notes

6. Avoid large objects

As explained in section 2, engines use shapes to optimize objects. However, when the shape grows too large, the engine has no choice but to use a regular hashmap (like a Map object). And as we saw in section 5, cache misses decrease performance significantly. Hashmaps are prone to this because their data is usually randomly & evenly distributed over the memory region they occupy. Let’s see how it behaves with this map of some users indexed by their ID.

// setup:
const USERS_LENGTH = 1_000
// setup:
const byId = {}
Array.from({ length: USERS_LENGTH }).forEach((_, id) => {
  byId[id] = { id, name: 'John'}
})
let _ = 0
// 1. [] access
Object.keys(byId).forEach(id => { _ += byId[id].id })
// 2. direct access
Object.values(byId).forEach(user => { _ += user.id })

benchmark8

And we can also observe how the performance keeps degrading as the object size grows:

// setup:
const USERS_LENGTH = 100_000

benchmark9

What the eff should I do about this?

As demonstrated above, avoid having to frequently index into large objects. Prefer turning the object into an array beforehand. Organizing your data to have the ID on the model can help, as you can use Object.values() and not have to refer to the key map to get the ID.

7. Use eval

Some javascript patterns are hard to optimize for engines, and by using eval() or its derivatives you can make those patterns disappear. In this example, we can observe how using eval() avoids the cost of creating an object with a dynamic object key:

// setup:
const key = 'requestId'
const values = Array.from({ length: 100_000 }).fill(42)
// 1. without eval
function createMessages(key, values) {
  const messages = []
  for (let i = 0; i < values.length; i++) {
    messages.push({ [key]: values[i] })
  }
  return messages
}
 
createMessages(key, values)
// 2. with eval
function createMessages(key, values) {
  const messages = []
  const createMessage = new Function('value',
    `return { ${JSON.stringify(key)}: value }`
  )
  for (let i = 0; i < values.length; i++) {
    messages.push(createMessage(values[i]))
  }
  return messages
}
 
createMessages(key, values)

benchmark10

Another good use-case for eval could be to compile a filter predicate function where you discard the branches that you know will never be taken. In general, any function that is going to be run in a very hot loop is a good candidate for this kind of optimization.

Obviously the usual warnings about eval() apply: don’t trust user input, sanitize anything that gets passed into the eval()‘d code, and don’t create any XSS possibility. Also note that some environments don’t allow access to eval(), such as browser pages with a CSP.

8. Use strings, carefully

We’ve already seen above how strings are more expensive than they appear. Well I have kind of a good news/bad news situation here, which I’ll announce in the only logical order (bad first, good second): strings are more complex than they appear, but they can also be quite efficient used well.

String operations are a core part of JavaScript due to its context. To optimize string-heavy code, engines had to be creative. And by that I mean, they had to represent the String object with multiple string representation in C++, depending on the use case. There are two general cases you should worry about, because they hold true for V8 (the most common engine by far), and generally also in other engines.

First, strings concatenated with + don’t create a copy of the two input strings. The operation creates a pointer to each substring. If it was in typescript, it would be something like this:

class String {
  abstract value(): char[] {}
}
 
class BytesString {
  constructor(bytes: char[]) {
    this.bytes = bytes
  }
  value() {
    return this.bytes
  }
}
 
class ConcatenatedString {
  constructor(left: String, right: String) {
    this.left = left
    this.right = right
  }
  value() {
    return [...this.left.value(), ...this.right.value()]
  }
}
 
function concat(left, right) {
  return new ConcatenatedString(left, right)
}
 
const first = new BytesString(['H', 'e', 'l', 'l', 'o', ' '])
const second = new BytesString(['w', 'o', 'r', 'l', 'd'])
 
// See ma, no array copies!
const message = concat(first, second)

Second, string slices also don’t need to create copies: they can simply point to a range in another string. To continue with the example above:

class SlicedString {
  constructor(source: String, start: number, end: number) {
    this.source = source
    this.start = start
    this.end = end
  }
  value() {
    return this.source.value().slice(this.start, this.end)
  }
}
 
function substring(source, start, end) {
  return new SlicedString(source, start, end)
}
 
// This represents "He", but it still contains no array copies.
// It's a SlicedString to a ConcatenatedString to two BytesString
const firstTwoLetters = substring(message, 0, 2)

But here’s the issue: once you need to start mutating those bytes, that’s the moment you start paying copy costs. Let’s say we go back to our String class and try to add a .trimEnd method:

class String {
  abstract value(): char[] {}
 
  trimEnd() {
    // `.value()` here might be calling
    // our Sliced->Concatenated->2*Bytes string!
    const bytes = this.value()
 
    const result = bytes.slice()
    while (result[result.length - 1] === ' ')
      result.pop()
    return new BytesString(result)
  }
}

So let’s jump to an example where we compare using operations that use mutation versus only using concatenation:

// setup:
const classNames = ['primary', 'selected', 'active', 'medium']
// 1. mutation
const result =
  classNames
    .map(c => `button--${c}`)
    .join(' ')
// 2. concatenation
const result =
  classNames
    .map(c => 'button--' + c)
    .reduce((acc, c) => acc + ' ' + c, '')

benchmark11

What the eff should I do about this?

In general, try to avoid mutation for as long as possible. This includes methods such as .trim(), .replace(), etc. Consider how you can avoid those methods. In some engines, string templates can also be slower than +. In V8 at the moment it’s the case, but might not be in the future so as always, benchmark.

A note on SlicedString above, you should note that if a small substring to a very large string is alive in memory, it might prevent the garbage collector from collecting the large string! If you’re processing large texts and extracting small strings from it, you might be leaking large amounts of memory.

const large = Array.from({ length: 10_000 }).map(() => 'string').join('')
const small = large.slice(0, 50)
//    ^ will keep `large` alive

The solution here is to use mutation methods to our advantage. If we use one of them on small, it will force a copy, and the old pointer to large will be lost:

// replace a token that doesn't exist
const small = small.replace('#'.repeat(small.length + 1), '')

For more details, see string.h on V8 or JSString.h on JavaScriptCore.

On strings complexity

I have skimmed very quickly over things, but there are a lot of implementation details that add complexity to strings. There are often minimum lengths for each of those string representations. For example a concatenated string might not be used for very small strings. Or sometimes there are limits, for example avoiding pointing to a substring of a substring. Reading the C++ files linked above gives a good overview of the implementation details, even if just reading the comments.

9. Use specialization

One important concept in performance optimization is specialization: adapting your logic to fit in the constraints of your particular use-case. This usually means figuring out what conditions are likely to be true for your case, and coding for those conditions.

Let’s say we are a merchant that sometimes needs to add tags to their product list. We know from experience that our tags are usually empty. Knowing that information, we can specialize our function for that case:

// setup:
const descriptions = ['apples', 'oranges', 'bananas', 'seven']
const someTags = {
  apples: '::promotion::',
}
const noTags = {}
 
// Turn the products into a string, with their tags if applicable
function productsToString(description, tags) {
  let result = ''
  description.forEach(product => {
    result += product
    if (tags[product]) result += tags[product]
    result += ', '
  })
  return result
}

// Specialize it now
function productsToStringSpecialized(description, tags) {
  // We know that `tags` is likely to be empty, so we check
  // once ahead of time, and then we can remove the `if` check
  // from the inner loop
  if (isEmpty(tags)) {
    let result = ''
    description.forEach(product => {
      result += product + ', '
    })
    return result
  } else {
    let result = ''
    description.forEach(product => {
      result += product
      if (tags[product]) result += tags[product]
      result += ', '
    })
    return result
  }
}
function isEmpty(o) { for (let _ in o) { return false } return true }
// 1. not specialized
for (let i = 0; i < 100; i++) {
  productsToString(descriptions, someTags)
  productsToString(descriptions, noTags)
  productsToString(descriptions, noTags)
  productsToString(descriptions, noTags)
  productsToString(descriptions, noTags)
}
// 2. specialized
for (let i = 0; i < 100; i++) {
  productsToStringSpecialized(descriptions, someTags)
  productsToStringSpecialized(descriptions, noTags)
  productsToStringSpecialized(descriptions, noTags)
  productsToStringSpecialized(descriptions, noTags)
  productsToStringSpecialized(descriptions, noTags)
}

benchmark12

This sort of optimization can give you moderate improvements, but those will add up. They are a nice addition to more crucial optimizations, like shapes and memory I/O. But note that specialization can turn against you if your conditions change, so be careful when applying this one.

Branch prediction and branchless code

Removing branches from your code can be incredibly efficient for performance. For more details on what a branch predictor is, read the classic Stack Overflow answer Why is processing a sorted array faster.

10. Data structures

I won’t go in details about data structures as they would require their own post. But be aware that using the incorrect data structures for your use-case can have a bigger impact than any of the optimizations above. I would suggest you to be familiar with the native ones like Map and Set, and to learn about linked lists, priority queues, trees (RB and B+) and tries.

But for a quick example, let’s compare how Array.includes does against Set.has for a small list:

// setup:
const userIds = Array.from({ length: 1_000 }).map((_, i) => i)
const adminIdsArray = userIds.slice(0, 10)
const adminIdsSet = new Set(adminIdsArray)
// 1. Array
let _ = 0
for (let i = 0; i < userIds.length; i++) {
  if (adminIdsArray.includes(userIds[i])) { _ += 1 }
}
// 2. Set
let _ = 0
for (let i = 0; i < userIds.length; i++) {
  if (adminIdsSet.has(userIds[i])) { _ += 1 }
}

benchmark13

As you can see, the data structure choice makes a very impactful difference.

As a real-world example, I had a case where we were able to reduce the runtime of a function from 5 seconds to 22 milliseconds by switching out an array with a linked list.

11. Benchmarking

I’ve left this section for the end for one reason: I needed to establish credibility with the fun sections above. Now that I (hopefully) have it, let me tell you that benchmarking is the most important part of optimization. Not only is it the most important, but it’s also hard. Even after 20 years of experience, I still sometimes create benchmarks that are flawed, or use the profiling tools incorrectly. So whatever you do, please put the most effort into benchmarking correctly.

11.0 Start with the top

Your priority should always be to optimize the function/section of code that makes up the biggest part of your runtime. If you spend time optimizing anything else than the top, you are wasting time.

11.1 Avoid micro-benchmarks

Run your code in production mode and base your optimizations on those observations. JS engines are very complex, and will often behave differently in micro-benchmarks than in real-world scenarios. For example, take this micro-benchmark:

const a = { type: 'div', count: 5, }
const b = { type: 'span', count: 10 }
 
function typeEquals(a, b) {
  return a.type === b.type
}
 
for (let i = 0; i < 100_000; i++) {
  typeEquals(a, b)
}

If you’ve payed attention sooner, you will realize that the engine will specialize the function for the shape { type: string, count: number }. But does that hold true in your real-world use-case? Are a and b always of that shape, or will you receive any kind of shape? If you receive many shapes in production, this function will behave differently then.

11.2 Doubt your results

If you’ve just optimized a function and it now runs 100x faster, doubt it. Try to disprove your results, try it in production mode, throw stuff at it. Similarly, doubt also your tools. The mere fact of observing a benchmark with devtools can modify its behavior.

11.3 Pick your target

Different engines will optimize certain patterns better or worse than others. You should benchmark for the engine(s) that are relevant to you, and prioritize which one is more important. Here’s a real-world example in Babel where improving V8 means decreasing JSC’s performance.

12. Profiling & tools

Various remarks about profiling and devtools.

12.1 Browser gotchas

If you’re profiling in the browser, make sure you use a clean and empty browser profile. I even use a separate browser for this. If you’re profiling and you have browser extensions enabled, they can mess up the measurements. React devtools in particular will substantially affect results, rendering code may appear slower than it appears in the mirror to your users.

12.2 Sample vs structural profiling

Browser profiling tools are sample-based profilers, which take a sample of your stack at regular intervals. This had a big disadvantage: very small but very frequent functions might be called between those samples, and might be very much underreported in the stack charts you’ll get. Use Firefox devtools with a custom sample interval or Chrome devtools with CPU throttling to mitigate this issue.

12.3 The tools of the trade

Beyond the regular browser devtools, it may help to be aware of these options:

  • Chrome devtools have quite a few experimental flags that can help you figure out why things are slow. The style invalidation tracker is invaluable when you need to debug style/layout recalculations in the browser.
    https://github.com/iamakulov/devtools-perf-features
  • The deoptexplorer-vscode extension allows you to load V8/chromium log files to understand when your code is triggering deoptimizations, such as when you pass different shapes to a function. You don’t need the extension to read log files, but it makes the experience much more pleasant.
    https://github.com/microsoft/deoptexplorer-vscode
  • You can always compile the debug shell for each JS engine to understand more in details how it works. This allows you to run perf and other low-level tools, and also to inspect the bytecode and machine code generated by each engine.
    Example for V8 | Example for JSC | Example for SpiderMonkey (missing)

Final notes

Hope you learned some useful tricks. If you have any comments, corrections or questions, email in the footer. I’m always happy to receive feedback or questions from readers.



From: https://romgrk.com/posts/optimizing-javascript

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2154612.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

多元形式助力商业价值最大化,王鹤棣商业影响力遥遥领先

明星商业代言层出不穷&#xff0c;但在个人影响力的升级玩法上&#xff0c;当代青年偶像王鹤棣以其独特的个人魅力和卓越的商业头脑&#xff0c;正逐步搭建起一个以个人形象为核心&#xff0c;与各大品牌相互成就的立体商业模型。通过一系列创新的商务合作模式&#xff0c;王鹤…

[Java并发编程] synchronized(含与ReentrantLock的区别)

文章目录 1. synchronized与ReentrantLock的区别2. synchronized的作用3. synchronized的使用3.1 修饰实例方法&#xff0c;作用于当前实例&#xff0c;进入同步代码前需要先获取实例的锁3.2 修饰静态方法&#xff0c;作用于类的Class对象&#xff0c;进入修饰的静态方法前需要…

React组件如何暴露自身的方法

一、研究背景 最近遇到一个如何暴露React组件自身方法的问题。在某些时候&#xff0c;我们需要调用某个组件内部的方法以实现某个功能&#xff0c;因此我们需要了解如何暴露组件内部API的方法。 二、实践过程 本文主要介绍React组件暴露子组件API的方法&#xff0c;以下是实…

2024年研赛-华为杯数模竞赛C题论文首发+论文讲解+代码分享

2024年华为杯-研赛分享资料&#xff08;论文分享部分代码&#xff09;&#xff08;已更新部分代码&#xff09;&#xff1a;链接&#xff1a;https://pan.baidu.com/s/1HGIYjV3lqzUc_3H0vg5H8w 提取码&#xff1a;sxjm 题 目&#xff1a; _基于数据驱动下磁性元件的磁芯损耗建模…

leetcode第十三题:罗马数字转整数

罗马数字包含以下七种字符: I&#xff0c; V&#xff0c; X&#xff0c; L&#xff0c;C&#xff0c;D 和 M。 字符 数值 I 1 V 5 X 10 L 50 C 100 D 500 M 1000 例如&#x…

OpenCV特征检测(7)角点检测函数goodFeaturesToTrack()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 确定图像上的强角点。 该函数根据 240中所描述的方法查找图像中最显著的角点或者指定图像区域内的最显著角点。 函数使用 cornerMinEigenVal 或…

华南理工大学信息工程高频电子线路课程设计——基于锁相环的调试解调器设计

完整报告链接如下&#xff1a; 通过网盘分享的文件&#xff1a;高频课设报告.docx 链接: https://pan.baidu.com/s/1J83UCDSU0UHcv4ONYxfyhg?pwdzqyr 提取码: zqyr --来自百度网盘超级会员v5的分享 懒得贴上来了&#xff0c;放一下截图。

ICM20948 DMP代码详解(34)

接前一篇文章&#xff1a;ICM20948 DMP代码详解&#xff08;33&#xff09; 上一回解析了inv_icm20948_initialize_lower_driver函数中设置FIFO_RST和FIFO_CFG寄存器相关的代码&#xff0c;本回继续往下解析inv_icm20948_initialize_lower_driver函数的后续代码。为了便于理解和…

蓝桥杯【物联网】零基础到国奖之路:十. OLED

蓝桥杯【物联网】零基础到国奖之路:十.OLED 第一节 硬件解读第二节 MDK配置 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/fa7660b81be9407aa19c603561553db0.png)第三节 代码 第一节 硬件解读 OLED硬件知识: 第二节 MDK配置 第三节 代码 include头文件。 编…

Webui 显卡有显存,会报错:CUDA out of memory

Webui 显卡明明有显存&#xff0c;会报错&#xff1a;CUDA out of memory 网上找了很多资料&#xff0c;都没有能解决这个问题 &#xff0c;后来发现和电脑虚拟内存设置有关&#xff0c;这里记录一下具体的解决方法&#xff1a; 什么是 CUDA Out of Memory 错误&#xff1f; …

【MySQL】字符集与Collation

今天做项目&#xff0c;突然发现&#xff0c;项目中使用的MySQL的库排序规则是 utf8mb4_general_ci&#xff0c;而我自己用的MySQL8默认库规则是utf8mb4_0900_ai_ci&#xff0c;于是想要弄清楚 出处&#xff08;写的非常详细&#xff09;&#xff1a;mysql设置了utf8mb4&#x…

WPF自定义Dialog模板,内容用不同的Page填充

因为审美的不同&#xff0c;就总有些奇奇怪怪的需求&#xff0c;使用框架自带的对话框已经无法满足了&#xff0c;这里记录一下我这边初步设计的对话框。别问为啥要用模板嵌套Page来做对话框&#xff0c;问就是不想写太多的窗体。。。。 模板窗体&#xff08;XAML&#xff09;…

面试-设计模式

策略模式 定义了一组算法&#xff0c;分别封装起来&#xff0c;这些算法直接可以相互替换 设计模式的开闭原则&#xff1a;对修改关闭&#xff0c;对扩展开放 装饰模式 将某种算法作为一个装饰品添加到对象身上&#xff0c;同时可以自由穿戴更换装饰品 两个主要的角色&…

C语言 | Leetcode C语言题解之第423题从英文中重建数字

题目&#xff1a; 题解&#xff1a; char * originalDigits(char * s) {int lenstrlen(s);int arr[26]{0},num[10]{0},cot0;for(int i 0; i < len; i)arr[s[i] - a];num[0] arr[z-a];num[2] arr[w-a];num[4] arr[u-a];num[6] arr[x-a];num[8] arr[g-a];num[1] arr[o…

[JavaEE] UDP协议

目录 再谈端口号 一、端口号的划分 二、UDP协议 三、UDP的特点 再谈端口号 一、端口号的划分 0-1023&#xff1a;知名端口号&#xff0c;端口号固定&#xff0c;其中包括HTTP&#xff0c;FTP&#xff0c;SSH等广为使用的应用层协议。 1024-65535&#xff1a;操作系统动态分…

演示jvm锁存在的问题

文章目录 1、AlbumInfoApiController --》testLock()2、redis添加键值对3、AlbumInfoServiceImpl --》testLock() 没有加锁4、使用ab工具测试4.1、安装 ab 工具4.2、查看 redis 中的值 5、添加本地锁 synchronized6、集群情况下问题演示 jvm锁&#xff1a;synchronized lock 只…

面试金典题2.4

给你一个链表的头节点 head 和一个特定值 x &#xff0c;请你对链表进行分隔&#xff0c;使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。 你不需要 保留 每个分区中各节点的初始相对位置。 示例 1&#xff1a; 输入&#xff1a;head [1,4,3,2,5,2], x 3 输出&a…

LeetCode从入门到超凡(二)递归与分治算法

引言 大家好&#xff0c;我是GISer Liu&#x1f601;&#xff0c;一名热爱AI技术的GIS开发者。本系列文章是我跟随DataWhale 2024年9月学习赛的LeetCode学习总结文档&#xff1b;在算法设计中&#xff0c;递归和分治算法是两种非常重要的思想和方法。它们不仅在解决复杂问题时表…

matlab绘制二维云图,划分区域,并显示每个区域的均值

绘制成图如下&#xff1a; 代码如下&#xff1a; %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%创建绘图的数据 ax0;bx1; ay0;by1; nx100; %数据的x轴点数 ny100; %数据的y轴点数 hx(bx-ax)/(nx-1); hy(by-ay)/(ny-1); Xax:hx:bx; Yay:hy:by; da…

HTTP中的301、302实现重定向

HTTP状态码301和302描述 ‌HTTP状态码301和302用于实现重定向‌&#xff0c;其中301代表永久重定向&#xff0c;而302代表临时重定向。这两种重定向方式在网页开发、搜索引擎优化&#xff08;SEO&#xff09;以及用户体验方面扮演着重要的角色。 301 301永久重定向‌意味着原…