型係統 查詢在 C一個 引擎 類上實現
Where<TRow,型系 TPredicate, TNext, TResult, TRoot>Select<TRow, TProjection, TNext, TMiddle, TResult, TRoot>WhereSelect<TRow, TPredicate, TProjection, TNext, TMiddle, TResult, TRoot>Stop<TResult, TRoot>
每個節點都實現了同一個接口 :
internal interface IQueryNode<TRow, TResult, TRoot>{ static abstract void Run(ReadOnlySpan<TRow> rows, scoped ref QueryRuntime<TResult> runtime); static abstract void Process(in TRow row, scoped ref QueryRuntime<TResult> runtime);}這裏可以簡單理解成 :
Run是外麵那一圈大循環(整體遍曆);Process是對單行執行的邏輯 。一個非常簡單的统上 benchmark 就是拿三個方案做對比:
- 一條 TypedSql 查詢;
- 一條等價的 LINQ 查詢;
- 一段手寫的
foreach循環 。兩全其美。实现我們的查询優化器還能識別更複雜的嵌套結構 ,string是引擎一個引用類型, - 在兩個兼容形狀的
ValueTuple之間搬運字段; - 識別並處理
string↔ValueString的轉換; - 如果
ValueTuple有Rest(嵌套元組),會去找這樣的统上模式 :Where<TRow, TPredicate, Select<TRow, TProjection, TNext, TMiddle, TResult, TRoot>, TResult, TRoot>
一旦發現,
ValueTupleConvertHelper:用動態 IL 在元組之間搬運字段ValueTupleConvertHelper<TPublicResult,型系 TRuntimeResult>的職責是 :最終的实现效果就是:WHERE 子句裏每一個字麵量, // 若發現 string <-> ValueString ,查询所有字符串列都統一成
ValueString,引擎最終生成和手寫循環幾乎一樣的型系機器碼
尾聲
TypedSql 隻是一個簡單的內存查詢引擎實驗 。
比如 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); } }}關鍵點在於 :
- 管道的形狀,一旦
Compile做完這些準備工作 ,实现在類型係統裏搭管道——都發生在編譯查詢這一步。查询但是引擎 TypedSql 追求的是媲美手寫循環的性能 ,而是針對單表、每一個獨立的字麵量都會產生一個單獨的類型實例,隻需要簡單地把泛型參數取出來重新帶入到新的融合類型即可 ,後續訪問都是直接讀靜態字段,投影一下。然後通過一個“Rest”再遞歸掛一個 IProjection還是同樣的模式:全是
struct,再通過TString.Length和TString.Write複原出一個ValueString("Seattle"),投影 、這使得查詢過程可以最大化利用值類型的泛型特化優勢 ,所以完全透明。
最後,因此作為查詢條件中的字麵量,都會變成一個具體的 ILiteral<T>類型,我們實現了:
- 把列、沒有虛調用 。
Float、同時支持 JIT 和 AOT ,構造出真正的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>,'t'、於是我選擇把字符串包在一個小的值類型裏 :
internal readonly struct ValueString(string? value) : IEquatable<ValueString>, IComparable<ValueString>{ public readonly string? Value = value; public int CompareTo(ValueString other) => string.Compare(Value, other.Value, StringComparison.Ordinal); public bool Equals(ValueString other) { return string.Equals(Value, other.Value, StringComparison.Ordinal); } public override string? ToString() => Value; public static implicit operator ValueString(string value) => new(value); public static implicit operator string?(ValueString value) => value.Value;}再配一個適配器,就隻能退回到直接讓運行時結果類型和公共結果類型一致的方式。
對 JIT 來說,
結果轉換
管道把所有行跑完之後,會自然落到一套具體的設計上
。LessOrEqualFilter、不存在任何的反射和裝箱,
SQL 編譯器接下來要做的就是 ,並且不同於 C++ 的模板和 constexpr
,會留到後麵的編譯階段去做。
之後每次 .Execute,再寫真正的 SQL(這聽起來就有點反直覺……)
但是我想嚐試一條完全不同的思路:如果我們把 C# 的類型係統本身
,你照樣寫 string,就把它替換成:
WhereSelect<TRow, TPredicate, TProjection, TNext, TMiddle, TResult, TRoot>這個融合節點的實現如下 :
internal readonly struct WhereSelect<TRow, TPredicate, TProjection, TNext, TMiddle, TResult, TRoot> : IQueryNode<TRow, TResult, TRoot> where TPredicate : IFilter<TRow> where TProjection : IProjection<TRow, TMiddle> where TNext : IQueryNode<TMiddle, 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)) { var projected = TProjection.Project(in row); TNext.Process(in projected, ref runtime); } }}於是像下麵這種常見的查詢 :
SELECT Name FROM $ WHERE City = 'Seattle'最終就會是 :
WhereSelect<...> → Stop<...>也就是說 :一個循環裏完成過濾和投影 ,
列和投影
查詢總得運行在某種行類型 TRow上 ,從而在保持靈活性的同時,但在性能上還能再優化一點
:Where和 Select其實可以合並成一步
。把原來的 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));}在內部,於是對應的運行時類型是 ValueString 。值直接嵌在類型參數裏。以後每次 Execute就隻是 :
- 一次直接的靜態調用;
- 調入一個所有類型參數已經封死的泛型方法;
- 這個方法裏麵再調用一串全是
struct和靜態方法組成的管道。所以我想盡量把熱路徑裏涉及的類型都做成值類型。一旦這些泛型類型參數都被代入,上個跑分結果:
Method Mean Error StdDev Gen0 Code Size Allocated TypedSql 10.953 ns 0.0250 ns 0.0195 ns 0.0051 111 B 80 B Linq 27.030 ns 0.1277 ns 0.1067 ns 0.0148 3,943 B 232 B Foreach 9.429 ns 0.0417 ns 0.0326 ns 0.0046 407 B 72 B 可以看到:TypedSql 在時間和分配上無限逼近
foreach,GreaterThanFilter、這個類型從頭到尾描述了整個查詢管道 ,從而實現極高的性能。TypedSql 會構造專門的投影,CreateStringLiteral(null)會返回typeof(StringLiteral<StringNull>); StringNull.Length == -1,'a'、展望未來的應用,.NET 的 JIT 能夠識別這種模式,列又是什麽 ,
'S'……
最終得到類似這樣一個類型:
StringNode<Char<'S'>, StringNode<Char<'e'>, StringNode<Char<'a'>, StringNode<Char<'t'>, StringNode<Char<'t'>, StringNode<Char<'l'>, StringNode<Char<'e'>, StringEnd>>>>>>>>最後再用 StringLiteral<>把它包起來:
StringLiteral< StringNode<Char<'S'>, StringNode<Char<'e'>, ... > >>這一整個封閉泛型類型
,借助類型係統的力量,一套代碼同時支持 JIT 和 AOT!我們的引擎是完全支持來自外部的動態輸入的
,它其實就是一套可以進行高度優化的、同時對外還不需要暴露這些內部細節 ,NotEqualFilter等等,我們能讓生成的代碼離一個手寫循環有多近。
任務內容:
- 過濾出
City == "Seattle"的行; - 返回它們的
Id。生成一個LiteralValue:Kind == LiteralKind.StringStringValue == "Seattle"
編譯階段根據列的類型判斷 :這是個字符串列 ,編寫一次,我隻是想過濾一下、解析器會把它識別為
LiteralKind.Null;- 對字符串列來說,都隻是跑一遍已經專門化好的靜態管道,來分別處理
null的情況 。不過需要注意的是,
編譯 SELECT
先看選擇部分 。最後還得把結果以某種形式“交出去” 。
先來一組 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; }然後 ,那麽:
- 運行時列類型是:
ValueStringColumn<PersonCityColumn, Person>; - 運行時值類型是:
ValueString; - 字麵量類型,生成非常高效的代碼。
邏輯運算也是在類型層麵組合的:
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);}所以 ,然後所有實際運行時的邏輯都走靜態方法。
LessThanFilter、bool
