Modern Pascal 仍在競賽中
作者:Arnaud Bouchez,2022年11月26日。永久連結
- Pascal 程式設計
- 部落格
- 集合
- 跨平臺
- 資料庫
- Delphi
- FPC
- 垃圾回收器
- 泛型
- Go
- 優秀實踐
- 超程式設計
- mORMot
- mORMot2
- 效能
- RTTI
- Rust
最近在Lazarus/FPC 論壇上的一項民意調查突顯了一個事實:Pascal 程式設計師比其他大多數程式設計師年齡都大。通常來說,到了我們這個年紀,應該做管理人員而非開發人員了。但我們仍喜歡用 Pascal 程式設計。幾十年過去了,它仍然很有趣!
但這是否意味著你不應該使用 Pascal 來做任何新專案呢?語言/編譯器/庫是否過時了呢?
在我目前工作的公司裡,我們有一些年輕的程式設計師,他們有的是剛畢業,有的還在上學,他們加入了團隊並寫出了出色的程式碼!
最近在同一個論壇上的一個帖子討論了使用 C#、Go、Scala、TypeScript、Elixir 和 Rust 等語言實現 REST 伺服器的比較。
即將貢獻出幾個 Pascal 版本,其中之一就是 mORMot 大放異彩的版本。
挑戰與演算法
最初的挑戰可在 transit-lang-cmp 找到,其中包含所有這些花哨語言和庫的原始碼。
實際上,此測試程式的目標是載入兩個大型 CSV 到記憶體中(80MB + 2MB),然後透過 HTTP 提供由路由識別符號生成的 JSON,同時連線兩個 CSV。
生成的 JSON 大小可能在 30KB 到 2MB 之間。所有資料都是根據記憶體中的 CSV 實時生成的。
說句公道話,一個普通的商業程式設計師會為此使用資料庫。而不是傻傻的記憶體結構。並要求資金支援,以建立一組龐大的雲端計算機器和負載均衡。😃
遵循mORMot的方式
對於FPC中的mORMot版本,我採用了另一種方法,使用了兩種不同的演算法:
- 我確保列表在記憶體中是已排序的,然後在其中進行了O(log(n))的二分查詢;
- 所有儲存的字串都被“內聯”了,即相同的文字共享一個字串例項,FPC的引用計數發揮了它的魔力。
這裡沒有使用像手動生成JSON或使用複雜資料結構這樣的底層技巧——資料結構仍然是高階的,具有可讀的欄位名等。邏輯和意圖都是清晰可讀的。
我們只是利用了Pascal語言和mORMot的特性。例如,如果需要,字串內聯是框架的一部分。
請檢視我們儲存庫中的原始碼。
結果如下:
- 程式碼仍然可讀、簡潔且高效(大部分處理都是由mORMot完成的,例如CSV、搜尋、JSON);
- 它使用的記憶體要少得多——儲存資料時使用的記憶體比Go少10倍,提供服務時使用的記憶體比Go少5倍;
- 效能與Go及其經過高度調優/最佳化的編譯器和RTL一樣快。
演算法的重要性
主要的思想是讓演算法匹配輸入資料和預期的結果集。
就像程式設計師在程式設計遊戲時所做的那樣。而不像編碼人員在編寫商業軟體時那樣隨意。😉
- 由於使用了mORMot有效地對映了動態陣列儲存及其CSV和JSON功能,原始碼仍然非常可讀。
TDynArray
- 我猜原始碼對於校外程式設計師來說仍然是可以理解的——比Rust等語言更可讀。
公平地說,我使用了型別化指標,但理解它們的目的並不難,而且FPC會將此函式編譯成非常高效的彙編程式碼。我本來可以使用帶有索引的常規動態陣列訪問,這樣雖然會稍微慢一點,但並不更容易跟蹤,也不更安全(如果我們在沒有範圍檢查的情況下編譯)。TScheduler.BuildTripResponse
值得注意的是,我們沒有進行任何特定的調優,例如像其他框架所做的那樣使用常量預分配結果。我們只是指定了資料,然後讓mORMot來處理它——僅此而已。
mORMot的RTTI級別符合我們對現代框架的期望:不僅僅是用一些類來儲存JSON,還可以使用像類或記錄這樣的結構進行方便的序列化/反序列化。
使用現代Pascal動態陣列和記錄來定義資料結構,讓編譯器為我們利用記憶體,無需編寫任何塊和使用介面。Pascal的“手動記憶體管理”不是強制性的,而且可以很容易地繞過。只有對於WebServer,我們有一個預期會關閉它的 try..finally..Free
和 Free
。
給我一些數字
以下是與Go的效能比較(左側為FPC,右側為Go):
引數 | FPC效能 | Go效能 |
---|---|---|
- | 在968.43毫秒內解析了1790905個停車時間 | 在3.245251432秒內解析了1790905個停車時間 |
- | 在39.54毫秒內解析了71091次行程 | 在85.747852毫秒內解析了71091次行程 |
- | 正在執行(0m33.4s),00/50 VUs,348個完成,0箇中斷 | 正在執行(0m32.3s),00/50 VUs,320個完成,0箇中斷 |
- | 預設✓ [===================] 50 VUs 30 | 預設✓ [===================] 50 VUs 30 |
資料接收量 | 31 GB,933 MB/s | 31 GB,971 MB/s |
資料傳送量 | 3.2 MB,97 kB/s | 3.0 MB,92 kB/s |
http請求被阻止 | 平均=9微秒,最小=1.09微秒 | 平均=6.77微秒,最小=1.09微秒 |
http請求連線 | 平均=2.95微秒,最小=0秒 | 平均=1.73微秒,最小=0秒 |
http請求持續時間 | 平均=47.59毫秒,最小=97.28微秒 | 平均=49.02毫秒,最小=123.81微秒 |
平均=47.59毫秒,最小=97.28微秒 | 平均=49.02毫秒,最小=123.81微秒 | |
http請求失敗 | 0.00%,✓ 0,✗ | 0.00%,✓ 0,✗ 3 |
http請求接收 | 平均=9.66毫秒,最小=15.35微秒 | 平均=5.92毫秒,最小=14.76微秒 |
http請求傳送 | 平均=87.24微秒,最小=5.2微秒 | 平均=70.71微秒,最小=5.2微秒 |
http請求TLS握手 | 平均=0秒,最小=0秒 | 平均=0秒,最小=0秒 |
http請求等待 | 平均=37.83毫秒,最小=54.74微秒 | 平均=43.02毫秒,最小=91.84微秒 |
http請求 | 34452,1032.205528/秒 | 31680,981.949476/秒 |
迭代持續時間 | 平均=4.72秒,最小=3.54秒 | 平均=4.86秒,最小=2.19秒 |
迭代次數 | 348,10.426318/秒 | 320,9.918682/秒 |
vus | 30,最小=30,最大 | 15,最小=15,最大 |
vus_max | 50,最小=50,最大 | 50,最小=50,最大 |
其實文字也很好看
parsed 1790905 stop times in 968.43ms | parsed 1790905 stop times in 3.245251432s
parsed 71091 trips in 39.54ms | parsed 71091 trips in 85.747852ms
running (0m33.4s), 00/50 VUs, 348 complete and 0 interrupted | running (0m32.3s), 00/50 VUs, 320 complete and 0 interrupted
default ✓ [======================================] 50 VUs 30 default ✓ [======================================] 50 VUs 30
data_received..................: 31 GB 933 MB/s | data_received..................: 31 GB 971 MB/s
data_sent......................: 3.2 MB 97 kB/s | data_sent......................: 3.0 MB 92 kB/s
http_req_blocked...............: avg=9µs min=1.09µs | http_req_blocked...............: avg=6.77µs min=1.09µs
http_req_connecting............: avg=2.95µs min=0s | http_req_connecting............: avg=1.73µs min=0s
http_req_duration..............: avg=47.59ms min=97.28µs | http_req_duration..............: avg=49.02ms min=123.81µ
{ expected_response:true }...: avg=47.59ms min=97.28µs | { expected_response:true }...: avg=49.02ms min=123.81µ
http_req_failed................: 0.00% ✓ 0 ✗ | http_req_failed................: 0.00% ✓ 0 ✗ 3
http_req_receiving.............: avg=9.66ms min=15.35µs | http_req_receiving.............: avg=5.92ms min=14.76µs
http_req_sending...............: avg=87.24µs min=5.2µs | http_req_sending...............: avg=70.71µs min=5.2µs
http_req_tls_handshaking.......: avg=0s min=0s | http_req_tls_handshaking.......: avg=0s min=0s
http_req_waiting...............: avg=37.83ms min=54.74µs | http_req_waiting...............: avg=43.02ms min=91.84µs
http_reqs......................: 34452 1032.205528/s | http_reqs......................: 31680 981.949476/s
iteration_duration.............: avg=4.72s min=3.54s | iteration_duration.............: avg=4.86s min=2.19s
iterations.....................: 348 10.426318/s | iterations.....................: 320 9.918682/s
vus............................: 30 min=30 ma | vus............................: 15 min=15 max
vus_max........................: 50 min=50 ma | vus_max........................: 50 min=50 max
因此,CSV載入速度要快得多,而HTTP伺服器的效能則幾乎相同。
No Alzheimer---無阿爾茨海默症
以下是一些關於記憶體消耗的資料:
在完成CSV載入後,mORMot僅佔用80MB,天哪,這麼少。聽起來有點神奇。但在負載測試中,它在250-350MB之間波動,測試結束後又回到80MB。Go版本在載入完CSV後佔用925MB。在負載測試中,它最高達到了1.5GB,之後又回到了925MB。
讀起來不錯。😃
Pascal擁有一個現代且功能強大的生態系統
這篇文章不僅僅是關於Pascal的,還涉及到演算法和庫。
最初的挑戰是比較它們。不僅僅是作為不現實的微觀基準測試,或“計算機語言基準測試遊戲”,而是作為實際應用場景中的資料處理能力。
而且……Pascal當然還在競爭中!
不僅僅是為了像我這樣的“老人”——我剛滿50歲 (譯者注:本文作者Arnaud Bouchez發表於2022年11月26日 )。😉
我們傳播的這類資訊越多,就越少有人會拿Pascal程式設計師開玩笑。
Delphi和FPC與Java一樣古老,所以是時候看到大局了,而不是跟隨市場趨勢。