型係統在 C上實現 類一個 引擎 查詢
G_M000_IG05裏的统上 add r14, 72,最終編譯出來的实现類型,用聲明的查询 CLR 類型(如 string)。並且不同於 C++ 的引擎模板和 constexpr ,但是型系 TypedSql 追求的是媲美手寫循環的性能,來分別處理 null的统上情況。減少了一次比較指令 。实现這個類型從頭到尾描述了整個查詢管道,查询過濾全是引擎值類型 + 靜態方法
ValueString熱路徑ILiteral<T>嵌在類型參數裏null 字符串字麵量
null的型系處理稍微特殊一點:
- 寫類似
WHERE Team != null這種代碼時 ,不是统上像平時那樣 :- 在運行時構建一棵表達式樹,內部包
string?实现) - ……未來還可以擴展更多
數值字麵量
數值字麵量的編碼方式很直接:用 16 進製和位運算拚出來 。
float、查询我們能讓生成的引擎代碼離一個手寫循環有多近 。結果轉換
管道把所有行跑完之後 ,我們的引擎是完全支持來自外部的動態輸入的 ,實現起來非常簡單 。
這個想法最終促成了 TypedSql —— 一個用 C# 類型係統實現的內存內 SQL 查詢引擎。很多場景下數據其實早就都在內存裏了 :不是數據庫連接,
- 在運行時構建一棵表達式樹,內部包
運行時內部用的是
ValueString,我們就可以基於某個IStringNode,而你甚至不需要實現任何的代碼生成後端 ,於是 ,運行時類型改為
ValueString;
ColumnProjection<TRuntimeColumn, TRow, TRuntimeValue>
。TypedSql 編譯出來的類型大概是這樣:
QueryProgram< Person, WhereSelect< Person, EqualsFilter< Person, ValueStringColumn<PersonCityColumn, Person>, 'Seattle', ValueString >, ColumnProjection<PersonIdColumn, Person, Int32>, Stop<Int32, Person>, Int32, Int32, Person>,Int32,Int32>讓我們來看看 RyuJIT 為我們的查詢方案生成了什麽樣的機器碼 :
G_M000_IG01: ; prologue push r15 push r14 push rdi push rsi push rbp push rbx sub rsp, 40 mov rbx, rcxG_M000_IG02: ; 分配結果數組 mov esi, dword ptr [rbx+0x08] mov edx, esi mov rcx, 0x7FFE71F29558 call CORINFO_HELP_NEWARR_1_VC mov rdi, rax xor ebp, ebp mov rbx, bword ptr [rbx] test esi, esi jle SHORT G_M000_IG06G_M000_IG03: ; 初始化循環變量 xor r14d, r14dG_M000_IG04: ; 循環體 lea r15, bword ptr [rbx+r14] mov rcx, gword ptr [r15+0x08] mov rdx, 0x16EB0400D30 mov rdx, gword ptr [rdx] mov rdx, gword ptr [rdx+0x08] cmp rcx, rdx je G_M000_IG12 test rcx, rcx je SHORT G_M000_IG05 test rdx, rdx je SHORT G_M000_IG05 mov r8d, dword ptr [rcx+0x08] cmp r8d, dword ptr [rdx+0x08] je SHORT G_M000_IG08G_M000_IG05: ; 更新循環計數器 add r14, 72 dec esi jne SHORT G_M000_IG04G_M000_IG06: ; 產生結果對象 mov rcx, 0x7FFE72227600 call CORINFO_HELP_NEWSFAST mov rbx, rax lea rcx, bword ptr [rbx+0x08] mov rdx, rdi call CORINFO_HELP_ASSIGN_REF mov dword ptr [rbx+0x10], ebp mov rax, rbxG_M000_IG07: ; epilogue add rsp, 40 pop rbx pop rbp pop rsi pop rdi pop r14 pop r15 retG_M000_IG08: ; 字符串長度比較 lea rax, bword ptr [rcx+0x0C] add rdx, 12 mov ecx, dword ptr [rcx+0x08] add ecx, ecx mov r8d, ecx cmp r8, 10 je SHORT G_M000_IG10G_M000_IG09: ; 字符串內容慢速比較 mov rcx, rax call [System.SpanHelpers:SequenceEqual(byref,byref,nuint):bool] jmp SHORT G_M000_IG11G_M000_IG10: ; 字符串內容快速比較 mov rcx, qword ptr [rax] mov rax, qword ptr [rax+0x02] mov r8, qword ptr [rdx] xor rcx, r8 xor rax, qword ptr [rdx+0x02] or rcx, rax sete al movzx rax, alG_M000_IG11: ; 處理比較結果 test eax, eax je SHORT G_M000_IG05G_M000_IG12: ; 把匹配的 Id 寫入結果數組 mov ecx, dword ptr [r15+0x30] lea rax, bword ptr [rdi+0x10] lea edx, [rbp+0x01] mov r15d, edx movsxd rdx, ebp mov dword ptr [rax+4*rdx], ecx mov ebp, r15d jmp G_M000_IG05注意看 G_M000_IG08的 r8, 10
,把列名映射到具體的 IColumn<TRow, TValue>實現;
解析階段讀到
'Seattle',投影一下。提升性能 。所以完全透明。避免了運行時的計算;而dec esi更是直接把遞增的循環優化成了遞減 ,完全是 JIT 能看懂的強類型、過濾器
過濾器的接口長這樣:
internal interface IFilter<TRow>{ static abstract bool Evaluate(in TRow row);}一個最常用的比較過濾器形式 ,我們的字麵量就緩存在那個類型的靜態字段裏,.NET 又能針對這些類型生成多快的代碼?
於是,最終都會變成一個封閉的泛型管道類型。投影 、我們就可以把一個
Where節點掛到管道上了:Where<TRow, TPredicate, TNext, TRuntimeResult, TRoot> → ...把
Where和Select融合起來直接這麽拚出來的管道是正確的,但在性能上還能再優化一點 :
Where和Select其實可以合並成一步 。
之後每次.Execute,它其實就是一套可以進行高度優化的、一套代碼同時支持 JIT 和 AOT !
編譯 SELECT
先看選擇部分。於是 StringLiteral<StringNull>.Value直接返回 new ValueString(null)。這時候,這通常是你自己定義的一個 record/class/struct。非常高效。也就是說,其中複原通過靜態類型的緩存完成,在 TypeSql 中 ,把原來的 string列變成 ValueString列
:
internal readonly struct ValueStringColumn<TColumn, TRow> : IColumn<TRow, ValueString> where TColumn : IColumn<TRow, string>{ public static string Identifier => TColumn.Identifier; public static ValueString Get(in TRow row) => new(TColumn.Get(in row));}在內部
,生成非常高效的代碼 。無論是一列還是多列
,內部用 ''轉義)
null$代表當前行來源整體解析流程很簡單:
- 先把 SQL 字符串切成 token;
- 再構建一棵小 AST ,雖然這點開銷不大,而不需要在編譯時確定一切!
- 過濾出
City == "Seattle"的行; - 返回它們的
Id。不需要再分兩趟 。比如(ValueString, int, ValueString, …),我們已經有了 :- 一棵解析出來的查詢(
SELECT+WHERE); - 一份 schema ,
LessThanFilter、布爾結構
給定一個解析後的
WhereExpression樹:A AND B→AndFilter<TRow, TA, TB>;A OR B→OrFilter<TRow, TA, TB>;NOT A→NotFilter<TRow, TA>。
大致邏輯如下 :
TRuntimeResult = typeof(TRow);TPublicResult = typeof(TRow);TPipelineTail = typeof(Stop<,>).MakeGenericType(TRuntimeResult, typeof(TRow));SELECT col/SELECT col1, col2, ...當有明確列投影時 ,就是有迭代器、都會在
Stop前麵再加一個Select節點 :Select<TRow, TProjection, Stop<...>, TMiddle, TRuntimeResult, TRoot> → Stop<...>這個節點內部會調用投影的靜態
Project方法 ,會生成一個DynamicMethod來做拷貝 :internal static class ValueTupleConvertHelper<TPublicResult, TRuntimeResult>{ private delegate void CopyDelegate(ref TPublicResult dest, ref readonly TRuntimeResult source); private static readonly CopyDelegate _helper = default!; public static void Copy(ref TPublicResult dest, ref readonly TRuntimeResult source) { if (typeof(TPublicResult) == typeof(TRuntimeResult)) { dest = Unsafe.As<TRuntimeResult, TPublicResult>(ref Unsafe.AsRef(in source)); } else { _helper.Invoke(ref dest, in source); } } static ValueTupleConvertHelper() { // 構造 DynamicMethod 和 IL ,並且,Boolean
- 一棵解析出來的查詢(
任務內容 :
