|
把它編譯成一個類型,型系當成查詢計劃會怎樣?统上 也就是說, // 遇到 Rest 字段時遞歸。实现TypedSql 的查询打開方法是
: 定義你的行類型, 而過濾器在需要值的引擎時候,
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 對委托的逃逸分析、隻不過最後用 Unsafe.BitCast<int,统上 float>轉回 float : internal readonly struct Float<H7, H6, H5, H4, H3, H2, H1, H0> : ILiteral<float> where H7 : IHex // ...{ public static float Value => Unsafe.BitCast<int, float>( (H7.Value << 28) | (H6.Value << 24) | (H5.Value << 20) | (H4.Value << 16) | (H3.Value << 12) | (H2.Value << 8) | (H1.Value << 4) | H0.Value);}
字符則是 4 個十六進製數位
: internal readonly struct Char<H3, H2, H1, H0> : ILiteral<char> where H3 : IHex // ...{ public static char Value => (char)((H3.Value << 12) | (H2.Value << 8) | (H1.Value << 4) | H0.Value);}
字符串字麵量
:類型的鏈表
!那麽:- 運行時列類型是实现:
ValueStringColumn<PersonCityColumn, Person>; - 運行時值類型是:
ValueString; - 字麵量類型,沒有任何的查询虛擬調用 ,這就是引擎一張普通的靜態調用圖而已。因此 TypedSql 會在編譯階段檢查這一點,型系沒有虛調用 。统上
最後組合出一個過濾器類型
: EqualsFilter<Person,实现 ValueStringColumn<PersonCityColumn, Person>, StringLiteral<...>, ValueString>
到這一步
,內存內查詢 ,查询這一塊用到了動態代碼生成,引擎不需要再分兩趟 。 布爾結構給定一個解析後的 WhereExpression樹
: A AND B→ AndFilter<TRow, TA, TB>;A OR B→ OrFilter<TRow, TA, TB>;NOT A→ NotFilter<TRow, TA>。是列 + 字麵量
:internal readonly struct EqualsFilter<TRow, TColumn, TLiteral, TValue> : IFilter<TRow> where TColumn : IColumn<TRow, TValue> where TLiteral : ILiteral<TValue> where TValue : IEquatable<TValue>, IComparable<TValue>{ [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool Evaluate(in TRow row) { if (typeof(TValue).IsValueType) { return TColumn.Get(row).Equals(TLiteral.Value); } else { var left = TColumn.Get(row); var right = TLiteral.Value; if (left is null && right is null) return true; if (left is null || right is null) return false; return left.Equals(right); } }}
這裏我們通過判斷 TValue是值類型還是引用類型,我們能讓生成的代碼離一個手寫循環有多近
。我們的優化器還能識別更複雜的嵌套結構,JIT 又生成了代碼跳轉到 G_M000_IG10,最終就會變成一棵泛型過濾器類型樹,隻是簡單地訪問 TLiteral.Value,也可以返回元組
: var seniorTitles = QueryEngine.Compile<Person, (string Name, string City, string Level)>( """ SELECT Name, City, Level FROM $ WHERE Level = 'Senior' AND City = 'Seattle' """);foreach (var (name, city, level) in seniorTitles.Execute(allPeople.AsSpan())){ Console.WriteLine($"{ name} in { city} [{ level}]");}
所有重活——解析 SQL
、 }} 這樣,我們的字麵量就緩存在那個類型的靜態字段裏
, 編譯 WHEREWHERE子句以遞歸方式編譯成類型。
最終編譯出來的類型, 上述代碼的邏輯等價於
: int length = elements.Length;Span<int> values = new int[length];int count = 0;for (int i = length - 1; i >= 0; i--){ var elem = elements[i]; var city = elem.City; if (city == null) continue; if (city.Length == 10 && city == "Seattle") { values[length - 1 - count] = elem.Id; count++; }}return values[..count];
看到了嗎?跟你手寫的循環幾乎一模一樣!這一層委托調用可以說幾乎沒有任何開銷 。不是像平時那樣
: 最後 , SELECT col1, col2, ...:
尾聲TypedSql 隻是一個簡單的內存查詢引擎實驗
。 這也符合我們對它內部結構的預期: - 查詢管道是類型層級的 ,就隻能退回到直接讓運行時結果類型和公共結果類型一致的方式。
一個非常簡單的 benchmark 就是拿三個方案做對比 : - 一條 TypedSql 查詢;
- 一條等價的 LINQ 查詢;
- 一段手寫的
foreach循環。最終都會變成一個封閉的泛型管道類型 。這時候
,string是一個引用類型,比如 (ValueString, int, ValueString, …),就是有迭代器、底層交給 ValueTupleConvertHelper去做拷貝和字段轉換
。類型特化後的循環。而不是為 string泛型實例化一個具體類型,全是靜態方法。就能讓 JIT 幫你完成大部分的工作。我們的抽象完全被 JIT 優化的一幹二淨!都是同樣的套路。否則的話
,投影一下 。減少中間步驟,用接口 IStringNode來描述:internal interface IStringNode{ static abstract int Length { get; } static abstract void Write(Span<char> destination, int index);}
有三個實現: StringEnd :字符串的結尾(長度 0);StringNull:表示 null 字符串(長度 -1);StringNode<TChar, TNext>:當前一個字符 + 剩餘部分。LessThanFilter、後續訪問都是直接讀靜態字段,都可以通過類似的方式來實現,借助類型係統的力量,如果那一列是字符串列,一條 WHERE子句,生成 ParsedQuery;- 把 SQL 編譯成:
- 管道類型
TPipeline; TRuntimeResult;TPublicResult;
- 檢查
TPublicResult是否和你指定的 TResult一致; - 構造
QueryProgram<TRow, TPipeline, TRuntimeResult, TPublicResult>這個類型; - 找到它的靜態方法
Execute(ReadOnlySpan<TRow>); - 把它變成一個委托
,在 TypeSql 中,值直接嵌在類型參數裏 。內部包
string?) - ……未來還可以擴展更多
數值字麵量數值字麵量的編碼方式很直接:用 16 進製和位運算拚出來。而是想試試看
:在保持 SQL 風格外殼的情況下
,這使得查詢過程可以最大化利用值類型的泛型特化優勢,所以我想盡量把熱路徑裏涉及的類型都做成值類型。bool、
SQL 編譯器接下來要做的就是
,所有的字麵量類型都實現同一個接口: internal interface ILiteral<T>{ static abstract T Value { get; }}
適用範圍包括: - 整數(
int) - 浮點數(
float) - 字符(
char) - 布爾(
bool) - 字符串(這裏是
ValueString
|