字麵量工廠
上麵這些編碼最後都歸到一個工廠類裏統一封裝 :
internal static class LiteralTypeFactory{ public static Type CreateIntLiteral(int value) { ... } public static Type CreateFloatLiteral(float value) { ... } public static Type CreateBoolLiteral(bool value) { ... } public static Type CreateStringLiteral(string?统上 value) { ... }}SQL 編譯階段會根據兩方麵信息來調用它:
- 列的運行時類型(
int、
比如 Where節點大概長這樣:
internal readonly struct Where<TRow,实现 TPredicate, TNext, TResult, TRoot> : IQueryNode<TRow, TResult, TRoot> where TPredicate : IFilter<TRow> where TNext : IQueryNode<TRow, TResult, TRoot>{ public static void Run(ReadOnlySpan<TRow> rows, scoped ref QueryRuntime<TResult> runtime) { for (var i = 0; i < rows.Length; i++) { Process(in rows[i], ref runtime); } } public static void Process(in TRow row, scoped ref QueryRuntime<TResult> runtime) { if (TPredicate.Evaluate(in row)) { TNext.Process(in row, ref runtime); } }}關鍵點在於 :
- 管道的形狀,這一層委托調用可以說幾乎沒有任何開銷。查询
編譯
WHEREWHERE子句以遞歸方式編譯成類型。引擎甚至是型系語言運行時等複雜係統,前言
在 .NET 裏寫查詢的统上時候,而這並不需要複雜的实现優化算法 ,然後所有實際運行時的查询邏輯都走靜態方法 。有幾個好處:
- 熱路徑裏盡量是引擎值類型 ,在類型係統裏搭管道——都發生在編譯查詢這一步 。型系
SELECT col1,统上 col2, ...:- 分別解析每一列;
- 構造一個
ValueTupleProjection,TypedSql 裏有一個很小的实现優化器,
列和投影
查詢總得運行在某種行類型
TRow上,查询.NET 又能針對這些類型生成多快的引擎代碼?於是 ,展開、會自然落到一套具體的設計上 。TypedSql 會構造專門的投影 ,我想針對每一個 SQL 語句都生成一份獨特的類型,再把結果轉交給
Stop.Process處理 。
對 JIT 來說,你既可以直接拿去執行,
兩邊都是某種
ValueTuple形狀
→ 用AsValueTupleRows<TPublicResult>(),我們就可以基於某個IStringNode,SELECT *最簡單的情況就是:
SELECT * FROM $。沒有虛調用 。把字符串塞進類型
LiteralTypeFactory.CreateStringLiteral負責把字符串字麵量轉換成這樣一個類型:public static Type CreateStringLiteral(string? value){ if (value is null) { return typeof(StringLiteral<StringNull>); } var type = typeof(StringEnd); for (var i = value.Length - 1; i >= 0; i--) { var charType = CreateCharType(value[i]); // Char<...> type = typeof(StringNode<,>).MakeGenericType(charType, type); } return typeof(StringLiteral<>).MakeGenericType(type);}比如我們有一個字麵量
'Seattle',可以這麽寫:internal readonly struct ColumnProjection<TColumn, TRow, TValue> : IProjection<TRow, TValue> where TColumn : IColumn<TRow, TValue>{ public static TValue Project(in TRow row) => TColumn.Get(row);}多列選擇時,
最後組合出一個過濾器類型:
EqualsFilter<Person, ValueStringColumn<PersonCityColumn, Person>, StringLiteral<...>, ValueString>到這一步 ,它實現 IQueryNode<TRow, TRuntimeResult, TRoot>;
TRuntimeResult;TPublicResult。一套代碼同時支持 JIT 和 AOT
!去虛擬化和內聯等優化 ,null和 ""在類型層麵和運行時都可以被區分開。'e' 、會生成一個 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,從而實際上並不存在任何的分支開銷。沒有任何的運行時分發
,都隻是跑一遍已經專門化好的靜態管道 ,SQL 編譯器接下來要做的就是,投影、
字符串字麵量就比較有趣了 。不是像平時那樣:
- 在運行時構建一棵表達式樹 ,
搭好整個管道類型
到目前為止 ,
順著這個想法 ,我們就可以把一個
Where節點掛到管道上了:Where<TRow, TPredicate, TNext, TRuntimeResult, TRoot> → ...把
Where和Select融合起來直接這麽拚出來的管道是正確的,還根據它生成了專門的代碼路徑 !
CompiledQuery<TRow, TResult>本身隻是包了一個委托:
private readonly Func<ReadOnlySpan<TRow>, IReadOnlyList<TResult>> _entryPoint = executeMethod.CreateDelegate<Func<ReadOnlySpan<TRow>, IReadOnlyList<TResult>>>();然後對外暴露 :
public IReadOnlyList<TResult> Execute(ReadOnlySpan<TRow> rows) => _entryPoint(rows);得益於 .NET 10 對委托的逃逸分析、我們的引擎是完全支持來自外部的動態輸入的,
這樣一來 ,同時對外還不需要暴露這些內部細節,投影 、
最終的效果就是 :WHERE 子句裏每一個字麵量 ,而不是為 string泛型實例化一個具體類型 ,避免了運行時的計算;而 dec esi更是直接把遞增的循環優化成了遞減 ,
過濾器
過濾器的接口長這樣:
internal interface IFilter<TRow>{ static abstract bool Evaluate(in TRow row);}一個最常用的比較過濾器形式,在 TypeSql 中
,我們能讓生成的代碼離一個手寫循環有多近
。外麵希望看到 string
→ 調用 AsStringRows,但代碼稍微有點囉嗦;
解析階段讀到
'Seattle',從而在保持靈活性的同時 ,把列名映射到具體的IColumn<TRow, TValue>實現;- 一套機製
,生成非常高效的代碼
。
展望未來的應用,用聲明的 CLR 類型(如
string) 。否則的話, - 過濾出
City == "Seattle"的行; - 返回它們的
Id。以及這個字麵量能不能用在那一列上之類的問題 ,而你甚至不需要實現任何的代碼生成後端 ,再通過 NativeAOT 編譯成原生二進製文件,Select、一旦這些泛型類型參數都被代入,達到了性能和易用性的平衡。不需要再分兩趟 。邏輯運算也是在類型層麵組合的:
internal readonly struct AndFilter<TRow, TLeft, TRight> : IFilter<TRow> where TLeft : IFilter<TRow> where TRight : IFilter<TRow>{ public static bool Evaluate(in TRow row) => TLeft.Evaluate(in row) && TRight.Evaluate(in row);}internal readonly struct OrFilter<TRow, TLeft, TRight> : IFilter<TRow> where TLeft : IFilter<TRow> where TRight : IFilter<TRow>{ public static bool Evaluate(in TRow row) => TLeft.Evaluate(in row) || TRight.Evaluate(in row);}internal readonly struct NotFilter<TRow, TPredicate> : IFilter<TRow> where TPredicate : IFilter<TRow>{ public static bool Evaluate(in TRow row) => !TPredicate.Evaluate(in row);}所以,你照樣寫
string, Where<TRow, TPredicate, Select<TRow, TProjection, TNext, TMiddle, TResult, TRoot>, TResult, TRoot>
任務內容 :
編譯器做的事情,
在 JIT 看來,
之後每次 .Execute
,會去找這樣的模式:
一旦發現
,就是有迭代器、而外麵看到的則是 (string, int, string, …)