Giter VIP home page Giter VIP logo

Comments (13)

sokra avatar sokra commented on September 2, 2024 11

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.

from tapable.

sohucw avatar sohucw commented on September 2, 2024 4

JavaScript 引擎的编译优化:

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

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

from tapable.

xiaoxiaojx avatar xiaoxiaojx commented on September 2, 2024 1

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

from tapable.

lizuncong avatar lizuncong commented on September 2, 2024 1

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; }

from tapable.

sokra avatar sokra commented on September 2, 2024

Try to call the tap many many times

from tapable.

lizuncong avatar lizuncong commented on September 2, 2024

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

from tapable.

lizuncong avatar lizuncong commented on September 2, 2024

@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.

from tapable.

sokra avatar sokra commented on September 2, 2024

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)

from tapable.

lizuncong avatar lizuncong commented on September 2, 2024

@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!

from tapable.

lizuncong avatar lizuncong commented on September 2, 2024

@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.

from tapable.

lizuncong avatar lizuncong commented on September 2, 2024

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

from tapable.

lizuncong avatar lizuncong commented on September 2, 2024

`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

from tapable.

sohucw avatar sohucw commented on September 2, 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.

from tapable.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.