![]()
摘要
本文介紹一個以 MoonBit 實現的符號計算內核 Symbit,目標是通過AI輔助,在保留 sympy 風格符號表達與精確計算能力的同時,將大部分算法移植到 MoonBit,利用 native 與 WebAssembly 后端提升執行效率并降低用戶訪問門檻,理想情況下用戶只需要用瀏覽器即可訪問此計算系統。
我們重點討論表達式表示的差異化設計,并通過 differential testing 與基準測試評估其正確性與性能。在若干典型 workload 上,該實現相對原始 Python/sympy 路徑取得數倍的 native 性能提升,在瀏覽器上使用 WASM 亦可得到 2x 的平均性能提升,更提高了可訪問性。
引言
計算機代數系統(computer algebra system, CAS)是計算機科學和數學領域的重要工具,可以對表達式進行構造、變換、化簡與求解。與數值計算不同,符號計算更強調精確性、可解釋性與結構保持,操作的對象通常是表達式樹。例如,有理數應以精確分數而非浮點近似表示,多項式應保留其代數結構而非退化為采樣結果,矩陣運算與方程求解也往往需要在精確域上完成。
在現有開源生態中,sympy 以 Python 為宿主語言,為符號表達、代數變換、微積分、矩陣、方程求解和多項式計算等任務提供了統一而靈活的抽象接口。其優勢在于能夠與 Python 的科學計算生態自然結合。
然而,這種設計也繼承了動態語言運行時的若干工程特征:大量細粒度對象分配、深層遞歸遍歷、頻繁的動態分派,以及在表達式重寫過程中反復進行的結構構造與析構。在以表達式規范化、多項式操作、精確線性代數為代表的一類高頻符號負載中,這些開銷會逐步累積,并成為性能瓶頸之一。換言之,符號計算系統的效率問題并不總是來自「數學算法本身不夠好」,也常常來自其承載算法的數據表示與運行時執行模型。此外,因為 Python 是動態類型語言,sympy 的實現也很難通過靜態分析來保證運行時無類型錯誤。
除了執行效率,符號計算系統的可部署性同樣值得關注。盡管桌面或服務器環境中的 Python 生態已經相當成熟,但當目標場景轉向瀏覽器時,傳統方案往往面臨更高的接入成本:用戶也許會期望「打開網頁即可使用」,而不是預先安裝解釋器、環境或依賴;另一方面,前端交互式教學、在線計算工具和嵌入式數學組件等應用,也要求核心引擎能夠以更輕量的方式部署到 Web 環境中。對于此類場景而言,一個既能在 native 環境高效運行、又能以 WebAssembly 形式直接進入瀏覽器的符號計算內核,具有明確的工程意義。
基于上述動機,我們提出 Symbit,一個用 MoonBit 實現的符號計算內核。其算法設計主要參照 sympy,但在實現層面上,我們重新審視了表達式對象設計、規范化、精確有理數、多項式操作與矩陣消元等核心結構,并通過差分測試驗證其正確性。最后我們將在 native 與 WebAssembly 兩類環境中評估其性能表現。
設計邊界
本節討論我們的設計的目標與非目標。
語義兼容性的目標
Symbit 的首要目標是在可觀測語義上盡可能保持 sympy 風格的符號行為。這里的「可觀測語義」至少包括四層:
API 的基本行為:一個用戶在構造表達式、調用化簡、求解、多項式操作或矩陣運算時,得到的對象種類與主要結果形態應與 sympy 保持一致;
對象語義:例如 Add 、 Mul 、 Pow 等核心表達式節點在構造時如何進行扁平化、系數合并、參數保序與未求值保留;
精確數值與集合語義:整數、有理數、有限集合、區間、條件集合等對象必須按照精確代數語義傳播,而不能為了簡化實現輕易退化為浮點近似或字符串占位丟失信息;
觀測語義:打印、展示與前端集成行為,這一層較為靈活,輸出字符串可與 sympy 不同,但是語義等價。
工程目標
在語義兼容之外,Symbit 將通過新的實現基座改善執行效率,尤其是 native 場景下的表達式重寫、多項式計算、精確矩陣運算與部分求解。當然性能目標并不是抽象的「整體更快」盡管使用 MoonBit 重寫確實可以免費獲得這一點,但更多的也包含一些算法決策級別的優化。
MoonBit 是多后端編程語言,這帶來了一些額外好處:我們可以近乎免費地獲得 WebAssembly 部署能力,理論上用戶可以直接在瀏覽器中使用 Symbit,而不需要安裝 Python 環境或依賴。這對于在線教學、交互式計算工具和嵌入式數學組件等應用場景具有明顯的吸引力。令人驚喜的是,我們最終也完成了這個目標。
非目標
Sympy 有部分包還依賴于 Python 的一些特性,例如 multidispatch 利用 class 機制做多重分派,這在 MoonBit 中并無直接對應,也有依賴于其他復雜 Python 庫的包,例如 plotting 依賴于 matplotlib,因此本文并不宣稱已經完整遷移 sympy 全部頂層包,而是把目標限定在核心 symbolic engine 相關的包,例如 core 、 polys 、 matrices 、 solvers 等,它們占據了 sympy 90% 的代碼。
評估邊界
我們采用如下方法評定 Symbit 的完成度:
新增功能必須同時具備局部行為測試與 parity/oracle (差分) 測試,且后者必須覆蓋到核心語義邊界;
通過靜態類型系統把所有類型錯誤暴露在編譯階段,而不是運行時;
在 native 場景下,Symbit 的算法應該比對應的 sympy 算法有數倍的性能提升。
系統架構
一個符號計算系統的難點在于如何將這些算法如何被組織到統一的對象模型之下,否則就很難稱為一個「系統」,而更像是一些獨立的工具庫。如果表達式表示、精確數值、多項式、矩陣和求解器各自維護一套局部規則,那么系統規模一旦變大,就會出現語義不一致、重復實現、難以測試等問題。因而本章節將討論 Symbit 的整體架構,以及它與 sympy 在組織方式上的相似與不同。
Sympy 的對象模型
Sympy 的核心是圍繞 Basic / Expr 的基于類繼承的表達式樹。無論是 Add 、 Mul 、 Pow 這表表達式基礎結構,還是矩陣、集合、關系、函數應用與特殊對象,都需要接入這一套動態對象模型,通過繼承來組織表達式,其關系大致是 Add <: Expr <: Basic ,在解析 + 的時候會構造一個 Add 對象, Add 的參數又是其他 Expr 對象,依此類推。一旦某個對象被納入 Basic 體系,它就自動獲得了打印、比較、替換、遍歷、匹配、重寫等一系列通用能力。
class Basic(Printable):
__slots__ = ('_mhash', '_args', '_assumptions')
is_number = False
is_Atom = False
is_Symbol = False
is_Add = False
is_Mul = False
is_Pow = False
...@sympify_method_args
class Expr(Basic, EvalfMixin):
__slots__ = ()
...
這讓系統在可擴展性和可讀性上都非常成功,特別適合逐步長出一個覆蓋面極廣的 CAS 生態。但它也存在許多不足:很多原本可以在更低層被隔離的成本,最終都會回落到通用對象系統中,例如對象構造開銷、動態分派、運行時比較與容器重建。并且因為 Python 的動態類型系統,若寫出類型 / 類不匹配的問題時很難在編譯階段發現。
Symbit 的代數數據類型
Symbit 在總體上仍然保留了 sympy 的主題劃分,例如 symcore 、 sympolys 、 symmatrices 、 symsolvers 、 symsimplify 等包大體對應上游的數學模塊。其中最底層的是 symcore 與 symnum 。前者負責表達式節點、規范化、比較、替換、遍歷以及若干基礎對象語義,后者則提供精確整數、有理數以及部分數值兼容層 (因此我們也重寫了 Python 的任意精度浮點數庫 mpmath )。
盡管我們可以用 tagless final 和 trait 來模仿 class 風格的代碼組織,但這里我們選擇了一種更直接的風格:用一個單一的代數數據類型來表示所有表達式節點,因此 Parser 的代碼會相對耦合一些,借助模式匹配和窮盡檢查等功能(擴展變體可讓類型檢查器發現遺漏之處),代碼也能保持相當清晰。
pub(all) enumExpr {
Number(@symnum.BigRational)
Float(Float)
NumberSymbol(NumberSymbolKind)
Boolean(Bool)
Apply(Expr, Array[Expr])
Add(Array[Expr])
Mul(Array[Expr])
Pow(Expr, Expr)
Mod(Expr, Expr)
Tuple(Array[Expr])
Dict(Array[(Expr, Expr)])
...
}這本質上算是一個表達式問題,但考慮到未來 MoonBit 可能會加入 Extensible Variants 的支持,且數學領域很多東西也是多年不變的,我們認為此種做法或不會受到太多維護成本的影響,并且相比 sympy 在許多方面是更優的。另一方面,sympy 很多表達式處理都是借助節點的「內在屬性」,因而有時候需要傳遞一些額外的參數來維持這些屬性,例如 evaluate=False 來控制是否在構造時進行規范化。
class Add(Expr, AssocOp):
...class AssocOp(Basic):
@cacheit
def __new__(cls, *args, evaluate=None, _sympify=True):
...
Symbit 則使用「智能構造子」形式來應對不同場景的規范化需求,可以在一定程度上解耦合(實際上,在編寫 Symbit 引擎的過程中我們還發現了 sympy 真實存在的因為參數傳遞和弱類型導致的 bug):
pub fnexpr_new_raw(op : ExprOp, args : Array[@symcore.Expr]) -> @symcore.Expr {
matchop {
ExprOp::Add => @symcore.raw_add(args)
ExprOp::Mul => @symcore.raw_mul(args)
ExprOp::Pow => @symcore.raw_pow(args[0], args[1])
ExprOp::Function(name) => @symcore.raw_function(name, args)
...
}
}pubfnexpr_new_eval(op : ExprOp, args : Array[@symcore.Expr]) -> @symcore.Expr {
matchop {
ExprOp::Add => @symcore.add(args)
ExprOp::Mul => @symcore.mul(args)
ExprOp::Pow => @symcore.pow(args[0], args[1])
ExprOp::Function(name) => @symcore.function(name, args)
...
}
}
在此之上則是各種代數基礎設施包,例如之前提到的 sympolys 和 symmatrices (分別用于多項式處理和矩陣計算)。還有一個 sympy 包,顧名思義,是一個 parity 層,專門用來調用 sympy 進行 oracle 測試。下一節我們將討論它的設計,以及它在整個系統中的作用。
測試系統
符號系統面對的是結構對象,這些對象往往存在多種語義等價但結構不同的表示形式,并且很多錯誤并不會立刻表現為程序崩潰,而是表現為規范形不穩定、定義域被忽略、過早求值或錯誤地返回了一個看似合理的表達式。因此,Symbit 的測試系統從一開始就并不只是依賴于簡單的單元測試,而采用一個分層驗證體系:用快照測試記錄可觀察表示,用行為測試約束局部語義,用 parity / oracle 測試對照 sympy,并在必要時直接嵌入 CPython 運行時來復用其參考語義。
更形式化地說,我們希望驗證的并不是單個函數的一個輸出值,而是一個 MoonBit 實現 f_mb與一個 sympy 參考實現 f_sp在輸入域 D 上的觀測等價性(當然,D 的表示也因語言不同而存在差異)
![]()
其中關系 并不總是簡單的字符串相等。在某些場景下,可以是完全結構相等;在另一些場景下,它可能是經規范化后的表達式相等、殘差為零(下方公式) 、集合意義下等價,或在浮點物理量場景中滿足一定誤差界,測試設計者應該根據對象語義選擇合適的比較關系。
![]()
MoonBit 的快照式測試
在最基礎的一層,Symbit 大量使用 MoonBit 自帶的 inspect(..., content=...) 風格測試來記錄穩定輸出。這種測試主要用來固定一段當前實現的可觀察表示,尤其適合打印器、parser、debug representation 以及某些公開 API 的回歸驗證。與傳統手寫 assert_eq 相比,這種寫法在符號系統里尤其自然,更大的便利是當輸出改變時可以直接給出 diff,檢查正確后可用 moon test -u 來更新快照,而不需要手動修改斷言文本。如果沒有成體系的快照更新機制,這類工作往往會退化成機械地逐條修改斷言文本,既低效,也容易遺漏。
test "prelude constructors round trip through printers" {
let expr = add([
integer(1),
mul([Symbol("x"), pow(Symbol("y"), integer(2))]),
])
debug_inspect(expr, content="(+ (* sym:x (^ sym:y 2)) 1)")
inspect(expr, content="x*y**2 + 1")
}這里的兩個 inspect 實際上對應兩類不同的斷言。前者是內部結構快照(通過 Debug trait 實現),用來保證表達式構造與規范化沒有悄悄改變底層表示;后者是用戶可見輸出快照,用來保證打印行為與 API 接口的穩定性。這兩類快照都很重要,因為對于符號系統而言, 「內部結構變化但外部字符串不變」與「外部字符串變化但內部結構不變」都可能導致一些意外的回歸問題。
Oracle / Parity Testing 的原理
僅靠快照測試仍然不夠。符號系統中更難的問題在于, 「看起來不同」的結果可能是等價的,而「看起來合理」的結果也可能在語義上是錯的。因此,Symbit 在包級遷移中采用了第二層測試機制:在 src/sympy/* 下維護一棵鏡像式的 parity/oracle 樹(諭示機),專門把 MoonBit 的實現結果與 sympy 參考結果進行對照。這一層基本上綁定了 sympy 全部的外部接口。而實現層應永遠不依賴 sympy,只有測試層可以調用 sympy 作為參考語義。也就是說,MoonBit 代碼能夠算出自己的答案,而 oracle 層會將這個答案轉換成 sympy 對象,并調用 sympy 對應的 API 來驗證它的正確性。或者直接將同樣的輸入 轉換為 sympy 對象并調用 sympy 函數,得到結果后與 symbit 進行比較。
在代碼層面上, symbit/src/sym* 負責實現,而 symbit/src/sympy/* 則按包鏡像上游模塊,包含 *_oracle.mbt 、 *_port_test.mbt 、 port_support_test.mbt 等文件。例如 core 、 polys 、 solvers 、 physics 等子樹都各自維護了對應的 oracle 層。
在比較關系 R 的設計上,正如我們之前所言,Symbit 并不假設所有對象都適合用同一種比較方式。實踐中我們主要使用了三類 parity 技巧。
第一類是直接的結構或文本 oracle。如果一個對象的輸出形式本身就是穩定語義的一部分,例如 srepr 、 latex 或某些調試表示,那么最直接的做法就是把 MoonBit 對象轉換成 sympy 對象,再調用 sympy 的對應 printer 取得參考輸出,并和 MoonBit 的序列化結果進行字符串比較。例如 core_oracle 中:
pub fn core_expr_sstr(expr : @symcore.Expr) -> String raise {
let obj = @sympy.expr_to_sympy(expr)
py_str_obj(
objenum_to_obj(
sympy_call_must("sympy.sstr", [@sympy.OracleArg::PyObj(obj)]),
),
)
}第二類是經規范化后的表達式等價。對于許多代數結果而言,直接比較字符串會過于脆弱。例如 x - y 與 -y + x 在文本上不同,但在表達式意義下等價。這時更合適的比較關系是:
![]()
這種比較方式特別適合 simplify 、 factor 、 expand 、 solve 等場景。在求解器中,進一步常見的做法是比較殘差而不是比較解的打印形式。如果一個候選解 滿足
![]()
并且其定義域條件與 sympy 一致,那么即便解集的內部排列或局部打印形式不同,它仍然應被視為正確結果。因此在 solver oracle 測試中,我們可以運行最后的結果放置在無序容器中進行比較,而不是試圖要求所有結果逐字符一致。
第三類是數值近似語義的 oracle。這類場景主要出現在物理、控制、繪圖或某些數值 API 中。在這些路徑里,sympy 本身也未必總返回純精確對象,而 MoonBit 側有時會經過不同的數值后端或離散化過程。此時更合適的關系通常是
![]()
這類比較在系統中并不是主流,但它們提醒我們:oracle 測試的關鍵不在于「永遠追求字符串一致」,而在于為每種對象語義選擇恰當的比較關系。
sympy 對象構造
在 parity/oracle 測試里,我們盡量直接通過 FFI 構造 sympy 對象,而不是先把 MoonBit 表達式打印成字符串再交給 Python 解析。如果 oracle 建立在字符串 round-trip 之上,那么 parser、printer 與對象橋接這三類問題就會纏在一起。此時一旦出現不一致,很難判斷究竟是 MoonBit 側對象語義錯了,還是打印錯了,還是 Python 側解析路徑改變了,非常不利于 debug。
因此,在 pybridge.mbt 中, Symbit 把 Expr 直接映射到 sympy 對象:
pub fn expr_to_sympy(expr : @symcore.Expr) -> @py.PyObject raise {
let dummy_cache : Map[Int, @py.PyObject] = {}
let wild_cache : Map[String, @py.PyObject] = {}
expr_to_sympy_cached(
@symcore.normalize_legacy_expr(expr),
dummy_cache,
wild_cache,
)
}更進一步, Add 、 Mul 、 Pow 這些對象在橋接時會顯式傳入 evaluate=false ,以保留結構語義而不是讓 sympy 在橋接階段重新化簡:
@symcore.Expr::Add(args) =>
sympy_nary_obj("sympy.Add", args, "0", dummy_cache, wild_cache, kwargs={
"evaluate": OracleArg::Bool(false),
})
@symcore.Expr::Mul(args) =>
sympy_nary_obj("sympy.Mul", args, "1", dummy_cache, wild_cache, kwargs={
"evaluate": OracleArg::Bool(false),
})
@symcore.Expr::Pow(base, exp) =>
sympy_call_obj(
"sympy.Pow",
[OracleArg::PyObj(base_obj), OracleArg::PyObj(exp_obj)],
kwargs={ "evaluate": OracleArg::Bool(false) },
)因為 oracle testing 想比較的是「兩個系統對同一對象的理解是否一致」,而不是「MoonBit 打印出的字符串能否再次被 sympy 接受并重寫成某種形態」。通過對象級橋接,oracle 層可以最大程度剝離 parser/printer 噪聲,把測試焦點放回真正的符號語義上。
利用 CFFI 接入 CPython 運行時的經驗
要讓上述 parity/oracle 機制可用,還需要一個現實前提: MoonBit 必須能夠穩定地嵌入 CPython 運行時,并以足夠低的摩擦調用 sympy。在 Symbit 中,這部分實現集中在 symbit/src/sympy 這層 Python bridge。從代碼組織上看,它又可以拆成三小層:
types.mbt :定義跨邊界參數類型 OracleArg
py_pack.mbt :負責參數打包與 Python 對象構造
py_call.mbt / pybridge.mbt :負責運行時初始化、調用和對象級橋接
例如 types.mbt 中,所有跨邊界參數都先被封裝成一個統一的代數數據類型:
pub(all) enumOracleArg {
Null
Str(String)
Int(Int)
Bool(Bool)
StrList(Array[String])
IntList(Array[Int])
List(Array[OracleArg])
Dict(Map[String, OracleArg])
PyObj(@py.PyObject)
}有了這些方便的函數我們就不必再到處手寫不同的 FFI 調用,而是通過一層統一的參數打包協議。絕大多數 oracle helper 都可以在 OracleArg 這一層復用相同的參數傳遞邏輯,而不用每個包都自己拼接 Python 參數。
隨后在 py_pack.mbt 中,這些參數被打包成真實的 Python 對象:
pub fn py_pack(arg : OracleArg) -> @py.PyObject {
match arg {
OracleArg::Str(s) => @py.PyString::from(s).obj()
OracleArg::Int(n) => @py.PyInteger::from(n.to_int64()).obj()
OracleArg::Bool(b) => @py.PyBool::from(b).obj()
OracleArg::PyObj(obj) => {
@cpython.py_incref(obj.obj_ref())
obj
}
...
}
}一個常見的坑時引用計數問題,但跨邊界傳入現有的 PyObj 時,必須顯式 incref ,否則一些的生命周期錯誤會非常難查(它們甚至看起來是隨機發生的)。
oracle 層盡量不依賴「把字符串塞進 Python 的 eval 函數」,這對代碼可讀性、錯誤定位和測試穩定性都非常不利。我們僅在 core_oracle.mbt 等受限場景下調用 Python eval ,但這類邏輯主要用于便捷地獲取參考值;而真正從 MoonBit 對象進入 sympy 對象的主路徑,仍然是前面提到的 expr_to_sympy 。這種分工可以概括為:
![]()
而不應該是
![]()
后者雖然實現更快(且我們早期確實希望通過這個簡化實現,但帶來的收益不如帶來的麻煩多),但它把 parser、printer、引用環境和語義比較混成了一條鏈,對于遷移工程來說噪聲過大。
最后,CFFI 接入還有一個經驗是:橋接層必須被視為「測試基礎設施」,而不是「核心實現的一部分」。實現層不能夠依賴 sympy_call(...) 這類接口來獲得最終結果,否則整個遷移項目就會失去意義。
評估
到目前為止,本文討論了系統目標、對象架構、一個代表性多項式算法以及測試與 oracle 機制。最后一個問題自然是:這些設計在實際運行中帶來了什么結果。對于一個 CAS 實現而言,評估不能只給出若干「更快」的例子,因為符號系統的性能高度依賴負載的結構:有些路徑主要受精確算術支配,有些受表達式重寫支配,有些則受數據結構構造、項排序與規范化頻率支配。因此我們應該給不同的包,挑選大部分具有代表性的 API 進行測試,實踐中我們借助 symbench 把負載按包與 API 家族組織起來,分別給出 native 與 WebAssembly 路徑上的結果。
實際上這里還有個相當有趣的問題就是構造「有趣」的數學表達式以用于測試,這并非一個顯然的問題,我們使用了很多 property based testing 中的生成器設計方法來完成此事,對此感興趣的讀者可以見我們之前發表的關于 QuickCheck 的文章。
評估方法
Symbit 的 benchmark 系統建立在 symbench 之上。每個 benchmark case 都包含三部分:MoonBit 側輸入構造器、sympy 側 oracle 定義,以及統一的運行入口。在進入計時之前,所有 case 都必須先通過語義校驗,也就是先確認
![]()
再把同一 workload 交給基準腳本計時。無論何時我們都應該先驗證語義正確性,再比較性能,畢竟更快地得到錯誤結果并沒有什么意義。
考慮到 Python 具有啟動成本,benchmark 為盡可能聚焦算法內核本身的運行代價,應該啟動之后多次重復執行同一樣例來攤薄啟動成本。至于 Symbit 則使用 native-binary + release 路徑,在這一定義下,本文采用如下速度比:
![]()
若該值大于 ,則表示 Symbit 更快。本文收集了幾種常見這些報表按負載家族分別整理,例如 sympolys-all 、 symsolvers-all 、 symseries-all 等,基本上也能對應到 symbit / sympy 的包結構。
基準測試結果
Native 路徑上的結果相當不錯,在當前已經整理成穩定分包報表的 workload 上,Symbit 在多數包內都取得了穩定領先,且這種領先并不局限于某一種類型的算法。在多項式、求解器、整數論、級數、集合運算、積分以及部分組合函數 workload,都表現出了明顯的優勢。
下表匯總了若干當前已經穩定維護的分包報表。其中「范圍」表示該包內最慢與最快樣例的加速比區間,而「代表 case」則列出若干最能體現該類 workload 特征的樣例。
![]()
下圖給出了 symseries 分包報表中的加速比分布,可以直觀看到不同多項式工作負載之間的差異。其中橫軸是具體 benchmark 樣例,縱軸是相對于 sympy 的加速比。
![]()
可見不同的負載均得到了提升,而不是某一個特例。更多結果表明,在 sympolys 中, gcd 、 resultant 、 discriminant 、 sturm sequence 、 Groebner basis 與有理插值都明顯快于 sympy;在 symsolvers 中,線性方程組、非線性方程組、 solveset 與線性規劃也都表現出穩定優勢。這說明速度提升并不是來自某一個特殊路徑,而是來自對象表示、規范化、精確域算術與底層算法組織方式的共同變化。在結構更復雜、對象層成本更突出的 case 上,提升還會繼續擴大。當然,在一些已經相對緊湊的組合或微積分核上,結果也可能只領先 1.x 到 3.x 。我們也對 WebAssembly 平臺進行了測試(對不 sympy 本地解釋),也有 1.x ~ 3.x 的整體提升,不過幅度并未有 native 這么大,它更大的優勢是瀏覽器部署,并且輸出小巧。
結果解讀與局限
這些評估結果支持了本文的基本判斷:把 sympy 風格的符號對象系統遷移到 MoonBit,并改變對象表示、規范化鏈路與底層精確算術的執行特性,確實帶來了可觀的性能提升。在一批經過 oracle 校驗的代表性負載上,這種變化最終表現為穩定的 native 優勢,以及仍具競爭力的 WebAssembly 路徑。
但與此同時,本文并不將這些數字解釋為「所有符號負載都會自動得到同等提升」。首先,當前報表雖然已經覆蓋了多項式、求解器、整數論、積分、集合、級數、函數與部分微積分,但它們仍然是經過篩選和構造的 kernel family,而不是完整地覆蓋整個 sympy API 空間。其次,benchmark 結果本身也是系統演化過程的一部分。某些樣例在項目早期曾經顯著落后,后來通過對齊 sympy 代碼路徑、調整對象表示或改進 exact kernel 而被拉回;這意味著評估系統同時也是性能診斷系統,而不僅是展示系統。若要把它推廣為一個更完整的學術評估,還需要在未來補充更多內容:例如更系統的 workload 分類、更大規模的隨機與性質測試集、更細粒度的 profiler 分析,以及跨版本的 longitudinal benchmark。
綜合來看,Symbit 的評估結果并不是「證明一切都已經完成」,而是表明這條技術路線是成立的:一個保持 sympy 風格語義與精確計算能力的符號系統,確實可以在編譯型語言中獲得顯著的內核運行收益,并同時保留進入瀏覽器環境的現實可行性。
瀏覽器 Demo
本項目還利用 MoonBit 的多后端能力打造了一個 Symbit Web-Demo,名為 symweb : https://caimeo.space/symweb
我們將計算內核編譯到 WASM 加速,而前端 GUI 部分則使用 Rabbita 編譯到 JavaScript。兩者之間通過一層很薄的 JSON bridge 通訊。另外借助 KaTeX 來渲染 LaTeX 輸出,整個系統在瀏覽器端形成了一個小型的 CAS 交互界面。 sympy 的經典用法往往需要安裝 Python 環境、配置依賴、編寫腳本或 notebook,而我們通過 Symweb 把這些交互直接搬到了瀏覽器里,輕量用戶可以直接在頁面里編輯表達式、切換 parser 選項、觀察結構化結果,并即時得到反饋。我們在 Demo 里面實現了大部分常用的 symbolic API,例如 simplify 、 expand 、 factor 、 solve 、 series 等,并且在一些頁面里還展示了表達式的結構化表示、LaTeX 輸出以及求值結果等多層信息。并且頁面相當小,整個前端代碼和內核編譯之后不到 3 MB,相比之下若我們想要把 sympy 運行在瀏覽器上,需要打包 Pyodide 運行時、自己單獨寫膠水代碼和 std 支持,然后加載 sympy 和 mpmath 本體,肯定不會這么簡潔 (運行性能更不用說了)。
![]()
Symbit Playground - caimeo.space/symweb
當然, symweb 仍然只是一個 demo,而不是完整的 notebook 或 CAS IDE。它目前更適合展示一個函數如何解析輸入、如何形成結構化結果、以及在當前 kernel 中給出什么輸出,而不是承擔長會話、多文檔、任意插件或大規模可視化等更重的任務。對于這種復雜任務,我們建議直接本地安裝 Symbit 體驗。
未來工作
雖然我們已經取得了許多進展,但未來仍有很多工作需要完成。除了繼續部分關鍵包的語義,減少與 Sympy 在語義上的差距。并逐步推進一些更復雜的算法實現和 Bug 修復:即使是 Sympy 本身也有數千個 issues 待修復和 PR 等待合并,我們希望 Symbit 能一定程度上跟隨這些變化和改進,甚至走在前面。
結論
本文致此完結,我們討論了 Sympy 風格的符號表達、精確計算與常見 CAS 算法,并不必然依賴 Python 這一宿主環境。通過 MoonBit 這樣的編譯型多后端語言,可以把同樣的語義重建為一個能夠同時面向 native 與 WebAssembly 的符號計算內核;在這一過程中,表達式表示、規范化、精確數值、多項式與矩陣算法、測試基礎設施與部署方式都會被重新組織。當前結果表明,這條路線不僅在工程上可行,而且在若干典型 workload 上已經能夠提供穩定的性能收益,并形成一個真實可交互的瀏覽器前端。這并不意味著 Symbit 已經完成,但至少說明:構建一個編譯型、可部署、且保持精確語義的現代 CAS 內核,是一條成立且值得繼續推進的方向。
若讀者覺得這個項目有趣,歡迎訪問 https://github.com/CAIMEOX/symbit.git 以了解更多細節。
特別聲明:以上內容(如有圖片或視頻亦包括在內)為自媒體平臺“網易號”用戶上傳并發布,本平臺僅提供信息存儲服務。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.