javascript
js与c语言效率_JavaScript比c语言的性能差了多少?
我這里先不說和C之間的性能差距,而是展開說JavaScript遞歸優化問題,有人問我為什么不說性能差距,我:???這個問題就跟問地球為什么是圓的一樣(明擺著)
傳統的遞歸函數,比如:
function a(){
return a()
}
這是一個經典的遞歸,在函數a內部調用自身,調用棧的機制如下:每調用一個函數,解釋器就會把該函數添加進調用棧并開始執行.
正在調用棧中執行的函數還調用了其它函數,那么新函數也將會被添加進調用棧,一旦這個函數被調用,便會立即執行.
當前函數執行完畢后,解釋器將其清出調用棧,繼續執行當前執行環境下的剩余的代碼.
當分配的調用棧空間被占滿時,會引發“堆棧溢出”錯誤.
這里我們做一個測試,嘗試運行這個遞歸函數,找出調用棧深度:
let index = 0
function a(){
index += 1
console.log(index)
return a()
}
執行結果如下:
// 省略多行輸出
> 12559
> 12560
> 12561
> 12562
> 12563
> Uncaught RangeError: Maximum call stack size exceeded
at console.X.t. [as log] (init.js:1)
這里可以看出,當遞歸深度達到12563的時候,調用棧爆掉了,平時使用是沒有問題的,但是對于某些特殊或者極端情況,你又有相似需求的情況下,怎么樣突破這個調用棧限制,做到永不爆棧呢?這里需要一個比較hack的寫法, 我們對上面的遞歸函數進行一下改造:
let index = 0
async function a(){
await undefined
index += 1
console.log(index)
return await a()
}
我們來執行一下看一下結果:
// 省略多行輸出
> 100590
> 100591
> 100592
// 省略多行輸出
事實上,這個遞歸函數永遠不會停止,它會一直執行下去,也沒有爆棧,這是一個神奇的優化,可以讓你寫出非常大深度的遞歸而不會出現問題,這個優化的關鍵就是:
async function() { await undefined }
首先將遞歸函數改為async函數,然后在內部最好第一行 await undefined;
這個操作的原理就是:
1, async創建微任務隊列,然后執行器執行當前隊列.
2,此時遇到await undefined,其實這個寫法等同于await (async () => {})和await Promise.resolve(setTimeout)這幾種寫法效果等同,用unedfined只是為了在實現同樣效果的情況下更簡潔,既然已經等同了,那就從這三個寫法分析起.
3,此時,執行器發現第一個任務完全沒有等待,馬上完成了,但是執行器發現后面的任務是需要等待的,并不會馬上完成.
4,這時候執行器為了microtask(也就是協程)調度的合理優化,不會讓這個微任務隊列始終占有這個execution,而是會把當前微任務隊列轉移到別的execution去執行(您幾位走得慢,請去那邊空閑的地方走).
5,轉移execution帶來的操作就是,因為沒辦法直接轉移調用棧,所以會先將當前調用棧入堆,然后把任務隊列轉移到別的execution.
6,然后隊列里面接下來的任務全部都是使用新創建的execution去執行.
這個操作的本意就是為了讓當前棧入堆,而且這個寫法在C#和Kotlin里面是完全通用的,因為這3個語言的異步方案都是基本類似,而這個寫法來自Rust群一位群友的發現,當時我看到這種寫法的時候也表示了驚奇,然后對于遞歸大面積使用這種寫法,目前沒有發現什么問題.
這兩天發現有人@星風雪月對執行機制有很深的成見,所以我這里使用Node.JS的async hooks做了一下異步執行的調試,這是測試代碼:
const async_hooks = require("async_hooks")
let index = 0
let print_buffer = ""
/*** async hooks會追蹤async調用,* 而console.log使用異步輸出,* 所以這里使用同步方法模擬console*/
function println(log) {
print_buffer += log + "\n"
}
/* 創建鉤子 */
async_hooks.createHook({
init(asyncId, type, triggerAsyncId) {
const eid = async_hooks.executionAsyncId()
println("init: *********************************")
println("init: triggerAsyncId " + triggerAsyncId)
println("init: executionAsyncId " + eid)
println("init: asyncId " + asyncId)
println("init: type " + type)
}
})
.enable()
/*********** 測試區 **********/
async function A() {
/* 為了觀察方便只執行2次 */
println("A: runing")
index += 1
if (index === 2) return undefined
// 有優化遞歸 await undefined
return await A()
}
/*********** 測試區 **********/
A().then(() => {
console.log(print_buffer)
})
執行之后的輸出:
init: *********************************
init: triggerAsyncId 1
init: executionAsyncId 1
init: asyncId 2
init: type PROMISE
A: runing
init: *********************************
init: triggerAsyncId 2
init: executionAsyncId 1
init: asyncId 3
init: type PROMISE
init: *********************************
init: triggerAsyncId 3
init: executionAsyncId 1
init: asyncId 4
init: type PROMISE
init: *********************************
init: triggerAsyncId 2
init: executionAsyncId 1
init: asyncId 5
init: type PROMISE
init: *********************************
init: triggerAsyncId 4
init: executionAsyncId 4
init: asyncId 6
init: type PROMISE
A: runing
init: *********************************
init: triggerAsyncId 6
init: executionAsyncId 4
init: asyncId 7
init: type PROMISE
這里看executionAsyncId標志,這個是當前執行器的ID,這里可以看到當經過await undefined之后, 執行器從1變成了4,說明這里發生了execution轉移,下面我修改一下代碼,變成不優化的寫法:
// 無優化遞歸// await undefinedreturn await A()
我這里將await undefined刪除,再來執行看看會是什么情況:
init: *********************************
init: triggerAsyncId 1
init: executionAsyncId 1
init: asyncId 2
init: type PROMISE
A: runing
init: *********************************
init: triggerAsyncId 1
init: executionAsyncId 1
init: asyncId 3
init: type PROMISE
A: runing
init: *********************************
init: triggerAsyncId 3
init: executionAsyncId 1
init: asyncId 4
init: type PROMISE
init: *********************************
init: triggerAsyncId 2
init: executionAsyncId 1
init: asyncId 5
init: type PROMISE
再看executionAsyncId,這里始終是使用1去執行,所以沒有轉移execution執行,這里就很能說明問題了,await undefined可以轉移執行器執行,讓當前棧入堆,這樣可以使調用棧不會溢出,達到深遞歸優化的目的.
對于評論區朋友說明,這種方式會阻塞macrotask,所以不推薦這種寫法,我這里并不表示完全反對意見,我這里來說一下我自己的看法:
異步任務歸屬microtask,而其他事件回調歸屬macrotask,microtask的優先級本身就比macrotask要高,所以肯定是microtask先執行,然后才輪到macrotask,像setTimeout(0)這種本身就是屬于macrotask,肯定要等到microtask執行完成之后才能執行,不過這確實會帶來一個問題,就是對已經運行的macrotask產生時間分辨率精度影響,比如定時器偏移,定時器不會精準得按時間分片執行任務,所以這種寫法見仁見智,你如果需要精確macrotask執行的場景還是慎用.
總結
以上是生活随笔為你收集整理的js与c语言效率_JavaScript比c语言的性能差了多少?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java8 迭代set集合_JavaS
- 下一篇: python自己创建模块路径_pytho