Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is the new Function performance really good? #162

Open
lizuncong opened this issue Sep 10, 2021 · 13 comments
Open

Is the new Function performance really good? #162

lizuncong opened this issue Sep 10, 2021 · 13 comments

Comments

@lizuncong
Copy link

I simply implemented a MySyncHook, and then traversed the callback function directly in the call method, and printed the execution time. At the same time, I used tapable's official SyncHook to execute the same logic, and found that the execution time of tapable's SyncHook was dozens of times longer than my own. Could anyone please help me answer.
1631269588(1)

@sokra
Copy link
Member

sokra commented Sep 13, 2021

Try to call the tap many many times

@lizuncong
Copy link
Author

First of all thank you for your reply. I try to call the callback 10,000 times, and the result is that the official tapable synchook execution time is still dozens of times longer than I wrote. Does this mean that the method of dynamically generating the function body through new Function is not suitable? Finally, here are some tapable hooks I wrote myself. I tested that the execution time of these hooks is shorter than the official tapable hooks. Of course, the robustness is not as strong as the tapable official. mini-tapable

image

@lizuncong
Copy link
Author

@sokra I just called the tap 10,000 times,and found that the official tapable sync hook took almost 60ms, while my synchook took
0.8ms.

@sokra
Copy link
Member

sokra commented Sep 13, 2021

I just called the tap 10,000 times

Try to call call multiple times. That's what happens in practice. Registering 10000 plugins it unlikely, but the plugin handler could be called 10000 times (e. g. hooks in the javascript parser are called for every statement)

@lizuncong
Copy link
Author

@sokra This time I call the tap 1000 times and call the call 10,000 times,the result as the image shows
`const { SyncHook } = require('tapable')
class MySyncHook{
constructor(argNames){
this.argNames = argNames;
this.tasks = []
}

tap(plugin, callback){
this.tasks.push(callback)
}

call(...args){
this.tasks.forEach(task => task(...args))
}
}
const hook = new SyncHook(['compilation'])
const myHook = new MySyncHook(['compilation'])

const compilation = { sum: 0 }
const myCompilation = { sum: 0}

for(let i = 0; i < 1000; i++){
hook.tap(plugin${i}, (compilation) => {
compilation.sum = compilation.sum + i
})

myHook.tap(plugin${i}, (compilation) => {
compilation.sum = compilation.sum + i
})
}

console.time('tapable')
for(let i = 0; i < 10000; i++){
hook.call(compilation)
}
console.timeEnd('tapable')

console.time('my')
for(let i = 0; i < 10000; i++){
myHook.call(myCompilation)
}
console.timeEnd('my')

`
image
I think you can take a moment to try.

Thank you!

@lizuncong
Copy link
Author

@sokra I think it should be the new version of nodejs that has been optimized, and the performance is better than the function body dynamically generated by new Function.

@xiaoxiaojx
Copy link

The number of calls in the above code is set to 100 0000 times, and the conclusion is correct,new Function performance is faster !!!

image

@lizuncong
Copy link
Author

I just tried it. When the number of calls is more than 17000 times, the efficiency of tapable does to be faster ! But in what scenarios will the call be called so many times

image

image

@lizuncong
Copy link
Author

lizuncong commented Sep 15, 2021

`const { SyncHook } = require('tapable')
const CALL_DELEGATE = function(...args){
this.call = this._createCall(); // The function is dynamically generated when it is called for the first time
return this.call(...args)
}
class MySyncHook{
constructor(argNames){
this.argNames = argNames;
this.tasks = []
// this._call = CALL_DELEGATE;
this.call = CALL_DELEGATE;
}

tap(plugin, callback){
// This.call must be reset every time a new plugin is added
this.call = CALL_DELEGATE;
this.tasks.push(callback)
}
_createCall(){
const params = this.argNames.join(',');
return new Function(params, "this.tasks.forEach(task => task(" + params + "))")
}
// call(...args){
// this.tasks.forEach(task => task(...args))
// }
}
const hook = new SyncHook(['compilation'])
const myHook = new MySyncHook(['compilation'])

const compilation = { sum: 0 }
const myCompilation = { sum: 0}

for(let i = 0; i < 1000; i++){
hook.tap("plugin" + i, (compilation) => {
compilation.sum = compilation.sum + i
})

myHook.tap("plugin" + i, (compilation) => {
compilation.sum = compilation.sum + i
})
}
const count = 20000000;

console.time('tapable')
for(let i = 0; i < count; i++){
hook.call(compilation)
}
console.timeEnd('tapable')

console.time('my')
for(let i = 0; i < count; i++){
myHook.call(myCompilation)
}
console.timeEnd('my')
`

This time i register 1000 plugins and call the call 2000,0000 times. But i generate the this.call function by new Function when i first call the this.call function. The result is as the image shows:

image

@sokra
Copy link
Member

sokra commented Sep 15, 2021

One major difference it that you pay the compilation cost only on the first compilation and each following compilation is fast after that. tapable also optimizes for the polymorphic case, so when different plugins are added and multiple hooks are used. The new Function approach allows v8 to optimize the call method for the compilation of plugins added and even allows to inline plugin functions into the generated code for call.

But I played around with benchmarking the different approaches with the following conclusions:

  • Here is the code I used if you want to play with it: https://gist.github.com/sokra/7c09cec71b4753a774ab07cc565087bd
    • It tries to be a bit more realistic by providing multiple different functions and arguments.
  • There is a large code generation overhead in tapable to generate the code for the new Function(). With large I mean in the µs range, e. g. for 50 plugins about 80µs for me.
    • Not sure if we can compare that to simplified implementations, since it also handles return values, interceptors, async calls, etc.
  • ...args is slower than generating a new function with the right arguments.
  • Unrolling the taps in a new Function is faster than a loop/forEach. Expect when the plugin count is really high.
  • The CALL_DELEGATE approach is only faster than checking for deopt in the generated code when the hook is called more than 10000 times.
    • CALL_DELEGATE avoids the extra check in each call.

=> While tapable is a bit slower when Hooks are small or called only a few times, it's very faster when hooks are called often. But when Hooks are small or called only a few times, they are also very fast in general, so maybe that's not the thing we should focus on.

Anyway maybe it makes sense to have a "interpreter mode" which is used for the first 10 calls, so we improve performance on hooks that are only calls once.
And we could look into caching the code generation when Hooks are recreated.

@lizuncong
Copy link
Author

lizuncong commented Sep 15, 2021

wow, you are so nice and patient. Thank you! My last question is, why not reset this.call with CALL_DELEGATE , but use this._call. such as
_resetCompilation() { this.call = CALL_DELEGATE ; this.callAsync = CALL_ASYNC_DELEGATE; this.promise = PROMISE_DELEGATE; }

@sohucw
Copy link

sohucw commented Mar 6, 2024

JavaScript 引擎的编译优化:

new Function 方法: 当你使用 new Function 创建一个函数时,这个函数通常会被 JavaScript 引擎视为一个独立的脚本。这意味着它可以进行更彻底的优化。由于该函数是在运行时创建的,引擎有机会根据当前上下文优化它,这可以包括内联优化、避免不必要的变量查找等。
forEach 循环: 相比之下,使用 forEach 循环直接调用数组中的函数则涉及到多次函数调用的开销。每次循环都会调用一个函数,这可能会导致更多的上下文切换和较少的优化机会。每次函数调用都涉及到创建新的调用栈、传递参数等开销。
函数调用的开销:

在 forEach 循环中,每次迭代都会进行一次函数调用。这些调用涉及到创建调用上下文、传递参数、以及在调用栈上进出的开销。
而使用 new Function 方法创建的函数,在它的内部直接编码了所有的函数调用。这种方式减少了函数调用的次数,因为所有的调用都被内联到一个单独的函数体内。这就减少了调用栈的变化,提高了执行效率。
总之,使用 new Function 创建的函数,由于更优的编译优化和减少的函数调用开销,往往在性能上优于简单的 forEach 循环调用。然而,这种优化的程度可能会因 JavaScript 引擎的实现细节而有所不同,而且它也带来了代码的复杂性和可维护性的挑战。在实际应用中,选择哪种方法取决于具体场景和性能需求。

@sohucw
Copy link

sohucw commented Mar 6, 2024

When comparing the use of new Function to create and execute functions with directly using a forEach loop to execute functions, the performance differences primarily stem from two aspects: the compilation optimization of the JavaScript engine and the overhead of function calls.

JavaScript Engine's Compilation Optimization:

Using new Function Method: When you create a function using new Function, it is often treated by the JavaScript engine as an independent script. This means it can undergo more thorough optimization. As the function is created at runtime, the engine has the opportunity to optimize it based on the current context, which can include inline optimization, avoiding unnecessary variable lookups, etc.
Using forEach Loop: In contrast, directly calling functions in an array with a forEach loop involves the overhead of multiple function calls. Each loop iteration calls a function, which may lead to more context switching and fewer opportunities for optimization. Each function call involves creating a new call stack, passing arguments, etc.
Overhead of Function Calls:

In a forEach loop, each iteration involves a function call. These calls involve creating a calling context, passing parameters, and the overhead of entering and exiting the call stack.
However, a function created using the new Function method encodes all the function calls inside it. This approach reduces the number of function calls, as all calls are inlined into a single function body. This reduces the changes in the call stack, enhancing execution efficiency.
In summary, functions created using new Function, due to better compilation optimization and reduced function call overhead, tend to perform better than simple forEach loop calls. However, the degree of this optimization might vary depending on the implementation details of the JavaScript engine, and it also introduces challenges in code complexity and maintainability. In practical applications, the choice between these methods depends on the specific scenario and performance requirements.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants