用 Windsurf 從0實現(xiàn)高性能JSON解析器
為了探索和改進 AI 工具在編程方面的體驗,同時也想驗證一些 AI 的邊界,于是又想到了嘗試從 0 實現(xiàn)高性能JSON解析器,說干就干。開始以為比較簡單,不會超過半天就能實現(xiàn),但是經(jīng)過各種提示詞優(yōu)化,最終花了兩天時間...
1. 選用工具
現(xiàn)在有各種 AI Copilot,比較常用的 Cursor,Windsurf,Trae 等,不過我現(xiàn)在用的比較順手的是:Windsurf。除了編程工具,然后就是模型,目前代碼領(lǐng)域比較強的:
- Claude 3.7 Sonnet 和 Claude 3.7 Sonnet Thinking
- GPT-4.1
- o4-mini-high
- Gemini 2.5 Pro
我在 Windsurf 上,使用 Claude 3.7 Sonnet 和 GPT-4.1 互相切換,簡單問題 GPT-4.1 能快速解決,復雜的問題可以嘗試 Claude 3.7 Sonnet 和 Thinking 分析,不過在使用過程中發(fā)現(xiàn) Gemini 2.5 Pro 在分析性能上很強大,但是上下文不太夠(可能是 Windsurf 上下文導致 prompt 太多了),對于代碼超過 200 行的效果不是很好。
2. Prompt
如果使用輔助編程工具,其實 Prompt 不是特別重要,對于開發(fā)者最重要的是如何把需求描述清楚。
比如本項目最開始的 Prompt 是:
用 golang 實現(xiàn)一個類似標準庫 "encoding/json" 的 JSON 解析器,可以參考 github 的 cJSON
通過如上 Prompt,將會獲得比較粗的代碼實現(xiàn),這個時候不應該基于是實現(xiàn)其他的功能,而是開始讓 AI 幫你生成測試用例:
基于 @lexer.go 生成測試用例,其中測試用例需要覆蓋如下 token 的支持:
EOFToken // 文件結(jié)束標記
NumberToken // 數(shù)字標記,例如:123, 45.67
StringToken // 字符串標記,例如:"hello"
NullToken // null值標記
TrueToken // true布爾值標記
FalseToken // false布爾值標記
CommaToken // 逗號標記 ','
ColonToken // 冒號標記 ':'
LeftBraceToken // 左大括號標記 '{'
RightBraceToken // 右大括號標記 '}'
LeftBracketToken // 左方括號標記 '['
RightBracketToken // 右方括號標記 ']'
為什么生成測試用例很重要?(1)驗證代碼的功能性問題,當你的代碼覆蓋率做的足夠高,生成的代碼就更安全。(2)通過測試用例可以更好的提示 AI 生成代碼,類似觸發(fā) CoT(Chain of Thought)。(3)測試用例可以更好的幫助開發(fā)者理解獨立的模塊。
生成測試用例后,可能會遇到各種測試用例不通過的情況,Prompt 其實就將報錯信息輸入給 AI 即可:
@parser_test.go#L291-729 @types.go @parser.go 執(zhí)行測試用例出現(xiàn)棧溢出的錯誤:runtime: goroutine stack exceeds 1000000000-byte limit
runtime: sp=0x14020260340 stack=[0x14020260000, 0x14040260000]
fatal error: stack overflow
runtime stack:
runtime.throw({0x10308321f?, 0xa2284a1184611846?})
/opt/homebrew/Cellar/go/1.24.1/libexec/src/runtime/panic.go:1101 +0x38 fp=0x16d4ced90 sp=0x16d4ced60 pc=0x102f9fff8
runtime.newstack()
/opt/homebrew/Cellar/go/1.24.1/libexec/src/runtime/stack.go:1107 +0x45c fp=0x16d4ceed0 sp=0x16d4ced90 pc=0x102f8682c
runtime.morestack()
/opt/homebrew/Cellar/go/1.24.1/libexec/src/runtime/asm_arm64.s:342 +0x70 fp=0x16d4ceed0 sp=0x16d4ceed0 pc=0x102fa57f0
...
需要注意: 測試用例一定要縮小范圍,并且測試用例最好按照 ??標記 #%d 類型錯誤: 期望 %v, 得到 %v?
??(參考如下代碼),讓 AI 更好的理解問題出在哪里(??期望 -> 得到?
?)。
比如本項目的測試用例都會將錯誤信息,原始信息等都打印出來:
for _, tt := range tests {
testCase := tt // 避免閉包問題
t.Run(testCase.name, func(t *testing.T) {
lexer := NewLexer(testCase.input)
for i, expected := range testCase.expected {
got := lexer.NextToken()
if got.Type != expected.Type {
t.Errorf("標記 #%d 類型錯誤: 期望 %v, 得到 %v", i, expected.Type, got.Type)
}
if got.Value != expected.Value {
t.Errorf("標記 #%d 值錯誤: 期望 %q, 得到 %q", i, expected.Value, got.Value)
}
if got.Pos != expected.Pos {
t.Errorf("標記 #%d 位置錯誤: 期望 %d, 得到 %d", i, expected.Pos, got.Pos)
}
}
})
}
3. 限制文件大小
從實踐經(jīng)驗來看,隨著功能的疊加,AI 生成的代碼在單個文件會越來越長,但是這樣會遇到一些問題(上下文限制,模型思考慢,問題分析不準確),因此需要定期將單個文件按照功能拆分多個文件,建議單個文件不超過 200 行(測試用例倒是不需要,由于測試用例是單一功能的),在 ??sjson?
? 的代碼行數(shù)基本上都少于 200 行:
4. 提供方向性的指引
提出一個問題 讓 AI 解決,可能方案有很多,比如 JSON 解析可以用方案:
- 遞歸下降方法,邊解析邊賦值(流式解析)
- 分階段解析(詞法+語法分析)
但是 AI 一開始并不一定能給出最優(yōu)的方案,比如本項目開始提供分階段解析方案,但是參考其他的開源項目,都是用流式解析,該方案對于 JSON 解析器比較合適(因為沒有需要動態(tài)計算的過程,所以掃一遍就可以處理,性能要比分段解析好),當然也要考慮業(yè)務(wù)場景,比如要實現(xiàn)動態(tài)腳本或者 ??expr?
? 功能,分階段解析更合適,所以在實現(xiàn)之前可以先了解當前領(lǐng)域的知識并分析方案的優(yōu)劣勢(其實整個分析的過程也可以喂給 AI 來做判斷并糾正),然后讓 AI 按照提示的方向?qū)崿F(xiàn)。
不過值得注意的是,當提出用 JIT 等方案優(yōu)化,AI 會提示方案不合理(實現(xiàn)的確不合理,JIT 需要增加各種適配代碼)。
5. 充分的測試
??sjson?
?? 需要對比其他庫的功能是否完整,依賴 AI 通常不一定能給全所有的測試用例,所以需要找到測試套件,這里我參考開源項目 ??https://github.com/nst/JSONTestSuite?
??,修改 ??run_tests.py?
?:
programs = {
"JavaScript":
{
"url":"",
"commands":["node", os.path.join(PARSERS_DIR, "test_json.js")]
},
"Python 2.7.10":
{
"url":"",
"commands":["python", os.path.join(PARSERS_DIR, "test_json.py")]
},
"sjson":
{
"url":"",
"commands":["/Volumes/my/github/mylib/go/sjson/tests/example", "-a"]
},
"stdjson":
{
"url":"",
"commands":["/Volumes/my/github/mylib/go/sjson/tests/example", "-b"]
},
"jsoniterator":
{
"url":"",
"commands":["/Volumes/my/github/mylib/go/sjson/tests/example", "-c"]
},
}
可以跑通 ??300+?
?? 個測試用例,其中與標準庫 ??encoding/json?
?? 和 ??Jsoniter?
? 的對比如下:
6. 性能優(yōu)化
通過 AI 生成 ??Benchmark?
? 函數(shù),然后進行性能的測試:
BenchmarkComplexJSON/Original-14 13598248 5525 ns/op 9993 B/op 178 allocs/op
BenchmarkComplexJSON/Optimized-14 12703338 5804 ns/op 10645 B/op 179 allocs/op
BenchmarkComplexJSON/Standard-14 17148706 4125 ns/op 5136 B/op 107 allocs/op
以上是第一輪的性能測試,比標準庫性能差了 60%,然后可以將當前性能測試數(shù)據(jù)當成 Prompt 輸入:
@sjson_marshal.go 性能測試如下:
BenchmarkComplexJSON/Original-14 13598248 5525 ns/op 9993 B/op 178 allocs/op
BenchmarkComplexJSON/Optimized-14 12703338 5804 ns/op 10645 B/op 179 allocs/op
BenchmarkComplexJSON/Standard-14 17148706 4125 ns/op 5136 B/op 107 allocs/op
基于當前性能對比進行性能優(yōu)化,比如增加緩存,減少 decode 次數(shù),減少 fmt.Sprintf 改為 byte[] 和 append 或者 strings.Builder,目標是減少內(nèi)存分配和數(shù)據(jù)拷貝次數(shù)。
... ...
然后經(jīng)過一系列漫長的優(yōu)化,最后性能對比標準庫:
BenchmarkUnmarshalCompareMedium/SjsonUnmarshal-14 2203020 5576 ns/op 5923 B/op 115 allocs/op
BenchmarkUnmarshalCompareMedium/StdUnmarshal-14 1484354 8036 ns/op 504 B/op 11 allocs/op
BenchmarkUnmarshalCompareMedium/JsoniterUnmarshal-14 5908438 1986 ns/op 352 B/op 38 allocs/op
性能已經(jīng)比 ??encoding/json?
?? 標準庫提升 ??80%?
??,雖然比 ??Jsoniter?
?? 性能差(Jsoniter開啟了 ConfigFastest),但是可以嘗試引入 ??reflect2?
?? 等方案來提升性能,從 ??flamegraph?
? 看需要對反射和拷貝的方向進行優(yōu)化:
7. 后續(xù)
(1)代碼已經(jīng)開源:https://github.com/linkxzhou/mylib/tree/master/go/sjson(2)繼續(xù)性能優(yōu)化,嘗試探索讓 AI 如何對項目進行性能優(yōu)化和構(gòu)建 ??go pprof MCP Server?
?
本文轉(zhuǎn)載自???周末程序猿??,作者:周末程序猿
