# 我所不知道的 JavaScript (上):理論篇 ###### tags: `JavaScript` `JavaScript Engine` `Java` `JVM` `Python` `C` `C++` `Rust` `Runtime` `System Design` `App` `API` `ABI` `Web Technology` `Event Driven` `Cross Platform` `AOT` `JIT` `Compiler` `V8` `TurboFan` `Compiler Design` `Optimization` `Security` `Operating System` `Linux` `Kernel` `Software` `Automata` `UNIX` 記錄多個 JavaScript 語言背後設計哲學、JavaScript 系統環境架構、程式行為與最佳化、ECMAScript 擴充、各技術規範標準和相依平台 API 實作之討論。 :::info :arrow_right: 下篇:[我所不知道的 JavaScript (下):應用篇 - shibarashinu](https://hackmd.io/@shibarashinu/H18RFljGkl)。 ::: ![](https://hackmd.io/_uploads/BkK3X-jzyg.png) > ~~標題剽竊~~ 標題參考自 [「你所不知道的 C 語言」系列講座 - jserv](https://hackmd.io/@sysprog/c-programming)。 :::info **「語言」vs.「系統」vs.「程式」。** :arrow_right: 呼應另一篇關於「程式編譯」討論: [Compiler: The Program, the Language, & the Computer Work - shibarashinu](https://hackmd.io/@shibarashinu/SyEHz-JHC)。 ::: Todos: - Tracing [V8](https://v8.dev/blog/v8-release-57): [Ignition](https://docs.google.com/document/d/11T2CRex9hXxoJwbYqVQ32yIPMh0uouUZLdyrtmMoL44/edit), [TurboFan](https://docs.google.com/presentation/d/1_eLlVzcj94_G4r9j9d_Lj5HRKFnq6jgpuPJtnmIBs88/edit) - [An Introduction to Speculative Optimization in V8 - Benedikt Meurer](https://benediktmeurer.de/2017/12/13/an-introduction-to-speculative-optimization-in-v8/) - [《Chrome V8 源码》 - 灰豆](https://www.anquanke.com/member.html?memberId=161290) - [V8](https://www.anquanke.com/post/id/253048) - [Ignition](https://www.anquanke.com/post/id/254554) - [TurboFan](https://www.anquanke.com/post/id/264846) - [V8 API Reference Guide - V8 Docs](https://v8docs.nodesource.com/node-0.8/index.html) - Tracing [CPython](https://github.com/python/cpython) - [CPython 專案簡介 - 為你自己學 PYTHON](https://pythonbook.cc/chapters/cpython/project-structure) - VizTracer: [【python】我用了多进程怎么程序反而变慢了? - 码农高天](https://www.youtube.com/watch?v=xFtEg_e54as) - Tracing [Node.js](https://github.com/nodejs/node/tree/main/src) - [Single executable applications](https://nodejs.org/api/single-executable-applications.html#single-executable-applications) - Better programming paradigm insights & implementations for modern software development at the programming design level - Safety critical systems - [The Power of Ten Rules for Developing Safety Critical Code - Gerard J. Holzmann - NASA/JPL Laboratory for Reliable Software](https://spinroot.com/gerard/pdf/P10.pdf) - [how NASA writes space-proof code - Low Level](https://www.youtube.com/watch?v=GWYhtksrmhE) - pre: static analyzers / compilers / linters / comments / generic programming / mid: runtime analyzers / assertion / sanitizers / debuggers / profilers / devtools / post: unit tests / TDD - [John Carmack: Best programming setup and IDE - Lex Clips](https://www.youtube.com/watch?v=tzr7hRXcwkw) - [Julia in 100 Seconds - Fireship](https://www.youtube.com/watch?v=JYs_94znYy0): A dynamic general purpose programming language for scientific computing & big data analytics. ## JavaScript 設計哲學 :::warning **[First Things First] Quick Quiz: How well do you know JavaScript?** > ![](https://hackmd.io/_uploads/HksiLJ_Ixx.png =500x) > (Source: [The JavaScript Date Quiz - jsdate.wtf](https://jsdate.wtf/)) ::: 在深入探索 JavaScript 以前,不妨先嚐嚐 JavaScript 語言背後被創造的動機、欲解決的問題、系統設計上的巧思,以及 runtime 系統架構與程式來回交互的精隨。 > 如此,也以一個更全面客觀的角度來縱觀整個計算機科學的發展進程、科技迭代和行業演化。以期掌握最新技術背後的來龍去脈後,能具備更好視野展望及掌握未來趨勢方向。 ### 淺談計算機發展歷史 #### Shell、Shell Script、動態語言和作業系統之沿革 Shell vs. Kernel 概念,或 Userland vs. Kernel Land,最初由 UNIX 作業系統發想並引入超越那時代對 Shell 的前衛理解和設計 (也是由 Multics 多 ring 權限簡化而來),成功帶起人們走向現代動態程式設計和 general-purpose 系統架構開發:如在作業系統中,能盡可能透過 runtime 程序、系統架構、機器指令簡化使用者繁瑣操作和重複任務的工作,就盡可能以抽象層包裝 —— 由底層系統服務來為使用者 / 開發者解決這些惱人問題: - 多使用者面向的階級式檔案系統存取、分類和管理 *(i.e. inodes, drwxrwxrwx, ELF, ...)*,解放舊有檔案系統對檔案約束的認知 (e.g., 檔案類型、檔案大小、檔案對檔案的操作、和後繼者 Linux 上可兼容多種檔案系統 (甚至兼容不同裝置) 的 VFS、...)。 ![image](https://hackmd.io/_uploads/r1GyTLDExl.png) > ![image](https://hackmd.io/_uploads/HytTiYbwkg.png) > > (Source: [IO stack of the Linux kernel - Wiki](https://upload.wikimedia.org/wikipedia/commons/3/30/IO_stack_of_the_Linux_kernel.svg)) - *“Everything is a file”* 在作業系統包裝下在 runtime 對其詮釋之前,任何檔案、程式、IO 裝置接口 (韌體形式互動)、控制器 (韌體形式互動)、協同處理器 (韌體形式互動)、硬體包裝解釋抽象層 (IO buffer、in-kernel 網路棧 socket、高階程式語言 IO stream 界面、UNIX 首創作業系統對資料處理流高階抽象化 [pipe](https://man7.org/linux/man-pages/man2/pipe.2.html))、... 等在底層看來始終都只是在處理二進制檔案流 (e.g., glibc 包裝的 FILE (e.g., for `stdin`, `stdout`, `stderr`), Linux kernel 各 process 擁有的 file descriptors),捨棄底層對進階裝置及資料的詮釋,而將它們的 meta-data & -properties 延留給更上層的系統或 middleware 去做進階判斷和操作。 > 當然,經過 50 年發展,當初簡約優雅的 UNIX `fd` 設計已無法滿足現今複雜的計算機系統需求 (e.g., `socket`, `ioctl`, `epoll`, `<resources>fd`, ...),這裡傾向以一種更超越於「檔案」操作管理形式的存在來解讀,在此指稱他為廣義的「系統資源指標」管理設計哲學。 :::info **使計算機系統邁向全能的 General-Purpose System** 作業系統在認知上選擇停留在這些都只是在處理「二進制資源 & 資料」的操作,是為 UNIX *"Everything is a file"* 核心理念的完全體現。從此擺脫作業系統只能處理認知內事物的舊有思維,UNIX 此舉解放了在一系統內軟硬體可無限擴展的可能性,為計算機系統工程邁向 general-purpose system 做出劃時代革新貢獻。 ::: - **優勢:** 各資源間可以互相交流、轉換、協作,打破各資源硬體軟體間交互的屏障 (也頗具跨裝置跨平台實作精神),任何 IO 裝置資源只要有相對應的軟韌系統配合就能正常工作,充分體現 *"program-able"* ---- 任何資料都可編程的強大力量。 :::warning *"The power of absraction — simple, elegant, reusable, extensible, yet powerful."* ::: > **註:** UNIX 系統哲學能在日後發揚光大,很大一部份也是受 *TCP/IP (derived from ARPAnet & BSD)* × *GNU gcc & maketools* x *Linux kernel* x *open-source softwares* 等種種時代環境因素累積而使其能迅速推廣部署的原因,並直接影響千禧年後網路崛起和數位資訊大爆炸時代,乃至當前前沿軟體科技產業,直至今日隨處仍可見當初 UNIX 優雅的系統架構設計的影子。 - **缺點:** 上層往往需要有相對應支援的軟體方案才能工作 (一體兩面),這也意味著各模組上層都必須有人維護才能實現跨平台功能,實作方法、API 接口、 bug 修復、開發習慣、迭代週期上也大相逕庭,如何推動與整合各家軟硬體生態將是一大難題,而這也是 Red Hat、Canonical、SUSE 等依附於 GNU/Linux 的 open source 生態起家的企業目前的運營策略和獲利模式 —— 為特定領域企業的客戶提供一套建基於 GNU/Linux 系統生態的完整 B2B 建設、支援與維護服務:一方面提供雇主定期安全性更新、防堵 zero-day 攻擊的熱修復、新業務擴展、既有業務 scale up 等;另一方面回 open-source 生態圈注入更多專業開發技術和豐富完善的基建,收互利共生之效。 :::warning **科技巨頭紛進駐 Open-Source 生態:是一劑強心針,還是裹著糖衣的毒藥?** 時代驟遷劇變,就連曾經那些擁護封閉軟體生態圈的寡頭們如 Microsoft、Meta、Apple 也相繼轉投入 open-source 市場,更不用說經營 *(布局)* open-source 已久的 Google。連巨頭們都不得不低頭匆匆加入這場 open-source 戰爭,為日後自身利益布局,搶佔先機,可見由 open-source 生態成功帶起的可行商業模式、意外開拓的極具潛力的新興市場和可觀的商業利益、及伴隨著 BSD & GNU/Linux 周遭連帶起來的 open-source 開發社群等,都是將來影響科技產業不容忽視的龐大力量,深深牽動著未來軟體和科技發展走勢。 但這又會為接下來自由軟體生態帶來什麼樣影響?會不會因為大財閥鉅額資本的介入導致逐漸失去當初創建 open-source 的初衷,發展目標從「為自由軟體生態服務」轉變成「為大企業財閥服務」,open-source 變質成為巨頭間獲利的工具、武器、談判籌碼,劣化成為大企業的附庸,淪為權力利益鬥爭的修羅場。而人人都想爭食這塊大餅,進而拉垮整個 open-source 生態圈。 曾經,由一群打著人本主義高喊 *freedom* 抱崇高信念的人們,藉由宣揚 copyleft 理念,集社群力量一點一滴打造的 open-source 帝國,是否已悄然被攀上的 *proprietary* 資本扼住咽喉,危在旦夕? ::: **例如:** 參考 UNIX 系統設計哲學實作的 Linux *"files"*。 ```sh ls /dev ``` ![image](https://hackmd.io/_uploads/H1c5EUVQyg.png =500x) > TTY 代指終端機界面裝置,也被歸類為是 file 的一種,只要有相應 character device driver 支援,系統即可兼容跨越現代「虛擬終端」、「液晶 / LED 螢幕面板」、「映像管掃描螢幕」,到當時普遍使用的遠距通話用「電傳打字機」輸出端口。如果願意,甚至可以回到過去向前支援 1876 年 Bell 手上那台「電話機」,和更古早時的「電報機」 (可能需要相對硬體橋接和韌體支援,如:serial port controller)。 > > [[轉載] Linux 中 tty、pty、pts 的概念區別](https://nano-chicken.blogspot.com/2014/07/linuxttyptypts.html) - [Glibc/libio/bits/types/struct_FILE.h](https://elixir.bootlin.com/glibc/glibc-2.40.9000/source/libio/bits/types/struct_FILE.h#L49) 的 glibc runtime 對 Linux *"UNIX-like"* 的 file 操作作 higer-level 包裝的 `FILE` 資料結構: ```c= struct _IO_FILE { int _flags; /* High-order word is _IO_MAGIC; rest is flags. */ /* The following pointers correspond to the C++ streambuf protocol. */ char *_IO_read_ptr; /* Current read pointer */ char *_IO_read_end; /* End of get area. */ char *_IO_read_base; /* Start of putback+get area. */ char *_IO_write_base; /* Start of put area. */ char *_IO_write_ptr; /* Current put pointer. */ char *_IO_write_end; /* End of put area. */ char *_IO_buf_base; /* Start of reserve area. */ char *_IO_buf_end; /* End of reserve area. */ /* The following fields are used to support backing up and undo. */ char *_IO_save_base; /* Pointer to start of non-current get area. */ char *_IO_backup_base; /* Pointer to first valid character of backup area */ char *_IO_save_end; /* Pointer to end of non-current get area. */ struct _IO_marker *_markers; struct _IO_FILE *_chain; int _fileno; int _flags2; __off_t _old_offset; /* This used to be _offset but it's too small. */ /* 1+column number of pbase(); 0 is unknown. */ unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1]; _IO_lock_t *_lock; #ifdef _IO_USE_OLD_IO_FILE }; ``` - [Glibc/libio/stdfiles.c](https://elixir.bootlin.com/glibc/glibc-2.40.9000/source/libio/stdfiles.c#L52) (合以對 Linux kernel 的 IO *(file)* 操作作進階包裝的 [Glibc/libio/libioP.h](https://elixir.bootlin.com/glibc/glibc-2.40.9000/source/libio/libioP.h)) 下定義的 —— 在 kernel 中各 process 使用 glibc runtime library 的 `__libc_start_main` 或其 child process `fork` 時會繼承使用到的 —— `stdio` (e.g., `stdin`, `stdout`, `stderr`) 綁定及初始化預設的 file descriptor (e.g., `0`, `1`, `2`)。 ```c= #ifdef _IO_MTSAFE_IO # define DEF_STDFILE(NAME, FD, CHAIN, FLAGS) \ static _IO_lock_t _IO_stdfile_##FD##_lock = _IO_lock_initializer; \ static struct _IO_wide_data _IO_wide_data_##FD \ = { ._wide_vtable = &_IO_wfile_jumps }; \ struct _IO_FILE_plus NAME \ = {FILEBUF_LITERAL(CHAIN, FLAGS, FD, &_IO_wide_data_##FD), \ &_IO_file_jumps}; #else # define DEF_STDFILE(NAME, FD, CHAIN, FLAGS) \ static struct _IO_wide_data _IO_wide_data_##FD \ = { ._wide_vtable = &_IO_wfile_jumps }; \ struct _IO_FILE_plus NAME \ = {FILEBUF_LITERAL(CHAIN, FLAGS, FD, &_IO_wide_data_##FD), \ &_IO_file_jumps}; #endif DEF_STDFILE(_IO_2_1_stdin_, 0, 0, _IO_NO_WRITES); DEF_STDFILE(_IO_2_1_stdout_, 1, &_IO_2_1_stdin_, _IO_NO_READS); DEF_STDFILE(_IO_2_1_stderr_, 2, &_IO_2_1_stdout_, _IO_NO_READS+_IO_UNBUFFERED); ... ``` - 系統自帶便利工具程式和標準函式庫,易於使用、維護和擴展。 - 高階的 C 語言使系統程式開發可忽略系統底層邏輯而專注更高階的程式設計,但當需要對底層個別硬體做客製化操作時也不受語言和編譯限制。 > 極具開發彈性又兼顧程式、語言、機器三者任一個性特色,又不失 UNIX 易用、極簡主義的精神 (多虧 1964 年的 Multics 開發經驗),展現設計者對計算機系統的超然理解、游刃有餘的掌握和恰到好處的設計,個性大膽卻心思細膩。UNIX 系統和 C 語言集劃時代工藝、科技、設計於一身,與圖靈機器堪稱當代最高工藝藝術品之一也不為過。 :::warning *"The real hero of programming is the one who writes negative code."* — Doug McIlroy ::: - 透過 Shell 抽象層包裝,充當跨檔案類型、跨程式語言、跨軟體的 command interpreter 和 runtime executive,使用者不再直接面對 kernel,而有 Shell 層代理。 > 是為最早體現動態程式設計核心理念的經典實作,也是第一個 interpreter 概念的實現。使用者從此不必手動去調度 runtime 各個程式執行週期所需要的環境、個別設定、流程安排及錯誤處理,現只要透過統一的 shell 使用者互動界面代理,大大的減輕以往使用者所需具備的底層維護知識、系統運作邏輯和日常重複性工作負擔,甚至可以編寫 shell script 動態的代為處理這些繁瑣的 runtime 使用者任務,如此的極富彈性和使用者友善的動態執行環境和互動界面也與「跨程式」、「跨平台」、「跨裝置」的應用非常契合,從此奠定動態語言、web 生態和跨平台開發的紮實基礎,也才有 1970 後各式應用的蓬勃發展,如移植 Fortran、COBOL 程式到 UNIX 上,以及新應用 yacc (& lex)、lisp、AMPL、awk 等。 :::info **Bell Labs and AT&T's Legends** | ![](https://hackmd.io/_uploads/HyyFi57ZJx.png) | ![](https://hackmd.io/_uploads/HJal2cXZkx.png) | ![](https://hackmd.io/_uploads/HJPz29QbJx.png) | |:----------------------------------------------:|:----------------------------------------------:|:----------------------------------------------:| | ![](https://hackmd.io/_uploads/S1iNaudbkl.png) | ![](https://hackmd.io/_uploads/rkKphO_-Jg.png) | ![](https://hackmd.io/_uploads/ByIqDdOb1g.png) | > [from top-left to bottom-right] *Brian Kernighan*, *Ken Thompson*, *Dennis Ritchie*, *Alfred Aho*, *Steve Johnson*, *Doug McIlroy* (, [*Robert Morris*](https://en.wikipedia.org/wiki/Robert_Morris_(cryptographer)) (son [*Robert Tappan Morris*](https://en.wikipedia.org/wiki/Robert_Tappan_Morris) - [Inet Worm](https://www.youtube.com/watch?v=2QwMv0_Rkec) creator, [YC](https://www.ycombinator.com/) cofounder), [*Lee McMahon*](https://en.wikipedia.org/wiki/Lee_E._McMahon), ...) ::: 更多: - **UNIX Intro:** - [The UNIX Operating System: Making Computers More Productive - AT&T Tech Channel](https://www.youtube.com/watch?v=tc4ROCJYbm0) - [UNIX: Making Computers Easier To Use -- AT&T Archives film from 1982, Bell Laboratories - AT&T Tech Channel](https://www.youtube.com/watch?v=XvDZLjaCJuw) - **Bell Labs / Multics x Assembler (machine-dependent OS) / UNIX x Compiler (portable OS) / [PL/I](https://en.wikipedia.org/wiki/PL/I), [TMG (recursive descent compiler-compiler)](https://en.wikipedia.org/wiki/TMG_(language)), B (from BCPL) x Interpreter, C x Compiler / [PDP-11](https://en.wikipedia.org/wiki/PDP-11_architecture) / Yacc (compiler-compiler) / pipelines / files / grep:** - [Ken Thompson interviewed by Brian Kernighan at VCF East 2019 - Vintage Computer Federation](https://www.youtube.com/watch?v=EY6q5dv_B-o) - ["C" Programming Language: Brian Kernighan - Computerphile](https://www.youtube.com/watch?v=de2Hsvxaf8M) > The history of C: > ![image](https://hackmd.io/_uploads/Bk0iDBE-ye.png =500x) > > (Source: https://unstop.com/blog/history-of-c-language) - [Nerd Talk - Doug McIlroy & Brian Kernighan - Nerdearla](https://www.youtube.com/watch?v=Xe5ffO6Ouwg) - **Others:** - [Bell Labs’ Role in Programming Languages and Algorithms - Simons Foundation](https://www.simonsfoundation.org/event/bell-labs-role-in-programming-languages-and-algorithms/) :::warning **編按:** 1. 降低應用程式開發門檻,使使用者不必操心底層實作邏輯,而可以全心投入上層的應用程式邏輯開發 (就如現在 AI 可期的發展方向)。 2. ==**Operating Systems**== x ==**Programming Languages**== x ==**Computing Machines**==: Every of them is interrelated & has an impact on each other. ::: #### “Write Once, Run Anywhere” 時代的到來 爾後,在 1990 年代程式語言百家爭鳴之際,距 1970 年代 20 餘年的時間,從聚焦「可移植到其他作業系統」的高階編譯程式,到開發「可直接跨平台運作」的 runtime 動態直譯程式,各程式語言為解決自家程式效能不佳的問題,尤其是當時新興的跨平台程式,由於跑的是 platform-independent 的 bytecode,必須使用虛擬機器的 interpreter 和相應的 runtime 系統才得以正常工作,因此效率肯定是比完全的編譯語言差上一截。 > **註:** 當編譯器前端 (「機器無關」部份) 最佳化來到盡頭時,就只能想辦法在後端動手腳了。 :::info **Portable vs. Cross-Platform System** - **Portable 可移植程式語言:** 指有別於由 assembly 撰寫而成的程式,透過編譯器及彙編器代為處理「機器相關」指令實現部份,其中涵蓋不同的機器運作邏輯 (e.g., CISC vs. RISC, servers vs. PCs vs. embedded devices)、底層 ABI (e.g., calling convention, 8 vs. 16 vs. 32 vs. 64-bit processors, big vs. little-endian, ...)、(co)processor 特別功能指令 (e.g., floating point, SSE, VLIW, MMIO, multithreading 邏輯, ...),使程式開發只需聚焦「機器無關」的演算法邏輯。 > 其他更廣泛的底層硬體功能支援則交由「作業系統」來維護 (換言之,取決作業系統實作) 如: > - **擴充硬體設備、功能:** drivers (e.g., for chipset, DMA controller, graphics card, filesystem, NIC, ACPI, ...)、signal、paging、MMU、TLB、multithreading 實作、硬體虛擬化等。 > - **系統軟體服務:** scheduling、dynamic loading、IPC、sockets、enclave、fast syscall、vDSO 等。 - **Cross-Platform 可跨平台程式:** 則是指除了程式碼可在不同硬體上的執行外,程式還保證可存取並兼容不同機器上的系統環境、資源服務、函式庫、動態事件等 (e.g., libraries, utilities, subsystems, networking, IPC, drivers, data/signal handlers, ...)。例如: 可透過在原生作業系統與應用之間以額外虛擬抽象層來實現 (e.g., shell, runtime, framework, middleware, ...)。或本身底層作業系統就存在以跨平台規範實作 (e.g., POSIX API),進而達到「可跨平台執行」程式效果。 :::warning 這與虛擬目標建於 OS 的 **container**、**VM** 甚者可模擬跨機器運作的 **emulator**、**simulator** 的系統軟體實作本質思維不同,因為這些屬於以 OS 角度去解決跨平台跨裝置問題,著重在處理各底層機器及 OS 間 API、ABI、環境兼容、... 等基礎設施問題,而不是在各 OS 範疇內尋求跨平台運作的解決方案。 ::: > **註:** 但由於「作業系統」與「底層機器硬體」往往具高度相關性 (在設計上高耦合),因此具「可移植程式」(跨硬體) 通常兼具備基本的「跨平台運作」(跨 OS) 執行能力。 ::: 更多: - [How similar is the execution of Java and JavaScript? - alexhwoods](https://www.alexhwoods.com/how-similar-is-the-execution-of-java-and-javascript) 在不犧牲跨平台運作及程式便攜性的前提下,當時開發一種可解決動態語言帶來的性能損失的程式加速技術: - **JIT 編譯:** 在程式執行時,分析程式碼執行熱區,(計算編譯成本與繼續直譯的性能損失 tradeoff 後),若可行,則將 bytecode 替換為 native code 執行。 > 同時期也具備 JIT 編譯加速功能的程式語言有 Java, Python, PHP, Ruby, Lua, C# 等。 實作邏輯大致如下: - **Virtual Machines:** interpreter/compiler, code execution, runtime environment support, thread workers, ... - **Interpreters/Compilers:** tokenizing, parsing, optimizing, bytecode generating, ... > 參考 V8 流程架構圖例: > ![image](https://hackmd.io/_uploads/HJERmq8JJg.png =500x) > > (Source: :+1: [V8: Behind the Scenes (November Edition feat. Ignition+TurboFan and ES2015)](https://benediktmeurer.de/2016/11/25/v8-behind-the-scenes-november-edition/)) - **[Choice 1] Directly Interpreter** E.g., [V8's Ignition](https://v8.dev/docs/ignition) ![image](https://hackmd.io/_uploads/ry40KL95Jl.png) ``` source code => (AST, IR, ...) => machine-independent bytecode ``` - **[Choice 2] With JIT Compilation Acceleration** E.g., [V8's TurboFan](https://v8.dev/docs/turbofan) ![image](https://hackmd.io/_uploads/rJQRAB7lJg.png) :::info :arrow_right: For more detailed design of TurboFan, please check out: [Compiler: The Program, the Language, & the Computer Work - shibarashinu](https://hackmd.io/@shibarashinu/SyEHz-JHC#JavaScript-V8-TurboFan-JIT-Compiler). ::: ``` machine-independent AST, IR, bytecode => (IR, ...) => machine-dependent IR, targe code ``` :::warning **JIT Compilation Tradeoffs** While the interpreted language code gets boosted after being compiled, the analysis & compilation process may drastically impact the runtime performance: you have to allocate resources to the compilation, & the target optimized code section isn't prepared to be executed before anything is done. Not saying the dynamic programming language characteristics: code may have been changed and behaves differently than what the JIT compiler expects, which then triggers code fallbacks to be executed by the interpreter again, which may cause security problems if the JIT compiler doesn't handle this situation properly. :arrow_right: For more details, see the below chapter: [Runtime 架構: JavaScript 引擎之 JIT Compiler](#Runtime-架構-JavaScript-引擎之-JIT-Compiler). ::: - **JavaScript Runtime:** event loop, memory management (e.g., GC, sandbox, ...), runtime system environment support, systems engineering, ... :arrow_right: 更多詳細討論,請繼續閱讀「*Runtime 架構*」相關小節。 - **AOT 編譯:** 在程式載入時,將程式盡可能編譯成 native code 的樣子,並預先做最佳化處理。 > 如此,便可預先針對 target machine 直接做 machine-dependent 最佳化,而可完全拋棄 virtual machine 的輔助,產出比 machine-independent bytecode 更貼近機器平台的 target machine code 或 IR。 :::warning **設計思路:** 哪個階段、哪個地方能獲得的「題目提示」越多,就越能先針對該「局部問題」進行事前規劃和分析,提前交出 100 分考卷。 > 考量是要「提前交卷」還是「拿高分」,又或者考慮當下作業環境、應用類別、系統架構、程式特性後再動態決定採取哪種方案 (e.g., hotspots) 等等,這些又是更進一步要考量的系統規劃問題。 *"The more constraits you apply, the more optimizations you gain."* :arrow_right: 更多相關討論,請往下參考: 「[*Is JavaScript still a Thing*](#Is-JavaScript-still-a-Thing)」章節。 ::: ### 作業系統 vs. Runtime 系統 *「系統」設計哲學* 推薦閱讀: [Runtime system - Wiki](https://en.wikipedia.org/wiki/Runtime_system)。 > 參考 JVM 架構圖例: > ![image](https://hackmd.io/_uploads/H1_rSJzk1l.png =550x) > > (Source: [JVM 優化系列(一) 內存區域介紹 - Cogga - iTHome](https://ithelp.ithome.com.tw/articles/10342447)) > JavaScript 引擎與系統週邊支援模組組成之 JavaScript Runtime: > ![image](https://hackmd.io/_uploads/BJCYDvsKyx.png =550x) > > (Source: [Understanding the JavaScript Runtime Environment and DOM Nodes - Vahid Dejwakh](https://vahid.blog/post/2021-03-21-understanding-the-javascript-runtime-environment-and-dom-nodes/)) 系統工程中,計算機上的系統框架和執行行為,終究與圖靈機器和計算機有著密不可分的關係,簡言之就是 `Input -> | Machine | -> Output`。所以無論是 kernel 或 runtime 設計思維和考量都相去不遠,僅只是系統設計的層次高低之差。 如同「虛擬化技術」是由 *虛擬機器 (virtual machine)* 還是 *容器 (container)* 實作;「同步機制」是由 *進程 (process)* 、 *執行緒 (thread)* 甚至是 *協程 (coroutine)* 實現。本質上核心關注的議題都是類似的,僅只於在實際應用於不同環境、層級執行單元中實作上的異同。 套用古典力學概念,即以「分離的自由體圖」來分析,誰是主體、誰是客體,負責哪一層服務的實作和維護,如何交互,舉例: - **作業系統 vs. 使用者程式:** 作業系統提供底層硬體資源管理服務,使用者僅需透過 system call 方能享用硬體資源,在作業系統訂定的標準下 (e.g., POSIX) 在 shell 和 kernel 間穿梭。 但事實上,以系統實際運行角度看待,使用者程式從來沒有真正獲得硬體使用權,從頭到尾都只是作業系統一支程式在運行,使用者程式充其量只是這計算機器上的「輸入」*(就連 ==runtime 系統==、==沙箱系統== 也只是原系統上的一種變體輸入形式!)*,在程式執行時期預期會發生的行為,都只是建立在作業系統會以說好的處理方式而為之。 反觀,以使用者對系統角度看待,作業系統以「硬體服務抽象層」形式包裝給使用者調用,就好像使用者 (程式) 真的擁有: 這是我的 processor (實由作業系統的 process、thread 抽象分配)、我的 RAM 資源 (實為作業系統分配下的局部 danger zone)、我的 hardware (實由作業系統的 driver 代理溝通) 等。 > 作業系統即以此方式實現可應付多使用者的抽象多工分時處理系統。 - **Runtime 系統 vs. 使用者程式:** 又比作業系統更為上層的「程式執行服務抽象層」,因其仰賴作業系統提供的基礎硬體服務才得以正常運作。由程式語言標準 (e.g., C99, ECMA-262) 來對兩者互動進行規範。 使用者程式作為 runtime 系統的「輸入」,所有交互概念都與上述雷同,只是改由 runtime 來負責提供和實現這些服務內容。 :::info **作業系統 vs. Runtime 系統 vs. 使用者程式** 如果開發者想要,或為因應特殊開發需求,其實無需程式 runtime 的協助,僅單單透過作業系統也能實作使用者程式。但因為作業系統 (與周遭設備 driver 擴充) 只管理最基礎、關鍵的服務,接口都過於底層 (e.g., system call APIs, subsystem ABIs, I/O controls, ELF spec, DWARF debug info, GDB protocol, ACPI standard, graphics APIs, ...),因此若有程式 runtime 的幫忙,提供更多功能的包裝點綴 (e.g., calling convention, variadic function call, adapter, dynamic linking, name mangling, RTTI, std library, OOP, ...),甚至具備跨硬體、跨平台的執行能力、polyfill 擴充兼容等特色,都將使程式開發變得更輕鬆省事。 所以綜上所述,談及使用者程式,通常是指在 kernel x runtime 雙重環境包裝下的程式執行,例如:可運行於 Linux 系統的 C 使用者程式指 *Linux* x *glibc runtime*。 「程式碼」(source code)、「程式」(programs)、「系統」(systems) 關係大略如下: 1. 程式碼透過程式語言的語法、型態規範,並由編譯器充當「合法程式」與「預期產物」(e.g., AST, machine-independent/-dependent IR, machine code, programs, ...) 中間的媒介。 2. 關於 ABI 接口、API 接口、跨平台 port 接口、函式庫調用、系統呼叫、... 等透過原生程式語言語法 (e.g., rust 記憶體安全操作)、標準規範 (e.g., headers, sdl files, ...) 與實作 (e.g., cpp files, ...) 來串接「使用者程式」與「執行時期 runtime / 作業系統」兩者。 - **跨平台規範 API 界面:** 系統照著業界規範實作 (e.g., POSIX, ISO C, ...),使程式也能達到 “Write Once, Run Anywhere” *(but recompilation required)* 的 portable 效果。 - **系統獨有 API 界面:** 或由編譯器方面來配合生成除了基本 machine-dependent,還與 platform-dependent 的目標產物,同樣也能達到程式碼 portable 效果。 3. 「runtime / 作業系統」最終將使用者程式按照其系統特定標準作業程序,如期運行「使用者程式」。 ::: 也因為有如此的軟體系統開發迭代,才有今天便捷和多元的使用者程式執行環境。 ### Runtime 架構: JavaScript 引擎之 JIT Compiler 相較於傳統靜態編譯最佳化,JIT 編譯是一種動態程式的解析和轉化,亦即邊做傳統直譯,邊在後端解析程式碼熱區並將其 bytecode / IR 做最佳化編譯,轉換成機器可直接執行的目標機器碼 (target machine code)。 將原本傻傻做無功用的程式,像開竅似的,施予黑魔法做一系列高級操作如: *Basic Block 最佳化*、*常數替換*、*Code Motion*、*Dead Code Elimination*、*Peephole Optimization*、或更會猜接下來的潛在執行熱區並預先將其最佳化等等。 不僅如此,這還只是前菜而已,在後端還能做更多與目標機器有關 (machine-dependent) 各種令人頭暈目眩的最佳化如: *暫存器如何分配*、*指令順序*、*分支安排*、*怎麼有效存取資料 on 特定作業系統 (e.g., *syscall*, *native APIs*) & CPU (*ISA 相關*)*、*軟解變硬解 (指令 or 資料)*、*怎麼有效存取指令 (e.g., 熱區、要併 vs. 不併)* 等。 > 談及最佳化策略,往往是針對效能做最折衷取捨、資源做最大化利用,皆是一體兩面。 :::info :arrow_right: 詳見: [Compiler: The Program, the Language, & the Computer Work - shibarashinu](https://hackmd.io/@shibarashinu/SyEHz-JHC) ::: 更多: - [[Slides] An overview of the TurboFan compiler - V8 Compiler](https://docs.google.com/presentation/d/1H1lLsbclvzyOF3IUR05ZUaZcqDxo7_-8f4yJoxdMooU/edit#slide=id.p) - [從編譯器優化角度初探 Javascript的 V8 引擎 - maxchiu](https://tech-blog.cymetrics.io/posts/maxchiu/turbofan/) - [A cartoon intro to WebAssembly Articles - Lin Clark](https://hacks.mozilla.org/2017/02/a-cartoon-intro-to-webassembly/) #### 安全層面議題 但因涉及底層架構和客戶端程式執行 (i.e. 不可信任),涉及多層安全面向問題: JIT 具體技術實現、語言本身特性、目標作業系統、不同 JavaScript 引擎架構和行為等。例如: :::warning **JavaScript 本身是弱型別語言,會隱性轉型,但如果 JIT 編譯是轉換為強型別語言,所生成程式碼就有可能因為此種語言差異導致的「認知偏差」,產生邏輯漏洞風險。** - [Exploiting Logic Bugs in JavaScript JIT Engines - saelo - Phrack Inc.](https://phrack.org/issues/70/9.html#article) - [[新聞] 北韓駭客駭入 Chromium 漏洞植入 rootkit 程式 Fudmodule - iThome](https://www.ithome.com.tw/news/164801) - [CVE-2024-7971 - National Vulnerability Database](https://nvd.nist.gov/vuln/detail/CVE-2024-7971): **Type confusion** *(Access of Resource Using Incompatible Type)* in V8 in Google Chromium allowed a remote attacker to exploit heap corruption via a crafted HTML page & do remote code execution (RCE). - [[2024/08/21] Security Fixes - Google Chrome Releases](https://chromereleases.googleblog.com/2024/08/stable-channel-update-for-desktop_21.html) - [CVE-2024-38106 - National Vulnerability Database](https://nvd.nist.gov/vuln/detail/CVE-2024-38106): Windows Kernel Elevation of Privilege Vulnerability that can be used as a sandbox escape. :::info :arrow_right: 詳見: [Hardware/Software-Based Security - shibarashinu](https://hackmd.io/@shibarashinu/H1UUR_p-0) ::: ### Runtime 架構: Event Loop & Event-Driven Architecture *體現 JavaScript 運作核心邏輯之載體* Event Loop 與 Async 事件處理是 JavaScript 引擎無法擺脫的原生架構設計:主執行緒使用事件循環來處理非同步操作 (e.g., 使用者互動 `dom.onclick`、網路資源 `ajax.callback`、頁面渲染 `react.flushSync`、... 等)。 如此刻在 JavaScript 骨子裡的 Async 事件處理系統框架,使 JavaScript 原生環境對需要 non-blocking IO 特性的應用極其友善,是為需要「及時」、「動態」處理大量繁瑣任務和 IO 操作時的不二人選。 More: [Event-Driven Architecture Topologies – Broker and Mediator - 3Pillar](https://www.3pillarglobal.com/insights/blog/event-driven-architecture-topologies-broker-and-mediator/) :::info **Event Loop 與 JavaScript** Event Loop (或說 Event-Driven Architecture) 充分體現了 JavaScript 誕生於 Web 應用初創時的核心精神:極少數程式語言從一開始的原生程式語言之作業環境就是設計必須跑在一高級架構及系統環境之上 (大部分程式語言通常只要求可運行在作業系統 shell 之上的作業環境即可),JavaScript 則是一開始便將系統定位在為 Web 應用 (e.g., 瀏覽器、Webview)、GUI 互動界面 (取代同質的 Adobe Flash、Java Applet、桌面環境等) 而生的腳本語言,直至今日仍是這些領域中熱門的生態。但也是因為如此特別 runtime 系統架構,使 JavaScript 難以跨足其他領域,無法像同為腳本語言的 Python 一樣通用。 ::: 通常實作在沙箱環境運作的程序中 (關係就像 kernel vs. user space programs),如此必須解決幾個重要議題: - **High-Level to Low-Level Code 轉換** 程式碼必須轉成特定平台機器碼才能運作 (e.g., 編譯、直譯)。且必須提供最基本的圖靈完備 (Turing-complete) 的環境支持。通常以虛擬機器 (virtual machine) 形式實現。 :::info **JIT Compilation 下錯誤 Rollback** 從目標 low-level code 轉回 high-level 以傳統 Interpreter 執行。 > **Deno Runtime 實作:** [用 Rust 设计高性能 JIT 执行引擎 - 周鹤洋](https://rustmagazine.github.io/rust_magazine_2021/chapter_1/jit.html)。 ::: - **平台資源實現和共享** 作為 high level 系統架構的程式語言,為滿足使用者進階需求,其必須具備存取底層系統服務、硬體資源的能力,提供 runtime 環境中必要的支持。 > 更甚者可設計確保每個個體 (entity) 有公平協作 (cooperative)、優先級搶佔 (preemptive) 等進階多工機制。 - **多客戶端程序管理、協作** 如何讓 JavaScript 能有效發揮現代多核多執行緒計算機架構的優勢,以及如何和其他程序互動、協作、管理等。 - **系統安全** - **免疫於客戶端代碼執行 (Immunibility):** 程式在 CPU & 程序上是否能正常完成指定 tasks,且不會導致系統崩壞或進入不可控狀態 (e.g., undefined behavior, kernel panic, ...)。 - **維護系統資源完整性 (System Integrity):** 設計保護個人/共享/他人的資源免受侵害,如: 程序/執行緒之資源、記憶體、接口、系統公有服務等。 - **Event-Driven I/O Model** 由於 Event Loop 的 Event Dispatcher 具主從處理架構意味,搭配各事件分級 Queue (e.g., *microtask*, *macrotask*)、主程序處理管線、其他可調用的執行緒及服務,其本身就是個應用層級的任務 Scheduler。 - **I/O Models 統整** | | Blocking IO | Non-Blocking IO (return immediately) | |:--------------------------------------:|:----------------------------------------------:|:--------------------------------------------:| | **Synchronous** | `read`, `write` | `read`, `write` (`O_NON_BLOCK` with polling) | | **Asynchronous (yield then callback)** | `select`, `epoll`, `kqueue` (I/O multiplexing) | `aio_read`, `aio_write` (async IO) | > <center> > <div style='display: grid; grid-template-columns: repeat(2, 1fr); gap: .5rem;'> > <img src='https://hackmd.io/_uploads/Sylo-ATJyx.png' /> > <img src='https://hackmd.io/_uploads/rJW3-Rakye.png' /> > <img src='https://hackmd.io/_uploads/HJZAWATJyg.png' /> > <img src='https://hackmd.io/_uploads/S1Oh-0pkkg.png' /> > <img src='https://hackmd.io/_uploads/SJbTWRayJe.png' /> > </div> > </center> > > (Source: [I/O Multiplexing - CSC 209H: Software Tools and Systems Programming - University of Toronto](https://www.cs.toronto.edu/~krueger/csc209h/f05/lectures/Week11-Select-1up.pdf)) Refs: - [Asynchronous I/O - Wiki](https://en.wikipedia.org/wiki/Asynchronous_I/O) - [Taking complete control: why IO multiplexing works well for webservers and GUIs. - Chris Kanich](https://www.youtube.com/watch?v=oD94jnuOHLM) - [Chapter 6. I/O Multiplexing: The select and poll Functions - shichao](https://notes.shichao.io/unp/ch6/) - **各 IO 操作策略在 JavaScript Runtime 中實際應用與對應實作** - 調用在 Renderer Process 的其他執行緒 (透過底層 OS 包裝提供的 thread library 或 JavaScript runtime 的 `Worker` API) 或與其他子 Process 協作,來實現 multithreading 程式設計,以 OS 層級達成 IO 應用的 parallel processing。 - 利用各採用不同 IO model 策略的底層、周圍系統環境,都可在 JavaScript runtime 之上來實現 ==blocking I/O multiplexing==、==non-blocking I/O polling==、==async I/O coroutines==、... 等方案。搭配上述 multithreading 程式設計的並行處理能力,可實作可應付更多樣複雜任務之系統。 > 參考 Redis 架構圖例: > ![image](https://hackmd.io/_uploads/BJZoNKxJyx.png =400x) > (Source: [Why is redis so fast? - ByteByteGo](https://blog.bytebytego.com/p/why-is-redis-so-fast)) 如此,透過作業系統 (non-blocking sockets x event-driven epoll x multi-threaded workers)、user-space library (e.g., *libuv*) 和 JavaScript runtime 之間協作,針對「使用場景」、「跨平台部署需求」等,挑選最合適之: - 調度策略。 - IO 操作方式。 - 開發工具。 - 應用實作層級:*app*, *middleware*, *runtime*, *daemon*, *kernel module*, *eBPF*, *coprocessor*, *supervisor*, ... 等。 便可在有限資源下有效防範 overhead multi-thread thrashing 以及 dead-/live-lock、starvation 等問題。 :::warning **為什麼不一個 socket 對應一個 thread 就好?** 相比開一個獨立 thread 的額外開銷,socket 所需要的資源通常只是需要頻繁溝通但數據量少的使用情境。 因此將這些小 non-blocking socket 全交由一配備 `epoll` 之統一事件處理及分派之單一 process,以「non-blocking async 應用層級多工」取代「blocking sync multi-threading 作業系統層級多工」。 就算真有大 I/O 讀寫或 CPU-intensive 應用需求,也只需要「外包」給獨立 workers 來實現 parallel processing -- 從而將應用分流為兩大需求及處理方式: - **需要頻繁密集與系統交流:** 導流至 async event loop 處理。 - **需要獨占使用系統服務:** 導流至特化 worker thread 集中管理及處置。 如此,也可在應用層級中依使用需求組合實現 sync、async 邏輯: - **sync:** 阻塞式在主線中使用 blocking I/O 或佔用 CPU。 - **async:** 使用 non-blocking I/O 或將任務外派 worker thread,完事再 callback 回主線。 ::: :::warning **Scale Up Event Loop** 若一個 async event loop 主線還是無法應付大量的 socket 處理需求,那便可使用 multithreading 及 load balancing。 > 但須注意 thundering herd 問題,可用 `epoll`: > - edge-triggered mode `EPOLLET` (e.g., 一次性處理 ++ready++ read 直到 ++idle++ `EAGAIN`)。 > - 標註 `EPOLLEXCLUSIVE` 一個事件只帶起一個訂閱該事件之 `epoll`。 ::: :::info **補充:** 在嵌入式 (資源有限) 系統中,甚至不須依賴作業系統提供的 *I/O-based non-blocking / multiplexing* 或 *multithreading* 等包裝,僅僅依靠 *Interrupt-Driven* 架構便可實作簡易版 Event Loop。 ::: ### Runtime 架構: JavaScript 引擎之記憶體管理 在具自動記憶體管理系統的 heap-based 程式語言中,主要利用「資源回收系統 (GC)」的輔助,來協助 runtime 系統處理使用者物件記憶體的完整生命週期: - **分配 (allocate):** 系統處理使用者要求之新物件記憶體空間。 - **識別 / 掃描 (identify、scan、scavenge):** 系統定時標記程式使用中物件,以便追蹤 heap 上動態記憶體使用情形。 - **回收 / 索回 (reclaim、collect):** 系統收回無用之記憶體空間,供下次分配使用。 使用者程式只管做記憶體存 (store) 取 (load) 操作。 :arrow_right: 「*自動資源回收系統*」詳細請繼續閱讀:「[系統記憶體管理與實作策略](#系統記憶體管理與實作策略)」節。 #### JavaScript 引擎記憶體管理系統 :::warning **[系統實作] JavaScript Call Stack / Heap 實作** 除了可直接利用作業系統現成的 C Runtime Call Stack 上來搭建,也可以以更高階的抽象層來實作更 higher-level 的 JavaScript Call Stack,以達到更好的跨平台、高階系統環境支援、語言擴充能力等。 > 同理 JavaScript Heap 的實作:可直接利用作業系統的 malloc 記憶體系統實現,或另實作一 higher-level 的 runtime 動態記憶體系統。 ::: [Visualizing memory management in V8 Engine (JavaScript, NodeJS, Deno, WebAssembly) - Technorage](https://deepu.tech/memory-management-in-v8/) - **Call Stack** :::info **Prgramming Language 角度下的 Stack** - **應用層存取邏輯:** array-like linear 連續完整區塊、靜態大小 literal。 - **系統層實作考量:** 同 segment 空間分配、直接地址存取。 - **圖靈機資源狀態:** 耦合函式呼叫 runtime 邏輯 & 狀態、deterministic 控制 (e.g., OOP 中的 RAII 實作)。 - **資訊安全:** buffer overflow、ROP attacks、race conditions、...。 ::: ![image](https://hackmd.io/_uploads/H16bS4K2C.png =500x) - **JavaScript Engine 實作** Stack frame 中存放有各為 *LIFO* 的 local function context、函式位址、參數、局部變數 (Stack 和 Heap 上) 等。 又因其涉及直接指標存取 (e.g., return address) 和作業系統、硬體機制上限制,無法對 Stack 實行完全封閉、安全的沙箱系統。 - **Value 存放考量** 因 linear 資料存放結構,適合存放靜態、簡單小巧、*Primitives* 固定長度資料型態的數值 (e.g., `String`, `Number`, ...)。 > 因 Stack 上空間小、直接存取快。 - **Heap** :::info **Prgramming Language 角度下的 Heap** - **應用層存取邏輯:** chunk-like 碎片化分散區塊、動態大小 literal。 - **系統層實作考量:** 額外 segment 空間分配 (由底層作業系統規劃 & 管理)、間接地址存取 (i.e., 以系統動態分配資源 ticket / id 索取)。 - **圖靈機資源狀態:** 獨立於函式呼叫 runtime 狀態、nondeterministic (out of control flow)。 - **資訊安全:** use after free、dangling pointers、heap overflow、illegal access、NX bit、race conditions、...。 ::: ![image](https://hackmd.io/_uploads/BkJ3WSF30.png =400x) - **JavaScript Engine 實作** 涉及底層 Memory Management (e.g., *mmap*, *page cache*, *buddy system*, *slab*, ...)、智慧指標連結和 Garbage Collector 參與。另外進階設計可選用沙箱容器隔離。或搭配 string pool、caching、似 LRU 層級式新舊物件操作區域。 > [參考] Linux kernel 中 `malloc` 對應實作: > ![image](https://hackmd.io/_uploads/BkhXWUgT0.png) - **Value 存放考量** 因 hierarchical 指標參照資料存放結構,適合存放動態、空間需求大的數值、*Non-Primitives* 的複雜資料型態 (e.g., `Object`, `function`)。或被其他函式 Closure 涵蓋的變數數值。或跨執行緒使用的全域物件。 > 因 Heap 空間大、間接存取慢。 - **沙箱隔離機制** 類似現代作業系統 *kernel-space* vs. *user-space* 和抽象隔離各 user process 概念,只是由 runtime 系統層面實作,亦即: 將所有系統內可能造成系統危害的危險操作 (e.g., *untrusted user program execution*) 透過額外硬體、軟體方法,對其進行而外管控、處理,以防範 RCE 漏洞、ROP 攻擊等。 為限制任何可能涉及「動態」或「不信任」的危險指標操作,分「信任與否」的記憶體區域操作、共享資源管制,由額外記憶體處理機制「代理 (proxy)」存取隔離環境 (user space) 之資源。 > **Tradeoff 考量:** *安全穩定的系統程式* vs. *額外檢查的效能 overhead*。 :::warning 在 JavaScript runtime 中,沙箱不僅限於限制 heap 中物件指標存取,其他可能危害系統的 untrusted 資源 (e.g., 具威脅性的 client *bytecode / machine code*、具 vulnarability 的程式碼),都可是 sandbox 加緊看管的對象。 ::: - **硬體支援實現** :::danger 硬體實作之沙箱 (e.g., Secure Enclave, TEE, TPM, ...)。 :::info :arrow_right: 詳細請參見: [Hardware/Software-Based Security - shibarashinu](https://hackmd.io/@shibarashinu/H1UUR_p-0)。 ::: :::warning 截至 2024,尚無完整硬體配套措施來實現 user-space 的 sandbox 機制。因此目前所有的 sandbox 方案都是以軟體方式代為實現。 ::: - **軟體資源隔離實現** - **[應用層級] runtime/supervisor 軟體控制** - 優點: 易實作。 - 缺點: 不易 scale、無法利用「硬體虛擬化技術」使資源利用效率差。 - **[作業系統層級] 虛擬化 / 容器化** 即在 host machine 上由 parent process (e.g., `dockerd`, `networkd`, `logind`, ...) 初始化一隔離、有限環境之 child runtime process (e.g., nginx apps),由 OS 提供 runtime 之虛擬化作業、有限資源調度存取。 > 首創以 Protection Rings 實現執行權限分級應用的 OS: Multics > ![image](https://hackmd.io/_uploads/HypAryGZkx.png =200x) > > (Source: [Multics - Wiki](https://en.wikipedia.org/wiki/Multics)) :::warning 拜硬體虛擬化之賜,現代計算機系統上的 container process 除「先天」存取資源受到額外管控限制外,其與其餘正常 process 無異。 ::: :::info :arrow_right: 更多請參見: [Docker Notes - shibarashinu](https://hackmd.io/@shibarashinu/BypbX74Cj)。 ::: #### 系統記憶體管理與設計考量 通常需要考慮的點有: - **記憶體分配與釋放:** 程式語言與底層作業系統搭配如: `malloc`、`free`。 - **記憶體存取安全:** 會不會此程式語言的記憶體管理機制有漏洞,或架構設計上的瑕疵,產生如: use after free、buffer overflow、memory leak 等。 - **記憶體使用效率:** - 減少使用記憶體碎片化 (compact / defragmentation)。 - Cache 機制。 - **多工處理:** - **作業系統軟體層級多工 (搭配多核多執行緒硬體架構):** 並行 (concurrent)、平行 (parallel) 程式設計。 - **處理器硬體層級多工:** SIMD、MIMD、MIMT、... 等。 - **系統穩定性:** 是否能保證在特定時間內交付指定任務、有效 runtime 資源監控與管理、多工保證 thread-safe 程序、... 等。 #### 概論各系統之記憶體管理 無論是 runtime system 或 operating system,在運行系統本身、使用者程式時,難免都需要用到高階的記憶體管理,使系統更方便使用也統一管理,也提升系統整體維護性和安全性。 除非是在特殊場景下有額外設計考量: - **Limited-Resources 的嵌入式系統:** 無法實作 virtual memory、沒有 MMU 機制、沒有 context switch、exokernel、unikernel 等。 - **Time-Critical 的 Realtime 應用:** 保證 high-priority task 可 preempt、worst case 仍可交付 critical 任務 / minimum 需求、低 latency、高可用性等。 - **Safety-Critical 系統:** 所有任務都處於 determinstic 的 stable state、可預測 latency 等。 - **Large-Scale 系統:** 考慮 distributed cache、cluster、locality 等。 - **High-Performance 計算系統:** 考慮 sequential R/W、parallel、throughput、efficiency 等。 - **Disributed 計算系統:** 考慮 ACID、BASE、CAP、lock 機制等。 否則一般應用 (e.g., Web 網頁、應用程式、Daemon) 情境下,使用通用的系統記憶體管理策略和原則足矣。 舉例: - [[Paper] Integrated Hardware Garbage Collection for Real-Time Embedded Systems - ANDRÉS AMAYA GARCÍA, 2021](https://sourcecodeartisan.com/download/phd_thesis.pdf) #### 系統記憶體管理與實作策略 依系統對記憶體資源處理方式,和系統與使用者交互模式,大致可分為: - **Manual Memory Management** 完全信任使用者 / 開發者,將底層系統之記憶體管理接口全部開放 (或說不做任何限制),全權交由使用者操作管理。例: C/C++ 的使用者程式。 換個角度想,C 語言本就是被用在設計實作 kernel 的工具,其語言特性和目標作業系統及 C runtime 的實作緊緊相依。程式即是在這些基礎上直接運行,因此程式運作的邏輯也完全反應了底層系統的設計思維。 > jserv:「你是個大人了,你必須對自己產出的東西有 100% 的掌握和負責。而 C 語言就是把你當成熟的大人看待,它把系統所有權力、責任和義務都交付於你。」 - 使用 *buddy system*、*slab* 做 fine-grained 記憶體篩選。 - 從 free list 回收區中尋找 best/first-fit 的記憶體配置。 - 使用 *mmap*、*brk* 做 coarse-grained 記憶體調整。 - 使用系統提供的 thread 實作將 stack 上資源分開。 :::info :arrow_right: 更多: [Multithreading Kernel Implmentation - shibarashinu](https://hackmd.io/@shibarashinu/r11l4Wkyke) ::: - **Resource Acquisition is Initialization (RAII)** 透過程式語言的設計,由編譯器自動安排安全的記憶體分配、釋放計畫。將 runtime 下程式碼記憶體資源的使用情形,在此資源使用之初,即與「具明確靜態生命週期 (well-defined static lifetime)」的宣告實體綁定並初始化。例如: C++ 的 class 建構子與解構子、stack frame、context、scope、物件、進程、執行緒 ... 等。 :::success 解決了 memory leak 的情況。 ::: :::danger 但仍無法阻止 use after free 的 dangling pointer 發生。 ::: [In what ways does C++ RAII differ from automated garbage collection? - Quora](https://www.quora.com/In-what-ways-does-C-RAII-differ-from-automated-garbage-collection) > 由於要求物件靜態生命週期,所以 RAII 只能用在 stack 資源,無法套用在 heap 上。 - **Compile-Time Automatic Reference Counting (ARC)** 在 RAII 設計之上實作「靜態自動記憶體管理」的智慧指標。由語言自身的編譯器支援或額外的 library 提供。 :::warning 但開發者仍需自行解決 *cyclic references* 的問題: 在指標存有指標的情境中,互相指認,造成無法釋放的循環。 解法如引入 `std::weak_ptr` (可指但不增加 reference count) 等。例: Objective C、Swift、C++ 的 `std::shared_ptr`。 ::: - **Compile-Time Ownership & Lifetime Mechanism** 在 RAII 設計之上實作「靜態自動記憶體管理」。以程式語言設計嚴格定義物件的歸屬 context,以此作為此物件是否還「存活」之依據。當物件不在被 owner 所擁有時,即釋放此資源。 且一對一的數值與 owner 關係,搭配 move 和 borrow 的概念,使得程式在程式語言位階便有嚴格的 alias 管控,與 thread safety 的特性 (嚴防 race condition)。如此的語言設計哲學,簡化變數與記憶體的關係,帶來以下好處: - **開發者面向:** 提昇了程式安全。 - **編譯器面向:** 增加最佳化發揮空間。 但也必然帶來一些不便,如: 程式設計變得複雜、限制多、無法真正的「共享」記憶體等。 > **編按:** 與 C++ 可利用 `std::move` 將 rvalue 用 rvalue reference (`type&&`) 包裝,並將此 rvalue 的 ownership 轉移給目標 lvalue 變數,概念上相呼應。 :::info :arrow_right: 更多: [[C++] Your Rvalue Isn't Meant to Be the Rvalue - shibarashinu](https://hackmd.io/@shibarashinu/By3XL37d0) ::: 資源: - [Resources for learning Rust for kernel development - LWN.net](https://lwn.net/Articles/990619/) :::warning 但 Rust 也提供 bypass 機制,只需宣告註明 `unsafe` 區域即可。 ::: :::success 有效避免了 dangling pointer 的情況。 ::: :::info **C vs. Rust for Linux kernel 開發方向討論** - [Linus Torvalds: Speaks on the Rust vs C Linux Divide - SavvyNik](https://www.youtube.com/watch?v=0bb3-bjgf88) - [Rust Compiler Not Stable Enough for Linux Kernel Development - Bryan Lunduke](https://www.youtube.com/watch?v=bD_dJ7OSUuA) - [Committing to Rust in the kernel - LWN.net](https://lwn.net/Articles/991062/) - [Linux 核心採納 Rust 的狀況 - linD026](https://hackmd.io/@linD026/rust-in-linux-organize) ::: 更多: - [Rust and RAII Memory Management - Computerphile](https://www.youtube.com/watch?v=pTMvh6VzDls) - [Resource acquisition is initialization - Wiki](https://en.wikipedia.org/wiki/Resource_acquisition_is_initialization) - **Automatic Memory Management (AMM)** 不信任使用者,也方便開發者以使用者角度來管理記憶體資源。其以作業系統或應用層 runtime「動態」管理記憶體分配 (allocate) 與釋放 (deallocate) 事宜。 [Automatic Memory Management - Techopedia](https://www.techopedia.com/definition/27271/automatic-memory-management-amm) **Runtime Garbage Collector (GC)** 使用獨立的「資源回收」系統來做記憶體管理。負責所有使用者物件的生命週期。作為系統封裝於內部的基礎設施,其無法任意被使用者存取、變更。 > 在程式語言中,採用「自動記憶體管理」的 runtime 系統通常都使用 Garbage Collector 機制來實作,例如: JavaScript、Python、Java、C#、Go、Ruby、Lisp (Common Lisp、Scheme) 等。 - **Reference Counting (需配套解 Cyclic Reference)** 簡單、效率高的 reference counting 智慧指標實作。 :::warning 仍存有 *cyclic references* 資源無法釋放問題,因此若無其他 workaround,此設計不可用在 GC 上。 ::: - **Traced-Based GC** 透過具記憶體追蹤和分析能力的動態管理系統 (可為獨立 process),幫助系統清除無用記憶體。 - **Stop-the-World** 當 runtime 系統之記憶體區塊已不敷使用者所使用 (可能因為碎片化佔用、殘骸尚未清除),必須暫停整個系統運行,做資源回收和清除後,始能重新開張。 - **Mark & Sweep (& Compact)** 將記憶體週期分為: *free* (可被分配)、*unreached* (被佔用但尚未被 root 探訪)、*unscanned* (被佔用被 root 探訪但 reference 還未更新)、*scanned* (被佔用被 root 探訪也有最新 reference)。 > 可額外記錄 allocated chunk list、free list 資訊來避免對整個 heap 做全局掃描 (Baker's Algorithm)。 1. **Mark:** 標記「倖存物件」。 1. 先假設所有記憶體物件為 *unreached* 狀態。 2. 從 root 探訪所有可觸及節點並標為 *unscanned* (DFS / BFS 的 graph 探索),再將這些節點的 reference count 更新成正確版本,標記為 *scanned*,並可用一 linked list 串起這些 live objects 以做後續處理。 到此,系統中記憶體標記只剩 *unreached*、*scanned* 兩種型態。 :::warning **從 root 直接尋訪所有倖存物件 push 到 linked list 上就好啦,為何還要分 *unscanned*、*scanned* ?** 因為可能存在「循環指標」,必須區分是否已訪問過此節點。 ::: 2. **Sweep:** 清除無用記憶體空間佔用。 1. 將所有 *unreached* 轉為 *free*。 2. 釋放記憶體空間。 3. (將所有記憶體物件再標為 *unreached*,為下一次 GC 做準備。) 3. **Compact (Optional):** 減少記憶體長期使用後的 *fragmentation* 問題。並將同性質資源 (e.g., 密集存取 vs. 疏散存取) 做集中 GC 操作和集體管理。 「Compact 記憶體的操作策略」又可細分為: - **原地搬移:** 將瑣碎的記憶體區塊一一 `memcpy` 到事先規劃的位置。 ![image](https://hackmd.io/_uploads/B1CY6vxykg.png =300x) - **兩地交換:** 將記憶體分兩等份,直接將瑣碎記憶體區塊 `memcpy` 到另一等份的指定位置,回合來回交換。 ![image](https://hackmd.io/_uploads/HJ-8sulykl.png =300x) - **技巧性複製:** 見接下 *Train Algorithm* 方法。 實作: - **Mark-&-Compact** 即在 stop & copy 資源回收機制基礎上,只改變 Sweep 階段操作: 1. 將記憶體分兩半,runtime 運行時只使用其中一半,當塞滿時便暫停所有程式執行,以進行 GC 操作。即從 root 開始 traverse 可觸及節點,並將他們進行 deep copy 到新區域 (順便 ==Compact 記憶體空間==)。 2. copy 過程中可能會參照到舊節點,所以 deep copy 後尚須將舊 pointer 指向新分配記憶體的 pointer (可用額外 table、hash table 做參照)。 3. 待所有「倖存」物件都被 copy 過去,即可 *flip* 記憶體作業區,恢復原程式執行。 更多: - [Cheney's algorithm - Wiki](https://en.wikipedia.org/wiki/Cheney%27s_algorithm) - **Short-Pause** 在 stop-the-world 基礎上,透過更 fine-grained 的 GC 操作和調度策略,減少及分散「mark」階段的工作量,盡量使程式不受 GC 而中斷。 例如區分、細分使用者及系統 GC 操作行為,非不到衝突狀況不去干涉使用者存取 heap 上記憶體資源,從而使整體 gain 提升,像是: - **Partial Collection** 分區執行,使 stop-the-world 不再每次執行就是來一波大的 downtime 嚴重損耗性能。 - **N-Generation Garbage Collection** 即在 stop & copy 資源回收機制基礎上,透過細分各 generation 之回收區域。達到多工資源回收處理,和給予 young generation 多一些的「回收照顧」。 通常實作方法為: 從 partition $p_0$ 開始填,當 $p_i$ 填滿或過閾值時,對 $p_0$ ~ $p_i$ 的區域進行 GC 操作,並將相對舊物件 (依判斷標準而定,見備註) 遞延至下一層 $p_{i+1}$ 存放。 > **註:** 倖存物件從 young 轉變為 old 階級稱為「晉升 (promotion)」或「老化 (aging)」機制,通常以判斷此物件被「copy 次數」實作。 :::info 從 *Generational Hypothesis* 中,我們可以預期 *“Most objects die young”*,意味著越 old generation 越不需要變動。 ::: ![image](https://hackmd.io/_uploads/rkShUHF6R.png =400x) :::warning **Generational GC 問題 & 最佳化策略** 從上述 N-Generation Garbage Collection 實作介紹可知,進行 GC 操作時,當最後一層即將填滿時,回收系統便需付出巨大成本: 所有世代 partitions 必須全跑一遍、長壽物件需多次遞移 ... 等。 極為考驗 GC Scheduling 演算法在 runtime 下的調度策略,例如設計對系統友善的回收實作,或盡可能動態配合當前程式記憶體使用習慣。 舉例: - **對於 young 物件區域:** - 因 dead object 多,區域內記憶體配置變動大,與其慢慢標記、整理、收集,不如整批直接換掉重作更快 (copy collection)。 - 借鏡參考 (固態硬碟上) SLC cache、RAM disk、Memcached、Redis 技術:先利用大區域記憶體充當一級戰場的前端粗糙快速存取區域 (想像如 TLC 作 SLC 用,並作為寫入寫出之 DRAM 替代方案的緩衝區),再配合 LSM Tree、bloom filter、multi-level hash table 等盡可能最大化 sequential write / random read 的效率。 - **對於 old 物件區域:** - 由於變動少,因此可改成記憶體寫入時,主動將 GC pages 標記為 "dirty" 再處理,如此的 *write barrier* 可由以下方法實現: - **硬體 (高效):** 透過 kernel 調校 MMU 硬體 policy 來聯動 GC pages 處理機制。 - **軟體 (低效):** 透過 runtime、compiler 及作業系統的協助 以軟體方法觸發對 GC pages 寫入標記事件。 :::info **可參考 Linux kernel 的 `userfaultfd` 機制:** ![image](https://hackmd.io/_uploads/ryCPqZsMyg.png =300x) - [[Slides] Userfaultfd: Post-copy VM migration and beyond - Mike Rapoport](https://blog.linuxplumbersconf.org/2017/ocw/system/presentations/4699/original/userfaultfd_%20post-copy%20VM%20migration%20and%20beyond.pdf) - [[Race Condition] userfaultfd 的使用 - CTF Wiki](https://ctf-wiki.org/pwn/linux/kernel-mode/exploitation/race/userfaultfd/) - [userfaultfd(2) - Linux manual page](https://man7.org/linux/man-pages/man2/userfaultfd.2.html) - [/fs/userfaultfd.c - Elixir Bootlin](https://elixir.bootlin.com/linux/v6.12/source/fs/userfaultfd.c) ::: - **對於中場暫停的回收操作:** - 多「追蹤記錄倖存物件」,少「回收操作實際記憶體」,以減少 runtime 暫停機會。 - 欲處理 $p_n$ 區域時,若已知 $p_k\text{ (k < n)}$ 以前不會影響後續 partition 層級回收操作,則只需對 $p_{k+1}\text{ , ..., }p_{n-1}$ 處理即可。 - 使用其他 *Short-Pause* GC 調度技巧 (見以下)。 - 使用 *Train Algorithm*。 - 使用 concurrent / parallel GC 操作。 Refs: - [[Paper] Barrier Methods for Garbage Collection - Benjamin Goth Zorn, 1990](https://scholar.colorado.edu/concern/parent/47429970d/file_sets/kp78gh35d) - [Generational Garbage Collection, Write Barriers/Write Protection and userfaultfd(2) - Martin Cracauer](https://medium.com/@MartinCracauer/generational-garbage-collection-write-barriers-write-protection-and-userfaultfd-2-8b0e796b8f7f) ::: :::danger **結論:** 各種 GC 操作技巧都必須在 *scalibility*、*time*、*space* 效率間做取捨! ::: - **Incremental Collection** 將部份 operation 做更精細的拆分,一次次累積最終完成整體工作。達到更好的 responsiveness 和 scalibility。 - **Train Algorithm** :::info **[[Paper] Incremental Garbage Collection: The Train Algorithm - Thomas Würthinger, 2006](https://www.ssw.uni-linz.ac.at/General/Staff/TW/Wuerthinger05Train.pdf)** Train Algorithm 為一 *genarational GC* 設計,目標是增進分區細緻 GC 操作和 sequential pattern access 的機會。實作特色包含: - 特別對 write 操作綁定 GC 機制設計。 - 捨棄 from-to 區域交換。 - 採用 write barrier 而非 read barrier。 - 以拆成各「block」的分區記憶體實體 (自訂空間上限),作為 mark-&-copy 的操作單位。 > 更可以此做 Distributed GC 的基礎! - 不同傳統以 copy 次數作 *promotion* 依據,這次採用 reference 引薦提拔。 - 可利用 reference count 作最佳化 *promotion* 依據。 - 可利用 block 和 partition 作為 heap 分類依據,並透過額外資料結構輔助提升查找效率。 from: ![image](https://hackmd.io/_uploads/HkBFTDV1Je.png =300x) to: ![image](https://hackmd.io/_uploads/B1y_6DVkke.png =300x) to: ![image](https://hackmd.io/_uploads/H1GKS3Nkyl.png =300x) > **車廂 (car):** 記憶體 block 分區。為 generational GC 的 partition;**列車 (train):** 一串記憶體 block 區塊。 1. **分配 (Allocate):** 使用者程式宣告新物件。 - 安插於 ==最後== 一輛列車的 ==最後== 一節車廂,若額滿則 ==往後== 編一節新車廂或一輛新列車。 並且在 block 中,小物件用「堆疊」,大物件用「關聯」。 > 設計考量又回歸類似 stack vs. heap 的哲學討論。 2. **計數參考 (Reference Count):** 在物件參照或刪除時增減 reference count。 - **Remembered Set:** 為一「car / train」上的 *set* 資料結構,記錄從外面的 root 和 ==往後==「car / train」指向內部物件的所有參照者。 > **複習:** 使用 reference counting 將復現 cyclic reference。 > ![image](https://hackmd.io/_uploads/ryW0kh41kg.png =300x) 4. **回收 (Collect):** 只對 ==第一== 輛列車進行處理,每次回收處理 ==最前== 一節車廂。 > 到最終 ==第一== 列車再無對外 reference 連結,銷毀一整輛列車。 根據隨車廂的 *remembered set* 將所有倖存物件「複製 (copy, rescue)」到指定區域後,銷毀此車廂,分兩情況: - **Remembered Set 只有「root」或「第一輛列車上其他車廂的倖存物件」之參照者:** 將該物件安插於第一節車廂 ==往後== 任一可填入車廂之空位,若額滿則 ==往後== 編一節新車廂或一輛新列車。 - **Remembered Set 有「其他列車上的物件」之參照者:** 將該物件安插於同其 reference 物件的列車的 ==最後== 一節車廂,若額滿則 ==往後== 編一節新車廂或一輛新列車。 > **注意:** 此參照可能來自於往後列車上已失去生命之物件 (`reference_count = 0`),而不是倖存物件。 :::warning **核心精隨: 將 Cyclic Reference「洗」到同一列車上** 由於互相參照者最後都會落腳同一輛列車,而同一輛列車只關注對外 reference set,如此解決仍被已死指標參照的物件問題,更解決了 cyclic reference。 **限制:** 列車容量必須大於一 cyclic reference 上的所有參與指標物件體積總和,才可以解掉。 > 彷彿俄羅斯方塊將互相關聯的物件都放在同一層中,並在沒「對外觸角」延伸時消掉一整行方塊。 > ![image](https://hackmd.io/_uploads/rJPoo_4JJl.png =300x) ::: - **Concurrent Collection** 對多執行緒下進行更 fine-grained 的獨立操作分類: *nth-level regions*, *concurrent / parallel*, *mutators vs. collectors* 等。 - **實例:** - **JavaScript V8's Minor GC** [Minor GC (Scavenger) - V8 Dev](https://v8.dev/blog/trash-talk) ![image](https://hackmd.io/_uploads/r1grqXt6A.png =400x) - **CPython's Garbage Collection (Cycle Collection)** - 含 GIL 鎖 GC 實作 (for legency / 單執行緒 apps): [cpython/Python/gc.c - GitHub](https://github.com/python/cpython/blob/main/Python/gc.c)。 - 無 GIL 鎖 GC 實作 (for modern / 多執行緒 apps): [cpython/Python/gc_free_threading.c - GitHub](https://github.com/python/cpython/blob/main/Python/gc_free_threading.c)。 :::info **Python 3.13 拋棄以往 Coarse-Grained 的 Global Interpreter Lock (GIL) 設計,解放多執行緒性能:** > 由於上個世紀的電腦最多就是單核 CPU 處理器虛擬化技術 (即 concurrency),運行其上的程式都假設系統不會有 race condition 問題,導致後來無論作業系統、runtime 系統都不得不在處理程序上加一把鎖以維持使用者程式 / drivers 的正常運作,甚至可能直接扼殺多核能力變單核處理器。 這也迫使原本在 CPython 中 Generational GC 的設計,放棄對 young 和 old partition 的差別回收機制。 因為對 young partition 頻繁的 stop-the-world GC 操作,反而造成 multi-threaded 的平行處理 scalibility 上不去。 根據 [PEP 703 – Making the Global Interpreter Lock Optional in CPython](https://peps.python.org/pep-0703/) 描述: > The existing Python garbage collector uses 3 generations. When compiling **without the *GIL***, ++the garbage collector will only use a single generation++ (i.e., becoming non-generational). The primary reason for this change is to ++reduce the impact of the stop-the-world pauses in multithreaded applications++. Frequent stop-the-world pauses for collecting the young generation would have more of an impact on multi-threaded applications than less frequent collections. 更多: - [【python】天使还是魔鬼?GIL的前世今生。一期视频全面了解GIL! - 码农高天](https://www.youtube.com/watch?v=XjBsk8JGHhQ) - [【python】听说因为有GIL,多线程连锁都不需要了? - 码农高天](https://www.youtube.com/watch?v=Flce9y5Qn38) :::warning 類似 Linux kernel 中的 *[Big Kernel Lock (BKL)](https://kernelnewbies.org/BigKernelLock)*。 ::: - **JVM's The (Mostly) Concurrent Mark Sweep Garbage Collector (CMS GC)** ![image](https://hackmd.io/_uploads/HJemXEsa0.png =400x) ![image](https://hackmd.io/_uploads/Skcr4kG1Jg.png =400x) [Garbage collection in Java, with Animation and discussion of G1 GC - Ranjith ramachandran](https://www.youtube.com/watch?v=UnaNQgzw4zY) [Introduction to Java Garbage Collection - Purrgramming](https://purrgramming.life/cs/cs-topics/gc/) JVM CMS GC 操作分為: - **Initial Mark (Short-Pause):** 僅標示位於 root 的倖存物件。 - **Concurrent Mark:** 遞迴尋找並標示倖存物件。 > application threads 無需暫停工作。 - **Concurrent Preclean:** 處理倖存物件 aging、新物件的記憶體分配問題及剛才 application thread 又新加入的物件 (即 write barrier 防護)。 > application threads 無需暫停工作。 - **Concurrent Abortable Preclean:** 同上一個工作內容,為延長此階段作業時間,使 GC 可以固定週期、預期效率 (e.g., 超過閾值) 執行。 > application threads 無需暫停工作。 - **Remark (Short-Pause):** 從 root 完整執行一遍 Mark 階段,將可能漏掉的倖存物件補齊。 > 前述 concurrent 的工作都是為了減輕此步驟的負擔。 - **Concurrent Sweep:** 釋放無用記憶體資源。 > application threads 無需暫停工作。 JVM CMS 有以下幾點特色: - 在 concurrent 的 GC 操作中不去 copy 和 compact 倖存物件 (減少觸碰 critical section 導致暫停)。 - GC 操作也傾向不去 move 倖存物件。 :::warning **如何處理記憶體 Fragmentation 問題?** JVM CMS 傾向直接索取更大的 heap 空間,犧牲 space 換取處理效率。 > *「[以空間換取時間](https://en.wikipedia.org/wiki/Space%E2%80%93time_tradeoff)」。* ::: :::info **JVM CMS 調度策略** - **Heap 空間額滿:** 在預設下,當超過整體程式運行時數的 98% 都耗在 GC 操作上,而得到的收穫卻不及整體 heap 容量的 2% 時,便會觸發: ``` OutOfMemoryError: GC overhead limit exceeded. ``` > JVM 中,關於 CMS 回收機制有許多應對策略可用來配合程式行為以改善整體效能,例如: > - 增加 heap 空間。 > - 調校 CMS 調度政策 (e.g., `UseConcMarkSweepGC`, `UseParallelGC`) 等。 > - 利用 `HeapDumpOnOutOfMemoryError` 來分析記憶體使用情形。 - **Remark 執行區間設置:** 為避免「各 generation partition 都不約而同集中在某時間區間上做 GC 操作」,因此刻意將 $p_{old}$ 交錯於 $p_{young}$ 的時間間隔之間進行。 > 因 remark 暫停效果影響效能最鉅,所以特針對此階段做調度。 參考: - [Concurrent Mark Sweep (CMS) Collector - Oracle Docs](https://docs.oracle.com/javase/9/gctuning/concurrent-mark-sweep-cms-collector.htm#JSGCT-GUID-FF8150AC-73D9-4780-91DD-148E63FA1BFF) ::: 更多: - [JVM调优实战: 解决CMS concurrent-abortable-preclean LongGC的问题 - 码农架构 - 腾讯云](https://cloud.tencent.com/developer/article/1725984) - [JVM Deep Dive - Binary Coders](https://binarycoders.wordpress.com/tag/deep-dive/) - [Generations - Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide - Oracle](https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/generations.html) ![image](https://hackmd.io/_uploads/B1kebNKaA.png =300x) :::warning **結論:** 綜合以上 GC 調度策略,可知每個 GC 都有其優缺點,可互補搭配,使系統有更健全可靠的回收生態。 > 參考 JVM HotSpot VM 的 GC 架構圖例: > ![image](https://hackmd.io/_uploads/S1hHq54kJe.png =300x) ::: :arrow_right: 關於 Automatic Garbage Collection 帶來的效能問題、解決方案及延伸討論,請往下參考「[談 JavaScript 劣勢](#談-JavaScript-劣勢)」節。 參考: - [Demystifying memory management in modern programming languages - Technorage](https://deepu.tech/memory-management-in-programming/) - [Tracing garbage collection - Wiki](https://en.wikipedia.org/wiki/Tracing_garbage_collection) - [Garbage Collection - Stanford InfoLab](http://infolab.stanford.edu/~ullman/dragon/w06/lectures/gc.pdf) - [Garbage Collection and the Other Kind of Heap - Data Structures and Algorithms with Object-Oriented Design Patterns in Java - Bruno R. Preiss](https://book.huihoo.com/data-structures-and-algorithms-with-object-oriented-design-patterns-in-java/html/page414.html#SECTION0014000000000000000000) - [Mark and Sweep Garbage Collection Algorithm - Arpit Bhayani](https://www.youtube.com/watch?v=4qLf0FJMyf0) ### 談 JavaScript 劣勢 當然凡事都是一體兩面,這些看似 JavaScript 的優點,反過來就成了他最大的短板。由於其程式語言和 runtime 架構的特殊性,論優缺點和適合的應用都是在光譜中兩端的極限。 > 可分為「*新語法糖/規範/擴充可解*」和「*沒救,請換個程式語言/系統生態*」。以下就這兩種情形討論分析。 #### JavaScript Standard 版本補丁 - **Callback Hell:** 回呼處理程序 (Callback) 是 event-driven 架構中必不可少的特色。因為有了 Callback 使整個程序變得更加動態,程式不必寫複雜的流程控制也能輕鬆跳轉程式的 state (以低階語言描述就是 *jump labels*)。 因為在 JavaScript 中 function 本就視作 `Object` *(first-class function)*,因此可將 function 作為 subroutine 的參數,並以 Callback 程序身份進行處理。 針對 Callback 機制,JavaScript 可以用程式語言的階層給出相對應的實作方法,並完美融入其 Event Loop 系統運作中,在此也體現了 *Generative Programming* 的精神。 - **Classic Callback:** 但若要在異步處理中定義 Callback,便可能 (也只能) 產出這樣醜陋的程式碼: ```javascript= // Declare the function that has callback. function func(arg, callback) { ... callback(event); } // Callback hell func(arg0, function(event) { ... func(arg1, function(event) { ... func(arg2, function(event){ ... }); }); }); ``` :::info **編按:** 想起以前使用 *jQuery Library* 開發順序 animation 路上必經如此程式碼風格的洗禮! jQuery: ```javascript= $("button").click(function() { $("p").hide("slow", function() { $( "#block" ).fadeIn( "slow", function() { // animation done }); }); }); ``` 另外,在 *C++* 尚未支援 `std::async` 前,也常常需要以匿名函式充當以 `std::function` 為參數的 callback 傳遞。如: C++: ```cpp= void DesktopWindow::animDoge() { // left hide iDoge->animate(Animate({ Left(5,vw), Bottom(-5,px) }, 1000, Easing::easeInCubic), [this]() { iDoge->getAnimate().sleep(500); iDoge->setZindex(100); // middle show iDoge->animate(Animate({ Left(18,vw), Bottom(0,px) }, 2000), [this]() { // simulate sleep iDoge->animate(Animate({ Left(18,vw), Bottom(0,px) }, 1500), [this]() { // right hide iDoge->animate(Animate({ Left(25,vw),Bottom(-5,px) }, 2000), [this]() { // right to the back iDoge->animate(Animate({ Left(25,vw),Bottom(-10,px) }, 200), [this]() { iDoge->setZindex(0); animDoge(); }); }); }); }); }); } ``` ::: - **Promise:** 於是將異步程序多一層包裝,使 Main Thread 可即時追蹤和調用的 `Promise` & `then()` (ECMAScript 2015) 便派上用場: ```javascript= // Declare the function that returns an async Promise item. function func(arg) { ... return new Promise(r => r(event)); } func(arg0) .then(function(event) { ... return func(arg1); }) .then(function(event) { ... return func(arg2); }) .then(function(event) { ... }); ``` - **Async / Await:** 更簡潔的語法還有 `async` / `await` (ECMAScript 2017),也是將異步程式用既有 `Promise` 上包裝,但語法上可徹底拋棄 Promise Chain。 :::info **[詳細實作比較] Async vs. Promise** - **Promise:** 所有 Promise 都是被列在一 function context 下的「可執行物件」,必需儲存所有 context 下資訊直到函式所有參照之 Promise 等物件 return 釋放。 - **Async:** 從底層 runtime 層級支援「suspended」函式呼叫,不必以一 function closure 形式去儲存各 Promise 實體。 [Asynchronous stack traces: why `await` beats `Promise#then()` - Mathias Bynens](https://mathiasbynens.be/notes/async-stack-traces) ::: - **Await Yielding:** 在異步程序需要同步阻塞時,利用 `await` 將異步程式從 Main Thread 上斷開,來實現阻塞停等的效果。 > 這裡相當於 Generator 的 `yield` 效果。 - **Callback Resumes:** 當停在 `await` 的 Promise `resolve` 時,再將剩餘的程式碼 (相當於 `then()` 的 Callback 程序) 放入 *microtask* 佇列中等待 Main Thread 繼續執行。 > 這裡相當於 Generator 的 `next()` 效果。 - **Async Function:** 將看似「同步寫法」的程式碼,用 Promise 包裝成實為「異步執行」與「Callback」的程序,換言之,在 Main Thread 和 Event Queue 來回交錯的程序。 > 使用 Promise 包裝的語法糖,讓原本需要寫層層呼叫異步處理 Callback 的程序,變成一行行同步順序寫法。 ```javascript= async function async_func() { /* * In async function, * the return value will be wrapped in a Promise object, * so this async function will be equivalent to: * * function async_func() { * return Promise * .resolve(func(arg0)) * .then((event) => {...}) * .resolve(func(arg1)) * .then((event) => {...}) * .resolve(func(arg2)) * .then((event) => { ... return 0; }); * } */ let event; event = await func(arg0); ... event = await func(arg1); ... event = await func(arg2); ... return 0; } const promise = async_func(); ``` :::info **小結:** 這些看似沒有關聯的語法糖,實際全是為了解決: > 當異步處理結束時,回 Main Thread 上等待處理的 Callback 程序 — 對異步程序添加一些高級包裝外,其他並沒有什麼特別的。 ::: - **Try Catch in Async:** 在解決 Callback Hell 之後,緊接而來的是與「try catch 錯誤處理」相關的問題: - `Promise` 的語法糖包裝 (callback in `.then(...)`) 的錯誤處理必須以 `.catch()` 處置,這與傳統「同步寫法」的 `try ... catch` 邏輯很是不同,而且允許省略,這導致此 `Promise` 異步錯誤處理更容易被忽略,或錯誤認知下產生誤操作: :::warning **開發者對「同步」和「異步」Try Catch 的錯誤認知** 若異步 `Promise` 沒有 `await` (從主線斷開後再排入 *microtask* 佇列中回主線繼續完成),而 Error 是在 `then()` 後拋出,但 try catch 是定義在 `async` 函式中,一個以「同步寫法但實際為異步交錯執行的程序」,於是 `then()` 在這就失去「同步寫法」的意義。 > 相當於錯誤是在 `then()`「未來」,而 try catch 是在「同步寫法」下的「現在」。 ::: 一個 `Promise` x `then()`、一個 `async` x `await`,兩者求的都是 Callback 的效果,但實際程式碼卻是不同的執行邏輯 (異步、同步);加諸第一點所述 `catch()` 容易被忽略。如此便極可能寫出一個誤以為 try catch 會捕捉 Error,實際上 Error 卻被忽略的危險程式碼。見以下示例: ```javascript= (async () => { try { // Promise without await // (Nothing caught at all !!) new Promise(r => setTimeout(r, 1000)) .then(() => { throw new Error("Error without await"); }); // Promise without await & with .catch // (Caught by promise's catch) new Promise(r => setTimeout(r, 1000)) .then(() => { throw new Error("Error without await"); }).catch(e => { console.log(`Promise's catch: ${e.message}`); }); // Promise with await & with .catch // (Caught by promise's catch) await new Promise(r => setTimeout(r, 1000)) .then(() => { throw new Error("Error with await"); }) .catch(e => { console.log(`Promise's catch: ${e.message}`); }); // Promise with await // (Caught by async function's catch) await new Promise(r => setTimeout(r, 1000)) .then(() => { throw new Error("Error with await"); }); } catch (e) { console.log(`Async function's catch: ${e.message}`); } })(); ``` 如上 async function 中未被捕捉的例外可被等價改寫為: ```javascript= (async () => { return Promise .resolve() .then(() => { new Promise(r => setTimeout(r, 1000)) .then(() => { throw new Error("Error without await"); }); }) .then(() => {...}); .catch((e) => { console.log(`Async function's catch: ${e.message}`); }); })(); ``` 由此可清楚看出: `catch()` 與實際 Error 是在不同 Promise 層啊! - **[Solution] Safe Assignment (?=):** [You Don't Need Try/Catch Anymore With This New Operator - CoderOne](https://www.youtube.com/watch?v=eh5RYeprXDY) ```javascript= const [error, response] ?= await fetch(url); ``` #### 論 JavaScript 的語言生態、系統框架體質 - **動態 vs. 效能:** JavaScript 應用擺脫不了資源回收機制 (Garbage Collection)、prototype 物件導向設計等,致使底層效能不可控,且無法依使用需求和場景做微調。 - **Garbage Collector 資源回收問題:** 系統會自動從 heap 分配適當大小的記憶體給程式使用,大量或長期的 alloc / free 後,系統便需要頻繁調度 GC 以打理可用的回收資源,導致整個 JavaScript 執行程序必需暫停 (*stop-the-world*, *short-pause*) 致效能嚴重下降。 這對於 time-critical 或注重穩定的應用是很致命的。 改善方案: - 從「自動記憶體回收機制」轉向「靜態強型記憶體 (compile-time or runtime) 檢查管理系統」程式開發: [Why Discord is switching from Go to Rust - Discord](https://discord.com/blog/why-discord-is-switching-from-go-to-rust)。 :::success **系統設計 Follow-Up: 減輕 GC 操作負擔的新設計?** > 既然 Automatic Garbage Collection 開銷這麼大,有沒有可能採用 Lock Free 機制使 Runtime 系統不再受 GC 暫停、不確定性所苦? - **Mark-&-Sweep 結合 RCU 操作:** 例如 Linux kernel 中的 RCU 同步機制,在需要 Automatic GC 整理記憶體時,遇 user's read operation,便將其導向使用 RCU 同步參照的正在進行 GC 遷移的記憶體區塊;遇 user's write operation,直接在新的記憶體操作區域分配,並 cache 該記錄,供此時期的最新寫入查詢,並在最後合併記錄 (作 write barrier)。 待整個 GC 流程走完後,便可撤回臨時的 RCU 讀取和 cache 寫入查詢方案,回復原來的記憶體系統工作模式。 更多: - [RCU as an alternative to conventional garbage collection - StackOverflow](https://stackoverflow.com/questions/17437165/rcu-as-an-alternative-to-conventional-garbage-collection) - **賦予開發者針對 heap 物件做更全面掌控和細緻操作空間:** 例如透過編譯器支援,在物件宣告時額外標示: `likely_old`, `thread_unsafe`, `high_access_priority` 等。 ::: - **尋找 Object 中的資源:** 考慮以下 JavaScript 對 Object 的操作: ```javascript= obj.method = {}; ``` 首先,先從目前的 Call Stack 的 execution context (block、function、global) 棧中尋找對應 scope 裡有無 identifier 為 `obj` 的實體。 若存在,則從此 `obj` 的 Prototype Chain 中逐一在 key-value 的 hash table 中尋找名為 `method` 的鍵值。 若存在,則註銷該 Object 對此 value 的 reference mark,再透過 Memory Management 分配一 Object literal `{}` 空間,最終與 key `method` 綁定。若再考慮 sandbox 則又需額外記憶體操作開銷。 如此,每每做一個 Object 相關操作都必須如此耗力費時。相反的,這在編譯語言(compiled language) 中只是 $O(1)$ 的地址定位和賦值而已 (所有資料結構的 padding 在編譯期便已定下)。 參考: - [Compile time/Early binding vs Run time - Wiki](https://en.wikipedia.org/wiki/Compile_time#Compile_time/Early_binding_vs_Run_time) 改善方案: - [【python】__slots__是什么东西?什么?它还能提升性能?它是如何做到的!? - 码农高天](https://www.youtube.com/watch?v=FqBKqYzaRaQ) - [【python】Faster CPython的重要力量 -- Specialized Instruction - 码农高天](https://www.youtube.com/watch?v=SNXZPZA8PY8) - **JIT 編譯問題:** 雖然可以從 interpreter 升級成 compiler 執行,但效能永遠比不上 compiled 程式,跟 AOT 編譯比又遜色些 (看需求)。問題就是出在 JavaScript 語言太動態了,許多可強型定義的語言基礎,以提供編譯過程更多資訊及限制,在這裡都不存在: - 要嘛只有在最後十足把握執行流程、資料不變才編譯。 - 要嘛以假設某些 invariant 變因為基礎做最佳化,但猜錯時必須 rollback 回原來的點重新執行,且需不斷額外記錄和分析監控。 > 輕者效能變差,重者可能導致系統漏洞。 :::info :arrow_right: 詳見: [System Performance Analysis - shibarashinu](https://hackmd.io/@shibarashinu/r1Ww_zvmR) ::: - **Event Loop 系統框架:** 論評 JavaScript 單一主線的系統架構設計實屬非常兩極: 優點是控制簡單程式好設計好驅動,且完善的生態環境得讓程式獲得最廣泛的支援;缺點是給定的框架就是如此,且只能在非常高階的抽象層上開發,無法輕易更改底層 runtime 架構和邏輯,使能發揮 JavaScript 優點的應用非常有限。 - **不適合 CPU-Bound 應用:** 架構本為 non-blocking I/O 所設計,對運算資源密集應用不友善直觀。 - **擴展性差:** 單一主線及沙箱執行緒註定讓他與 scalability 扯不上邊,或只能應用在執行緒不需要太頻繁互動場景 (執行緒限制多且 high level,且互動僅能靠 `message`)。無法真正做到完全 statless 擴展分散式計算。 > **例:** 在瀏覽器中,網路資源下載依賴於閒置且數量有限的 Web Worker 進行多執行緒網路代理 (proxy) 下載。 - **動態難以控制執行流程:** 主線分配處理事件之設計,使欲被處理之事件和任務依賴當下主線 runtime 狀況: - 若以時間作為各程序先後順序之判斷,可能導致事件先後順序不可控 (e.g., `setTimeout`)。 - 或以事件佇列執行順序為依據,但同樣底層可能不是以同樣處理順序實作 (e.g., `requestAnimationFrame`, event loop vs. microtask vs. macrotask vs. rendering pipeline)。 :::info :arrow_right: 詳見: [Exploring the JavaScript Engine - shibarashinu](https://hackmd.io/@shibarashinu/r1XdprVCj) ::: - **相容 vs. 效能:** 雖然 JavaScript 蔚為當前 Web 語言生態首選,其易於開發的特性以及日漸完善的跨裝置、跨平台、跨應用支援,和更多的先進技術成為標準,論廣泛性、相容性和實用性一定是大拇指,但論效能、程式可控性、擴展性,相比其他更接近 native 的底層語言就略顯劣勢。 - **跨平台支援 & 沙箱層層包裝:** 為了保持平台相容性和系統安全性,JavaScript 需要「封閉的沙箱」及「由底層實作以完善對外服務」的執行環境支持才能正常運作。對於那些簡單任務的應用、需頻繁做低階操作或密集使用底層資源,可能有種「隔靴搔癢」使不上力的感覺。且僅能在平台/引擎提供的有限環境中 (限制的 API、資源存取) 運行,與原生 (native) 應用效能之差距不言而喻。 :::warning **結語:** JavaScript 雖有許多新奇、易用、廣為支援的功能,但本質上還是那台: *裝載單執行緒為主的 JavaScript 引擎* (適合密集 async IO 處理場景!),*後面又拖著標配 garbage collector*,*和採用層層系統框架和 sandbox 包裝* —— 像極了一台吃力、耗油、散熱又差的老舊跑車。 > 看看現在充斥市面琳瑯滿目的 Web 應用: 效能極差、資源霸佔、量體肥大、版面錯位、使用者體驗還相當糟糕 ... ::: ### Is JavaScript still a Thing? *JavaScript 是否已過時?* 無法靜態分析編譯以及為支援廣泛的平台相容標準而有著迂迴的設計框架是 JavaScript 的硬傷: - **語言缺乏靜態約束:** 使系統無法預測程式下一步走向,對需要這些額外資訊的程式分析和轉換束手無策。 :::info 概念上類似於: 在 C 語言中如果指標沒標明 `restrict` 則編譯器會默認有潛在的 *"Pointer Aliasing"* 可能,即放棄做進一步相關的最佳化。 > *"The more constraits you apply, the more optimizations you gain."* ::: - [[Slides] Static Hermes (React Native EU 2023 Announcement) - Tzvetan Mikov](https://speakerdeck.com/tmikov2023/static-hermes-react-native-eu-2023-announcement) ![image](https://hackmd.io/_uploads/Hys0_gNhR.png =400x) - **無一體式系統處理:** 這不是語言本身問題但歸結於 JavaScript 相依的環境生態: 以瀏覽器舉例,因 JavaScript 多用在需要跨平台支援,目標是成為 *Device-Independent* 的 web 框架 (甚至還可分飾成為網頁前後端兩角)。導致為了容納更多平台的支援和降低系統設計的複雜程度,泛型化 JavaScript 框架間的系統架構,採用多進程 / 多執行緒分工,並利用 IPC 呼叫 API 方式協作,提升了程式碼高內聚低耦合的設計。 如此雖使系統分工明確、除錯容易、開發環境健全、多元生態高擴充性,但也因此犧牲掉許多程式碼、資料、runtime 資源等最佳化的操作空間。 :::success **系統設計討論: JavaScript 與系統工程之難處** 接上所述,瀏覽器中各元件的程序各司處理多為 IO-Bound 的工作,而 CPU-Bound 時機只有在解析執行腳本和渲染上;又瀏覽器作為一般使用者應用程式,來回在作業系統和應用層切換也是必不可少部份;為安全考量在如此多工中又增設沙箱隔離容器,容器中 JavaScript 腳本又是運行在虛擬環境上的直譯器和 JIT 編譯器;又瀏覽器牽涉幾乎所有系統資源操作如: 網路堆棧、視窗系統與顯示卡、檔案系統、多媒體編解碼、設備 (e.g., USB、相機、鍵盤滑鼠)、Cookie 與密碼保護或保管系統 (e.g., *TPM*, *Key Storage*)、硬體加速等,而這些都是現代 JavaScript 和瀏覽器 API 基礎必備支援的功能。 種種原因導致瀏覽器無法有效發揮現代多核處理器架構性能,又必須完成大量繁複的工作,又要安全、高響應、高相容性,不怪瀏覽器成為吃電、吃記憶體、吃 CPU 的洪水猛獸。 挑戰最佳化如此龐大的系統肯定是一門藝術。 ::: 即便 WebAssembly 做為底層更接近機器執行狀況的語言,也還是改變不了 JavaScript 天生的動態特性。 了解到上述困境限縮 JavaScript 發展,改變必須得從語言革新做起,如從 ECMAScript 標準或 JavaScript 超集如 TypeScript (目前主流還是需轉譯才能在原生 JavaScript 環境執行) 下手使其成為態編譯語言,否則 JavaScript 相比其他競爭者終究只是個花里胡哨的玩具而已。 #### Bun、Deno — 舊瓶裝新酒的 Node.js - **[Deno](https://deno.com/):** 原生支援 JavaScript、TypeScript 和 WebAssembly 生態的 runtime 系統,核心基於 V8 JavaScript engine in Rust 打造,支持 Rust 生成之 WebAssembly 應用。 - **[Bun](https://bun.sh/):** 原生支援 JavaScript、TypeScript 生態的 runtime 系統 (但支援透過 FFI 串接 Rust 或其他底層語言系統函式庫),核心基於 JavaScriptCore in Zig 打造,主打更好的兼容、更短的啟動時間和更好的執行效率。 > Zig: 類似 C 的具手動記憶體管理的低階語言,但在語言層面有更好更完善易用的語法規範、函式庫、toolchain 支援等。 ![image](https://hackmd.io/_uploads/SkfxvEg2A.png =400x) 兩者理念都是想讓已年老力衰的 Node.js 能有個像樣的替代品,提供安全架構、原生支持許多原本依賴 Babel 擴充的熱門語法、取代萬惡難纏的 package modules、打造更有效率和安全性的引擎等。 但無論再怎麼修修補補還是無法徹底擺脫 JavaScript runtime 這個沈重的原生枷鎖。 更多: - :+1: **Deno 背景、系統背後開創哲學、Open Source 與公司營運策略**: [From Node.js to Deno: How It All Began - Honeypot](https://www.youtube.com/watch?v=zxitJn9MwYs) - **C++ vs. Zig vs. Rust**: [Zig or C++, Which of these would be a good second language to know, while having Rust as your strongest / main language? - Reddit](https://www.reddit.com/r/rust/comments/19d718o/zig_or_c_which_of_these_would_be_a_good_second/) - **Benchmarking**: [Deno vs. Node.js vs Bun: Performance (Latency - Throughput - Saturation - Availability) - Anton Putra](https://www.youtube.com/watch?v=yJmyYosyDDM) #### JavaScript Low Latency Runtime (LLRT) AWS 這類雲端服務也開發 [LLRT](https://github.com/awslabs/llrt),採用 QuickJS 作為 JavaScript 引擎,並以 IaaS (Infrastructure as a Service) 底層服務角度來對 JavaScript 進行最佳化。使 serverless 的 JavaScript 應用有更短的 Startup 時間、更輕量的執行負擔和資源消耗。 #### Static Hermes: 將 TypeScript 轉換為 Native Code Meta 相應也推出的比 WebAssembly 更激進做最佳化的 [Hermes](https://github.com/facebook/hermes) 引擎,主要用於提升自家 React Native 應用效能,Hermes 除了有基本 AOT 編譯、runtime 更少的記憶體資源佔用,Static Hermes (基於 Hermes 之上可選的新功能),正如其名稱中 *static* 一詞,目標就是將動態的 JavaScript 語言加諸更嚴謹的靜態型別規範: 把強型別的 TypeScript 做額外靜態語意檢查和加強,轉換為 bytecode、IR 或 native code。 盼此方案能徹底解決 JavaScript 最大短版 — 動態語言特性。 > ![image](https://hackmd.io/_uploads/HkhMYlV2R.png =400x) > > (Source: [[Slides] Optimizing With Static Hermes (Chain React 2024) - Tzvetan Mikov](https://speakerdeck.com/tmikov2023/optimizing-with-static-hermes-chain-react-2024)) 不僅如此,Static Hermes 還能用 FFI 方法直接串接其他底層語言系統函式庫,取代間接的 JavaScript Interface (JSI) 使用,意謂著僅使用 JavaScript 便能直接與 native APIs 互動 (見下小節)。 此外,後端也支援串接 LLVM 和相關 toolchain。更實作一套自己的 Memory Management 機制,完全拋棄傳統 JavaScript 自動記憶體管理的 Garbage Collector 機制: > The technical underpinnings of Static Hermes also involve sophisticated memory management techniques. By using static types, Static Hermes can allocate and deallocate memory for native types more predictably, avoiding the overhead of garbage collection that can cause pauses and stutters in dynamically typed, garbage-collected languages like JavaScript. > > (Source: [JavaScript Achieves Breakthrough Performance with Static Hermes - Elves Vieira - Medium](https://medium.com/@elves.silva.vieira/javascript-achieves-breakthrough-performance-with-static-hermes-6286b0ac8ef7)) :::info **在瀏覽器中「*Rust x WebAssembly*」vs. 「*JavaScript 原生生態*」的 Web 應用效能比較** 縱使現階段瀏覽器中的 WebAssembly 尚需仰賴 JavaScript API 轉手才得以與瀏覽器和網頁正常互動,但由於 Rust 及其上的 framework 採取有別 JavaScript 的靜態分析編譯,使生成之 WebAssembly 程式可擺脫 Garbage Collector 等原 JavaScript 的枷鎖,使其生成之 WebAssembly 效率以與 JavaScript 原生應用不相上下。加上效能測試中也發現絕大部分資源被 CSS 渲染管線佔用,木桶理論下使兩者差距更為微小。 Refs: - [The Truth about Rust/WebAssembly Performance - Greg Johnston](https://www.youtube.com/watch?v=4KtotxNAwME) ::: #### 結論 系統開發、語言演進、科技迭代都是活的,必須得跟上時代潮流前進,否則只有被取代淘汰的份。誰能戰到最後,只能靜觀未來的開發及使用生態的演變吧。 最後再參考各程式語言 performance benchmark: > ![image](https://hackmd.io/_uploads/H1iE_6NNxe.png =500x) > > (Source: [Charts showing the fastest programs by language - Benchmarks Game](https://benchmarksgame-team.pages.debian.net/benchmarksgame/box-plot-summary-charts.html)) :::warning **肯定的是,JavaScript 不是為效能而生** JavaScript 更像是面向開發者友善的工具,如 Python、Shell Script、~~Java~~ 等,強調完整開發生態和工具易用性。 > 以系統層面角度而論,這也是為什麼 scripting language 多使用 interpreter 的原因,旨在程式碼易讀、部署簡單、多平台支援、多功能應用等。 ::: ### [補充] 語言對外函式界面 Foreign Function Interface (FFI) 雖然 JavaScript 在談及硬核的效能、語言優勢上遠不及其他底層語言,但他完整的開發生態、軟體環境和開發者友善的特性,使其受開發者歡迎的程度不容小覷 (如同 Python)。 於是如果有 FFI 的幫忙,一個語言無關的對外函數接口,作為一函式庫可以靜態 / 動態連結,即可透過這些定義好的 API 及特定的 ABI 規範上,進行更底層的函式調用,從而彌補原生 JavaScript 語言性能之不足,或使用其他語言的現成函式庫、服務、產品等如資料庫管理系統 (DBMS)。 在 runtime 下,透過定義好的溝通規則 (e.g., 利用 IDL 生成雙邊 call stub 與函式庫),資料在本地被 marshall (可能進行 serialization 轉換及搬移) 後在目的 unmarshall (透過 deserialization 重組)。 ![image](https://hackmd.io/_uploads/B1QX4p4Nxl.png =500x) > 但如此也增加雙邊語言環境交互上的複雜程度,和合作開發 Debug 上的困難。例如: 使用 GC 語言 vs. 自主管理記憶體語言。 :::warning **如何有效生成雙邊「Glue Code」?** 類似 preprocessor 可由 code 生成 code (metaprogramming),我們可用更高階的「語言無關」IDL (為定義雙邊合作界面),以額外定義 IDL 文件方式,告知 client 母系統提供什麼樣的函數功能實作可供調用。 即在 build time 根據 IDL 文件,用 IDL 專用 parser 生成函數實體 library;並生成在 client 端 build time 靜態鏈結 / runtime 動態鏈結的 code。 ![image](https://hackmd.io/_uploads/SkHHZaENgx.png =500x) [Foreign function interface - Wiki](https://en.wikipedia.org/wiki/Foreign_function_interface) ::: :::info **例1: 在 Static Hermes 環境下使用 TypeScript 取得作業系統環境變數** > 在 C 語言中,沒有 "String" 概念,只有 "ASCII array with terminator *NULL* at the end"。 ```typescript= // Under Static Hermes runtime environment // declare the native external function const _getenv = $SHBuiltin.extern_c( // options for calling conventions, ... {}, // function name & types in native library function getenv(name: c_ptr): c_ptr { // throw away body to satisfy type checker throw 0; } ); function js_getenv(name: string): string { "use unsafe"; // from "JS string" to "native C String" let name = stringToAsciiz(name); try { // call native API let val = _getenv(name); // from "native C String" to "JS string" return asciizToString_unsafe(val, 2048); } finally { // free native memory buffer free(name); } } print(js_getenv("PATH")); ``` ::: :::info **例2: 將 Rust 編譯成具 FFI 的動態連結函式庫,使其可被 Bun 上的 TypeScript 應用所調用** 這裡 Rust 借鑒 C++ extern "C" 用法實現 FFI 實作。 > *C-Like* FFI 是目前最主流支援的語言界面 (直覺、系統親和)。 1. 將 primes.rs 透過 Rust 編譯為 C-style 動態連結函式庫。 ```rust= fn is_prime(n: u32) -> bool { if n < 2 { return false } if n % 2 == 0 { return n == 2; } if n % 3 == 0 { return n == 3; } let mut p = 5; while p * p <= n { if n % p == 0 { return false; } p += 2; if n % p == 0 { return false; } p += 4; } true } fn cycle(n: u32) -> u32 { let mut m: u32 = n; let mut p: u32 = 1; while m >= 10 { p *= 10; m /= 10; } m + 10 * (n % p) } // public external C-like function without name mangling #[no_mangle] pub extern "C" fn is_circular_prime(p: u32) -> bool { if !is_prime(p) { return false; } let mut p2: u32 = cycle(p); while p2 != p { if p2 < p || !is_prime(p2) { return false; } p2 = cycle(p2); } true } ``` 2. 回原 index.ts 利用 FFI 串接 primes.rs 的動態連結函式庫 ```typescript= // Under Bun runtime environment import { dlopen, FFIType, suffix } from "bun:ffi"; const path = `libprimes.$(suffix)`; const dynamic_lib_symbols_resolve = { // function name is_circular_prime: { // meta data args: [FFIType.u32], // args type returns: FFIType.bool, // returns type }, }; const lib = dlopen(path, dynamic_lib_symbols_resolve); console.log(lib.symbols.is_circular_prime(193939)); ``` [What is a Foreign Function Interface? - Code With Cypert](https://www.youtube.com/watch?v=fcx02vw9GNs) ::: ## Programming Paradigm 如同作文,程式設計也分為: 什麼語言適合什麼文體、技法去編排撰寫。這個語言天生適合做什麼工? > 寫作和程式設計,相同之處都是為「研究如何更好地去使用語言」而努力。 ### Imperative Programming vs. Declarative Programming 在什麼樣的情境,用什麼樣的工具、設計理念會比較好?又 JavaScript 能否有效發揮、展現這些程式設計哲學的特色? :::danger **注意:** 理論上這裡的 ++寫作++ / ++程式設計++ 手法並不互斥,每個設計哲學都有其著重的論點和特色。重點應放在理解各論點所帶來的好處,吸收各設計觀點強調的核心精神,最終再依照實務需求下組合、設計出最理想的 ++寫作++ / ++程式設計++ 策略,確保產出之 ++文案++ / ++軟體++ 是最佳實作方案。 > 不糾結於句讀,宜不求甚解。 ::: - **Imperative Programming:** HOW to do it? *I will tell the step-by-step implementation.* > E.g., object-oriented, procedural, structured, C, C++, JavaScript, ... > *「命令」這單元個體 (函式、物件、context) 應該做什麼!像亞洲父母管「行為」。* - **Declarative Programming:** WHAT to do with it? *Actually, I don't quite care about how to do it; I just describe the most essential tasks/important parts that matter here, hope somebody/some magic tricks will do that for us, and we're done.* :::info Most of the declarative programming language is ::: > E.g., functional (+no side effects e.g., stateless, immutable), CSS, React, SQL, Haskell, ... > *「描述」這單元個體 (函式、物件、context) 是在做什麼!「解函式 (遞迴、展開)」直到到不可再繼續的最終型態而終止。* :::info **Functional Programming (on Lambda Calculus) vs. Imperative Programming (on Turing Machine)** 因為 functional programming 離電腦底層實作較遠,所以給人較為疏離抽象的感覺,與 structural/procedural 的 imperative programming 的程式在 Turing Machine 上「原生」運作相比,其實只是出發點、實現方式不同,結果都是一樣的。 > 因還沒有真正能 run pure Lambda Calculus 的機器,這些都僅止於數學模型 (雖然 Turing Machine 和 Lambda Calculus 兩者等價),目前一切 functional programming style 的程式都是在 Turing Machine 上模擬合成的。 [Lambda Calculus vs. Turing Machines (Theory of Computation) - Advait Shinde](https://www.youtube.com/watch?v=ruOnPmI_40g) :::danger **但其實兩者不是相斥概念,而是可互相協作、==互補==的兩設計/實作哲學精神** *Oftentimes, they are inter-cooperative, which means one behavior can be implemented in the other code.* > Maybe via the syntactic sugar; otherwise the code looks terrible (hard to read). E.g., - Functional-programming-like behavior in imperative-programming-style: - `async`/`await`: turns functional `promise.then(...)` into step-by-step code. - [Monoids](https://www.youtube.com/watch?v=dYN8Q4Ms5U4): functional programming with effects (then can synthesize step-by-step functional code). - [What is a Monad? - Computerphile](https://www.youtube.com/watch?v=t1e8gqXLbsU) - [How Monoids are useful in Programming? - Tsoding](https://www.youtube.com/watch?v=BovTQeDK7XI) Monoids, the rules of *the operations* that satisfy: 1. **binary** (only take 2 parameters at one time). 1. **closed** (concatenating *the operations* will still yield the result in the same type as *these operations*). 2. **associative** (wherever processing first doesn't matter). > But not requires: > - **commutative:** the sequence of *the operations* matters (e.g., `a+b` is not `b+a` in some cases). > - **inverse:** exist *operations* that "undo" ("recover") *the operations* (Monoids don't require this, but Groups do). 3. has a **null** element in this set (no effect, e.g., nops). :::info Then, we can concatenate binary operations of this kind, monoid, into a single n-ary operation: `sum(a, sum(b, ...))` => ... => `sum(0, 1, 2, 3, -1, 4)` > Doesn't the process seem like a self-reentrant recursion in reverse, huh? ::: - Imperative-programming-like behavior in functional-programming-style: - `() => {}`, `promise.then(...)`, `for_each(...)`, C++20 range adaptors, ... ::: :::info **Functional Programming: Still Relevant & Powerful** FP 仍適合在各自分工明確、多工互不干涉、沒有中間產物 side effect (想像化學式一堆副產物污染整個環境) 的應用場景中使用。 > **註1:** 程式碼極簡主義。更適合編譯器最佳化。但過於抽象 (不聚焦產物,純粹討論函數交互意義 e.g., *functor*, *recursion*, *higher-ordered function*, *currying*) 可能導致開發 Debug 困難、維護不易。 > **註2:** 世間大部份事物沒有太多規則可循 (e.g., 數學系統中必然存在瑕疵:公理系統中不完備性 (真不可證)、不一致性 (矛盾論述);計算形式系統中不可判定性 (圖零機器侷限性)、... 等),這時 FP 的優點在此便顯得不甚理想。 > > :::info > **數學:在框架中用「可信」思路來解釋、歸納、演繹邏輯** > > 例: 1 取 x 個 1 取 y 個的組合數學 (離散) ---- 二項式定理;y=x 帕斯卡三角形;y=2x 費氏數列;動態規劃、生成函數、Z-transform (discrete-equivalent Laplace)、... transforms。 > > > 什麼是「可信」?符合直覺?不違背日常生活經驗?多數決普世價值?放諸四海皆準? > > 如此,又評判標準是以何為依據?個人、群體、環境,唯物、唯心,抑或是相依? > > 我們所探討的「框架」、我們互動的「環境」、環境中互動的「我們」 ---- 問題的本質、事物的核心、環境的意義、我們的角色、存在的定義,又分別是什麼? > ::: > :::warning > **擺脫「自指」悖論,邁向真理?** > *存在 vs. 意義* > > 當我們窮盡探索世間一切規律,創見一套完備無瑕的思想系統時,我們最終還是免不了自問系統中那最核心本質存在的意義是什麼:我是什麼?如果我否定我自己,我是什麼? > > > > :::info > > 就像一個遞迴函式不斷呼叫自己,每個子函式都有著明確且嚴謹的定義及經歷,但就在最終當函式即將返回到最初原呼叫函式時,恍然意識到究竟是誰來呼叫這個函式?找不到原遞迴函式的 caller,一切基礎不再,所有子函數當初經歷的過程、結果和意義都在此俱滅。 > > > > > 例: 此時此刻,我能清楚感知我的存在,但 `(whoami(i) { whoami(i) })(i)`? > > ::: > > 公理系統的哥德爾矛盾、計算形式化系統的圖靈機永不停機問題、數學邏輯的羅素悖論、物理時空下的祖父悖論、哲學的生命意義、子非魚焉知魚之樂也、桶中腦實驗、忒修斯之船、意識或高維空間存在、... 等,在認清所處世界的真實意義前,所有事物、邏輯演繹都只是片面局部解,綜觀全局仍是一個無意義的存在,而問題就出在當我們開始認識意識到並探尋事物本質的意義時,在我主觀觀點裡那個最核心的「我」究竟是什麼?我所身處的客觀世界又是如何定位「我」的存在和意義? > > 我們終究還只是停留在「意識到世界與我的存在」階段,卻從未「認識過世界與我的意義」。 > ::: ::: 而 JavaScript 作為一程式語言,在上述這些程式概念設計哲學中,因 JavaScript 語言內建 prototype 作為複雜資料結構的傳承和擴充方法,使其語言原生提供之環境便非常適合用於 *Imperative Programming* (e.g., OOP, Procedual/Structured Programming, ...) 等。 :::success **程式設計 Follow-Up: Declarative Version of Object-Oriented Programming (OOP)?** | method \ input type | 有形物件 | 無形指令 | |:-------------------:|:---------------:|:---------------------:| | Imperative | Object-Oriented | Procedural/Structured | | Declarative | ??? | Functional | > 在 computing 領域,這兩種 input types 剛好就是計算機器詮釋 binary 的兩種形式,所以以此做區分。 請問,有無「Declarative」針對「有形物件」操作的框架設計概念? 即:「描述型針對特定物件的 programming paradigm」而不是著重描述其方法、服務、相互反應。 可以 ***Aspect-Oriented Programming (AOP)*** 來體現這樣的方法: 有些「超脫實際服務內容的系統層級概念」零散在系統各個角落,需要被集中統一、靈敏、有效管理時 (e.g., [cross-cutting concerns in distributed systems](https://www.geeksforgeeks.org/system-design/cross-cutting-concerns-in-distributed-system/): policy, logging, security, authentication, transaction management, scalibility, synchronization),就需要以物件來有形化此類系統設計、實作,並以「提供描述 WHAT to do with it 為導向」來方便管理和解決系統耦合。 > 通常 AOP 建構在 imperative-programming-style 的底層系統上,並通常以 meta-programming 結合 runtime dynamic execution 方式來接合底層系統。 - [來談談 AOP (Aspect-Oriented Programming) 的精神與各種主流實現模式的差異 - Cymetrics Tech Blog](https://tech-blog.cymetrics.io/posts/maxchiu/aop/) - [Aspect-oriented programming - Wiki](https://en.wikipedia.org/wiki/Aspect-oriented_programming) - [Imperative vs Declarative, S.O.L.I.D and AOP(Aspect-oriented programming) - dev.to](https://dev.to/parasharrajat/imperative-declarative-s-o-l-i-d-and-aop-aspect-oriented-programming-4pji) - [AOP 與 Pointcut 淺談 - Bingdoal](https://bingdoal.github.io/backend/2020/11/aop-and-point-cut-in-spring-boot/) E.g., JavaScript (by using closure wrapper, `Proxy`, HOC (React), `@decorator` (ES7)): ```javascript= // closure wrapper function withLogging(fn) { return function (...args) { console.log("[logger] before addItem."); const result = fn(...args); console.log("[logger] after addItem."); return result; }; } function addItem(item, quantity) { this[item] = quantity; } addItem = withLogging(addItem); addItem("banana", 1); ``` Java (by using `java.lang.reflect.Proxy` (Spring Boot)): - Spring Boot (Spring AOP): - `@Aspect` - Advice (`@Before`, `@After`, `@Around`) - `@Pointcut` ```java= // no any logging/validation code required in the main service logic @Service public class OrderService { private final Map<String, int> map = new HashMap<>(); public void addItem(String itemName, int quantity) { map.put(itemName, quantity); }} } ``` ```java= @Aspect @Component public class LoggingAspect { // or using @Pointcut to register the function signature as an alias @Before("execution(* com.example.service.OrderService.addItem(..))") public void logBeforeAddItem() { System.out.println("[logger]: before addItem."); } @After("execution(* com.example.service.OrderService.addItem(..))") public void logAfterAddItem() { System.out.println("[logger]: after addItem."); } } @Aspect @Component public class ValidationAspect { @Pointcut("execution(* com.example.service.OrderService.addItem(..))") public void addItemPointcut() {} @Before("addItemPointcut() && args(itemName, quantity)") public void validateAddItem(String itemName, int quantity) { if (quantity <= 0) { throw new IllegalArgumentException("quantity should > 0"); } if (itemName == null || itemName.isBlank()) { throw new IllegalArgumentException("itemName required"); } } } ``` ::: ### Dynamic Programming Language JavaScript 的動態語言特性: 1. 許多特性必須依照 runtime 當下狀態決定,如*動態變數型態 (dynamic type)*、*弱隱性轉型 (weak typing)*。意謂著**無法**在執行前提前確定狀態做最佳化,最佳化還可能造成反效果 (e.g., *JIT 編譯 rollback*)。 2. 萬物皆是 `Object`,就連 *"excutable"* 的 function 也是 `Object` (i.e., first-class objects)。 3. 萬物皆以 prototype chain (繼承)構件和**動態**引用,且可用 `Well-Known Symbol` 來擴充語言規範 (e.g., 迭代、型別轉換),並在執行時期循此特性做程式行為之參照。 4. 動態 this 搭配 `call()` / `apply()` / `bind()`,使 ownership 概念可隨 runtime 需求、context 動態轉換。 > C++ 的 Object-Oriented `this` 可沒那麼萬能。 5. Cooperative (coroutine-like) 的 functional `Promise`,讓程式設計變為簡單。可用於 lazy execution 或 async operation。 :::info **動態語言應用: 執行時期多模態 RPC 開發** 善用 JavaScript 的 closure 與 async 異步處理特性,可在 runtime 下註冊未來 callback 中會使用到的物件。 使用場景如: 在伺服器 RPC 設計中 要處理 JSON、資料庫、擴充指令集,都可動態按照 RPC 傳來的 type 再做分配和適當處理。 :arro: 在伺服器 RPC 設計中 要處理 JSON、資料庫、擴充指令集,都可動態按照 RPC 傳來的 type 再做分配和適當處理,如此應用以 JavaScript 來開發便顯得游刃有餘。 :arrow_right: For more details, p ::: :::warning **JavaScript 如此方便又全能零缺點的程式語言應該倍受推崇愛戴?** 並不如此,因為使用者在上層享受語言帶來的便捷同時,底層為兌現對應的功能必須嘔心瀝血想辦法實現、維護及拓展,無形中增加整個系統開發負擔,越來越複雜的系統導致無論是使用者與生產上下游: 程式開發、程式編譯、runtime 效能在如此惡性循環下必出現衰退,最終因各方成本不敷負擔考量而逐漸衰敗,而新語言繼承上一代的生態鏈、經驗及教訓又是新一輪百家爭鳴的語言革命即將到來,而某一強勢語言必將殺出血路取代曾幾何時也是如此風光、獨霸一方的舊時代王者,如此週而復始的迭代著。 > 而如何看準轉機來臨時的契機、搭上這波可馳騁征服四界的風頭,就在這些細微的眉角上,值得圈內圈外人細細品味。 結語: 系統設計往往不是只有單一面向,有利有弊,端看現實需求、環境。 :::