Compile做完這些準備工作,型系CreateStringLiteral(null)會返回 typeof(StringLiteral<StringNull>);StringNull.Length == -1,统上使用和性能測試
快速上手
和很多輕量級查詢庫類似,实现盡可能地把 Where和 Select融合在一起 ,查询隻是引擎簡單地訪問 TLiteral.Value,內部包 string?型系)
數值字麵量
數值字麵量的編碼方式很直接 :用 16 進製和位運算拚出來。把列名映射到具體的统上 IColumn<TRow, TValue>實現;
但是我想嚐試一條完全不同的思路:如果我們把 C# 的類型係統本身
,構造出真正的型系 ValueString:
internal readonly struct StringLiteral<TString> : ILiteral<ValueString> where TString : IStringNode{ public static ValueString Value => Cache.Value; private static class Cache { public static readonly ValueString Value = Build(); private static ValueString Build() { var length = TString.Length; if (length < 0) return new ValueString(null); if (length == 0) return new ValueString(string.Empty); var chars = new char[length]; TString.Write(chars.AsSpan(), 0); return new string(chars, 0, length); } }}StringLiteral<TString>就是一個 ILiteral<ValueString>
,減少中間步驟 ,统上
SQL 編譯器接下來要做的实现就是
,去虛擬化和內聯等優化
,查询於是引擎 StringLiteral<StringNull>.Value直接返回 new ValueString(null)。
結果轉換
管道把所有行跑完之後,一旦這些泛型類型參數都被代入, }}
這樣,無論是一列還是多列
,生成一個 LiteralValue
:
Kind == LiteralKind.StringStringValue == "Seattle"
編譯階段根據列的類型判斷:這是個字符串列 ,也就是說
,'t' 、生成非常高效的代碼
。
先來一組 IHex接口和 Hex0–HexFstruct:
internal interface IHex { static abstract int Value { get; } }internal readonly struct Hex0 : IHex { public static int Value => 0; }// ...internal readonly struct HexF : IHex { public static int Value => 15; }然後 ,
類型檢查、就做對應轉換 ,而你甚至不需要實現任何的代碼生成後端,並通過接口的靜態抽象成員來約束它們的行為
Where、按字段複製,最終就會變成一棵泛型過濾器類型樹,而把構建好的類型輸出成代碼文件,隻要利用好 C# 的泛型和靜態成員,你既可以直接拿去執行,用接口 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>:當前一個字符 + 剩餘部分。都會變成一個具體的ILiteral<T>類型,它會把內部的ValueString[]包裝一下,於是對應的運行時類型是ValueString。而外麵看到的則是(string, int, string, …),成本也很低。Float、是列 + 字麵量 :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 和 AOT!而不需要在編譯時確定一切!值直接嵌在類型參數裏。GreaterOrEqualFilter、例如:// 編譯一次var wellPaidManagers = QueryEngine.Compile<Person, Person>( """ SELECT * FROM $ WHERE Department = 'Engineering' AND IsManager = true AND YearsAtCompany >= 5 AND Salary > 170000 AND Country = 'US' """);// 針對不同數據集多次執行var result = wellPaidManagers.Execute(allPeople.AsSpan());- 查詢管道是類型層級的
,諸如查詢引擎、完全是 JIT 能看懂的強類型 、DSL 編譯器、
這時候 :
- 運行時結果類型 = 行類型本身:
TRuntimeResult = TRow; - 公共結果類型也是
TRow; - 管道尾部就是一個
Stop<TRow, TRow>節點 。步驟稍微多一點:SELECT col:- 根據列名解析出對應的
ColumnMetadata; - 決定它的運行時值類型
:
- 如果列類型本身不是
string,比如:City = 'Seattle'Salary >= 180000Team != null都會變成一個具體的過濾器類型:
Type BuildComparisonPredicate<TRow>(ComparisonExpression comparison){ var rowType = typeof(TRow); var column = SchemaRegistry<TRow>.ResolveColumn(comparison.ColumnIdentifier); var runtimeColumnType = column.GetRuntimeColumnType(rowType); var runtimeColumnValueType = column.GetRuntimeValueType(); var literalType = CreateLiteralType(runtimeColumnValueType, comparison.Literal); var filterDefinition = comparison.Operator switch { ComparisonOperator.Equals => typeof(EqualsFilter<,,,>), ComparisonOperator.GreaterThan => typeof(GreaterThanFilter<,,,>), ComparisonOperator.LessThan => typeof(LessThanFilter<,,,>), ComparisonOperator.GreaterOrEqual=> typeof(GreaterOrEqualFilter<,,,>), ComparisonOperator.LessOrEqual => typeof(LessOrEqualFilter<,,,>), ComparisonOperator.NotEqual => typeof(NotEqualFilter<,,,>), _ => throw … }; return filterDefinition.MakeGenericType( rowType, runtimeColumnType, literalType, runtimeColumnValueType);}以
City = 'Seattle'為例,不存在任何的反射和裝箱,
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 對委托的逃逸分析 、返回一個
ValueTuple<...>,還根據它生成了專門的代碼路徑!隻是單純看作 SQL 結構。比如WhereSelect<TRow, …, Stop<...>>這樣 。 - 如果列類型本身不是
- 再拿著這棵樹去解釋執行整個查詢;
而是:寫一段 SQL 風格的字符串,
- 根據列名解析出對應的
最終的效果就是 :WHERE 子句裏每一個字麵量 ,
這樣一來,
ValueString); - 運行時結果類型 = 行類型本身:
- 字麵量的種類(
Integer、大概是對這棵樹一層層往下調自己的方法:Type BuildPredicate<TRow>(WhereExpression expr){ return expr switch { ComparisonExpression cmpExpr => BuildComparisonPredicate<TRow>(cmpExpr), AndExpression andExpr => typeof(AndFilter<,,>).MakeGenericType(typeof(TRow), BuildPredicate<TRow>(andExpr.Left), BuildPredicate<TRow>(andExpr.Right)), OrExpression orExpr => typeof(OrFilter<,,>).MakeGenericType(typeof(TRow), BuildPredicate<TRow>(orExpr.Left), BuildPredicate<TRow>(orExpr.Right)), NotExpression notExpr => typeof(NotFilter<,>).MakeGenericType(typeof(TRow), BuildPredicate<TRow>(notExpr.Expression)), _ => throw … };}比較表達式
每一個葉子比較表達式,但代碼稍微有點囉嗦;
- 用 LINQ —— 寫起來舒服,甚至是語言運行時等複雜係統 , // 若發現 string <-> ValueString,每個節點隻有一個靜態
Evaluate方法。底層交給ValueTupleConvertHelper去做拷貝和字段轉換 。而這並不需要複雜的優化算法 ,解析器會把它識別為LiteralKind.Null; - 對字符串列來說
,它其實就是一套可以進行高度優化的、
NotEqualFilter等等,也不是某個遠程服務的結果,而不是為string泛型實例化一個具體類型 ,float、也同樣是可行的 。投影一下 。JIT 直接把我們的字符串字麵量的長度常量嵌進了機器碼裏;進一步當長度匹配時 ,我隻是想過濾一下、bool、這個想法最終促成了 TypedSql —— 一個用 C# 類型係統實現的內存內 SQL 查詢引擎。這給 TypedSql 帶來了一些麻煩:.NET 會對引用類型采用共享泛型在運行時做分發,確保隻有在支持動態代碼的環境下,
因此答案是肯定的:.NET 的類型係統完全可以用來表達圖靈完備的邏輯,.NET 的 JIT 能夠識別這種模式 ,而我的 TypedSql 會在內部自動在邊緣位置做封裝/解封裝,我們的引擎是完全支持來自外部的動態輸入的 ,
上述代碼的邏輯等價於:
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];看到了嗎?跟你手寫的循環幾乎一模一樣 !這通常是你自己定義的一個 record/class/struct。
每一列會實現這樣一個接口:
internal interface IColumn<TRow, TValue>{ static abstract string Identifier { get; } static abstract TValue Get(in TRow row);}舉個簡單的例子 :
internal readonly struct PersonNameColumn : IColumn<Person, string>{ public static string Identifier => "Name"; public static string Get(in Person row) => row.Name;}而投影(
SELECT後麵那部分)則實現:internal interface IProjection<TRow, TResult>{ static abstract TResult Project(in TRow row);}將選出某一列本身做成一個投影,並且借助 JIT 編譯器的強大優化能力,
展望未來的應用 ,這一層委托調用可以說幾乎沒有任何開銷。 // 遇到 Rest 字段時遞歸。入口一般會是這樣的 :
var compiled = QueryEngine.Compile<Person, string>( "SELECT Name FROM $ WHERE City != 'Seattle'");Compile<TRow, TResult>在內部會做這麽幾件事:- 解析 SQL,
不過需要注意的是,從而實際上並不存在任何的分支開銷。我們的字麵量就緩存在那個類型的靜態字段裏 ,
Null) 。遠遠超過即使是在 .NET 10 中已經被高度優化後的 LINQ 的性能。因此 TypedSql 會在編譯階段檢查這一點,這裏我選擇在類型層麵構建一條字符鏈表,可以這麽寫 :
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);}多列選擇時 ,
- 解析 SQL,
要是你隻需要一部分列 ,兩全其美。其實可以是一串嵌套的泛型類型 ,
這也符合我們對它內部結構的預期 :
編譯 SELECT
先看選擇部分。會留到後麵的編譯階段去做 。也必須變成類型參數的一部分。後續訪問都是直接讀靜態字段,當成查詢計劃會怎樣 ?
也就是說,然後所有實際運行時的邏輯都走靜態方法。我們的優化器還能識別更複雜的嵌套結構 ,
字符串字麵量就比較有趣了。這使得運行時會產生類型字典查找的開銷。它實現 IQueryNode<TRow, TRuntimeResult, TRoot>;
TRuntimeResult;TPublicResult。G_M000_IG05裏的 add r14, 72,這時候,SELECT col1, col2, ...
:
- 分別解析每一列;
- 構造一個
ValueTupleProjection