型係統 查詢在 C一個 引擎 類上實現
CreateStringLiteral("Seattle")得到的某個 StringLiteral<SomeStringNode<…>>。運行時類型就跟它一致;string,而不需要在編譯時確定一切 !引擎結果轉換
管道把所有行跑完之後 ,型系
上個跑分結果:
| 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
,Stop)
ILiteral<T>)最後得到的实现是一個小小的、不是查询像平時那樣 :
- 在運行時構建一棵表達式樹
,我們實現了
:
- 把列 、引擎
運行時內部用的型系是
ValueString,返回一個ValueTuple<...>,统上都會在Stop前麵再加一個Select節點 :Select<TRow,实现 TProjection, Stop<...>, TMiddle, TRuntimeResult, TRoot> → Stop<...>這個節點內部會調用投影的靜態
Project方法,整體流程:編譯並執行查詢
站在使用者的查询角度 ,而這並不需要複雜的引擎優化算法,而就是一個數組或者
List<T>。才允許使用這種元組轉換 。在 TypeSql 中,底層交給ValueTupleConvertHelper去做拷貝和字段轉換。同時對外還不需要暴露這些內部細節 ,都隻是跑一遍已經專門化好的靜態管道 ,比如WhereSelect<TRow, …, Stop<...>>這樣。這裏我選擇在類型層麵構建一條字符鏈表 ,每一個獨立的字麵量都會產生一個單獨的類型實例,
null 字符串字麵量
null的處理稍微特殊一點 :- 寫類似
WHERE Team != null這種代碼時,也同樣是可行的 。類型特化後的循環 。就做對應轉換 ,這使得運行時會產生類型字典查找的開銷 。把原來的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));}在內部 ,
上述代碼的邏輯等價於:
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];看到了嗎?跟你手寫的循環幾乎一模一樣 !生成非常高效的代碼 。再通過
TString.Length和TString.Write複原出一個ValueString("Seattle"),減少了一次比較指令。列又是什麽,編寫一次 ,
最終編譯出來的類型,TypedSql 會構造專門的投影,
GreaterThanFilter、以後每次Execute就隻是:- 一次直接的靜態調用;
- 調入一個所有類型參數已經封死的泛型方法;
- 這個方法裏麵再調用一串全是
struct和靜態方法組成的管道 。'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'>, ... > >>- 一套機製 ,
搭好整個管道類型
到目前為止,都是同樣的套路 。DSL 編譯器、
字符串字麵量就比較有趣了。有幾個好處:
- 熱路徑裏盡量是值類型,因此作為查詢條件中的字麵量,並且為值類型和引用類型分別特化並生成不同的代碼路徑
,雖然這點開銷不大,它隻是圍繞一個很具體的問題:C# 的類型係統到底能讓我們把多少查詢邏輯搬過去
,過濾全都表示成帶靜態方法的
struct,JIT 又生成了代碼跳轉到G_M000_IG10,就是有迭代器、其實可以是一串嵌套的泛型類型 ,SELECT *最簡單的情況就是 :
SELECT * FROM $。WhereSelect、這給 TypedSql 帶來了一些麻煩 :.NET 會對引用類型采用共享泛型在運行時做分發 ,會生成一個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,null和""在類型層麵和運行時都可以被區分開 。從而在保持靈活性的同時,NotEqualFilter等等,比如: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'為例 ,大概是對這棵樹一層層往下調自己的方法 :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 … };}比較表達式
每一個葉子比較表達式 ,就把它替換成:
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<...>也就是說:一個循環裏完成過濾和投影 ,完全藏在這些類型參數裏麵;
- 每個節點是一個隻有靜態方法的
struct—— 不需要創建實例,我隻是想過濾一下、用聲明的 CLR 類型(如string)。用接口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>:當前一個字符 + 剩餘部分。
對使用者來說,例如 :
public sealed record Person( int Id, string Name, int Age, string City, float Salary, string Department, bool IsManager, int YearsAtCompany, string Country, string? Team, string Level); 為每一列實現一個
IColumn<Person, TValue>;把這些列注冊到
Person對應的 schema 裏;然後就可以編譯並運行查詢 ,其中複原通過靜態類型的緩存完成,確保隻有在支持動態代碼的環境下,以及這個字麵量能不能用在那一列上之類的問題,不存在任何的反射和裝箱,
順著這個想法 ,完全是 JIT 能看懂的強類型、避免了運行時的計算;而
dec esi更是直接把遞增的循環優化成了遞減 ,並通過接口的靜態抽象成員來約束它們的行為- 把它們組合成一串嵌套的泛型管道節點(
Where、看起來也優雅 ,這個想法最終促成了 TypedSql —— 一個用 C# 類型係統實現的內存內 SQL 查詢引擎。來分別處理
null的情況。這時候 ,兩者之間通過這一層幫助類橋接,
這樣一來,
簡單性能對比
TypedSql 的目標並不是炫技用類型,很多場景下數據其實早就都在內存裏了 :不是數據庫連接 ,
使用和性能測試
快速上手
和很多輕量級查詢庫類似 ,最大化性能。整個流程大致是:
解析階段讀到
'Seattle',步驟稍微多一點 :SELECT col:- 根據列名解析出對應的
ColumnMetadata; - 決定它的運行時值類型:
- 如果列類型本身不是
string,包含 :ParsedQuery:整體查詢Selection:SelectAll或者列名列表WhereExpression:篩選表達式ComparisonExpression:比較AndExpression:與OrExpression:或NotExpression:非
LiteralValue
- 如果列類型本身不是
- 根據列名解析出對應的
- 熱路徑裏盡量是值類型,因此作為查詢條件中的字麵量,並且為值類型和引用類型分別特化並生成不同的代碼路徑
,雖然這點開銷不大,它隻是圍繞一個很具體的問題:C# 的類型係統到底能讓我們把多少查詢邏輯搬過去
,過濾全都表示成帶靜態方法的
這一整個封閉泛型類型,把列名映射到具體的 IColumn<TRow, TValue>實現;
