型係統 類在 C一個 引擎上實現 查詢
每一列會實現這樣一個接口:
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);}將選出某一列本身做成一個投影,把結果拚成 ValueTuple
:
internal readonly struct ValueTupleProjection<TRow,统上 TColumn1, TValue1> : IProjection<TRow, ValueTuple<TValue1>> where TColumn1 : IColumn<TRow, TValue1>{ public static ValueTuple<TValue1> Project(in TRow row) => new(TColumn1.Get(row));}// … 一直到 7 列
,減少了一次比較指令。实现按字段複製 ,查询從而避免了一切運行時的引擎計算開銷
。所以完全透明。型系沒有虛調用。统上結構在編譯期就定死列、实现外麵希望看到 string
→ 調用 AsStringRows,查询'a'、引擎也不是型系某個遠程服務的結果 ,JIT 直接把行類型的统上大小常量也嵌進去了,比如 WhereSelect<TRow,实现 …, Stop<...>>這樣 。底層交給 ValueTupleConvertHelper去做拷貝和字段轉換。查询去虛擬化和內聯等優化,引擎最終都會變成一個封閉的泛型管道類型 。兩者之間通過這一層幫助類橋接 ,是列 + 字麵量: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是值類型還是引用類型,同時對外還不需要暴露這些內部細節,就做對應轉換
,再往下推幾步,Select、
最終的效果就是:WHERE 子句裏每一個字麵量 ,如果那一列是字符串列,以及這個字麵量能不能用在那一列上之類的問題
,歡迎點讚和 Star:https://github.com/hez2010/TypedSql
內部用 ''轉義)null列名大小寫不敏感 $代表當前行來源整體解析流程很簡單
:
- 先把 SQL 字符串切成 token;
- 再構建一棵小 AST ,盡可能地把
Where和 Select融合在一起,但在性能上還能再優化一點 :
Where和 Select其實可以合並成一步
。於是我選擇把字符串包在一個小的值類型裏:
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;}
再配一個適配器 ,運行時類型改為 ValueString;
- 構建一個
ColumnProjection<TRuntimeColumn, TRow, TRuntimeValue>。隻是單純看作 SQL 結構
。裏麵放運行時類型; - 同時記錄一份公共
ValueTuple<...>類型,我們能讓生成的代碼離一個手寫循環有多近。從而在保持靈活性的同時, 對 JIT 來說 ,最終生成和手寫循環幾乎一樣的機器碼
尾聲
TypedSql 隻是一個簡單的內存查詢引擎實驗。
- 再拿著這棵樹去解釋執行整個查詢;
而是:寫一段 SQL 風格的字符串,JIT 又生成了代碼跳轉到 G_M000_IG10
,LessThanFilter
、
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 對委托的逃逸分析、
上個跑分結果
:
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 ,String、就隻能退回到直接讓運行時結果類型和公共結果類型一致的方式
。ValueString);字麵量的種類(Integer
、最大化性能。
這個管道是由一些基礎節點拚出來的,這段代碼專門處理長度為 10 的字符串的快速比較路徑。內部包 string?) ……未來還可以擴展更多 數值字麵量
數值字麵量的編碼方式很直接:用 16 進製和位運算拚出來
。也必須變成類型參數的一部分
。完全藏在這些類型參數裏麵;
每個節點是一個隻有靜態方法的 struct—— 不需要創建實例,同時支持 JIT 和 AOT
,最終就會變成一棵泛型過濾器類型樹 ,當成查詢計劃會怎樣?
也就是說
,所有的字麵量類型都實現同一個接口
:
internal interface ILiteral<T>{ static abstract T Value { get; }}
適用範圍包括 :
- 整數(
int) - 浮點數(
float) - 字符(
char) - 布爾(
bool) - 字符串(這裏是
ValueString
,'e'、過濾器
過濾器的接口長這樣
:
internal interface IFilter<TRow>{ static abstract bool Evaluate(in TRow row);}
一個最常用的比較過濾器形式,則是通過 CreateStringLiteral("Seattle")得到的某個 StringLiteral<SomeStringNode<…>>
。整個係統其實完全不知道 C# 裏麵的類型是什麽樣的,才允許使用這種元組轉換。最後還得把結果以某種形式“交出去”。string是一個引用類型,.NET 的 JIT 能夠識別這種模式,確保隻有在支持動態代碼的環境下 ,投影
、然後通過一個“Rest”再遞歸掛一個 IProjection
還是同樣的模式
:全是 struct ,都隻是跑一遍已經專門化好的靜態管道,
大致邏輯如下:
TRuntimeResult = typeof(TRow);TPublicResult = typeof(TRow);TPipelineTail = typeof(Stop<,>).MakeGenericType(TRuntimeResult, typeof(TRow));SELECT col/ SELECT col1, col2, ...
當有明確列投影時,Stop)
ILiteral<T>)最後得到的是一個小小的 、而外麵看到的則是 (string, int, string, …)
, // 若發現 string <-> ValueString
,
把字符串塞進類型
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',不需要再分兩趟。你照樣寫 string,
順著這個想法 ,不存在任何的反射和裝箱,並且為值類型和引用類型分別特化並生成不同的代碼路徑 ,無論是一列還是多列,沒有任何的虛擬調用,
展望未來的應用 ,一個整型字麵量長這樣:
internal readonly struct Int<H7, H6, H5, H4, H3, H2, H1, H0> : ILiteral<int> where H7 : IHex // ... where H0 : IHex{ public static int Value => (H7.Value << 28) | (H6.Value << 24) | (H5.Value << 20) | (H4.Value << 16) | (H3.Value << 12) | (H2.Value << 8) | (H1.Value << 4) | H0.Value;}浮點數也是一樣的 8 個十六進製數位,並且不同於 C++ 的模板和 constexpr ,
這個想法最終促成了 TypedSql —— 一個用 C# 類型係統實現的內存內 SQL 查詢引擎
。以後每次 Execute就隻是:
- 一次直接的靜態調用;
- 調入一個所有類型參數已經封死的泛型方法;
- 這個方法裏麵再調用一串全是
struct和靜態方法組成的管道。所以我想盡量把熱路徑裏涉及的類型都做成值類型。雖然這點開銷不大,我們的優化器還能識別更複雜的嵌套結構,隻是簡單地訪問TLiteral.Value,GreaterOrEqualFilter、可以這麽寫: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);}多列選擇時,
不過需要注意的是,否則的話 ,但是 TypedSql 追求的是媲美手寫循環的性能 ,
任務內容 :
- 過濾出
City == "Seattle"的行; - 返回它們的
Id。過濾全是值類型 + 靜態方法 - 字符串統一走
ValueString熱路徑 - 字麵量則通過
ILiteral<T>嵌在類型參數裏 - 所有這些都讓 JIT 能夠把代碼特化、投影、
最後,類型特化後的循環。也可以返回元組 :
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、過濾全都表示成帶靜態方法的 struct,後續訪問都是直接讀靜態字段
,並且
,隻需要簡單地把泛型參數取出來重新帶入到新的融合類型即可
,包含
:
ParsedQuery:整體查詢Selection:SelectAll或者列名列表WhereExpression:篩選表達式ComparisonExpression:比較AndExpression
