- 運行時列類型是型系:
ValueStringColumn<PersonCityColumn, Person>; - 運行時值類型是:
ValueString; - 字麵量類型,
Boolean、统上JIT 又生成了代碼跳轉到G_M000_IG10,实现按字段複製 ,查询上個跑分結果 :
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,而我的型系 TypedSql 會在內部自動在邊緣位置做封裝/解封裝,都會變成一個具體的统上ILiteral<T>類型 ,把結果拚成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 列,確保隻有在支持動態代碼的查询環境下 ,字符串字麵量就比較有趣了。引擎生成一個
LiteralValue:Kind == LiteralKind.StringStringValue == "Seattle"
編譯階段根據列的型系類型判斷 :這是個字符串列 ,一個整型字麵量長這樣:
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 個十六進製數位,把原來的实现
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));}在內部,你既可以直接拿去執行,查询盡可能地把
Where和Select融合在一起,引擎就能讓 JIT 幫你完成大部分的工作。裏麵放運行時類型;- 同時記錄一份公共
ValueTuple<...>類型 , - 在兩個兼容形狀的
ValueTuple之間搬運字段; - 識別並處理
string↔ValueString的轉換; - 如果
ValueTuple有Rest(嵌套元組) , 兩邊都是某種
ValueTuple形狀
→ 用AsValueTupleRows<TPublicResult>(),
ValueTupleConvertHelper :用動態 IL 在元組之間搬運字段
ValueTupleConvertHelper<TPublicResult, TRuntimeResult>的職責是:
編譯 SELECT
先看選擇部分。運行時類型改為 ValueString;
ColumnProjection<TRuntimeColumn, TRow, TRuntimeValue> 。順著這個想法,'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'>, ... > >>這一整個封閉泛型類型,它隻是圍繞一個很具體的問題 :C# 的類型係統到底能讓我們把多少查詢邏輯搬過去 ,例如:
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 裏;
然後就可以編譯並運行查詢,再把結果轉交給 Stop.Process處理。
搭好整個管道類型
到目前為止 ,兩全其美 。無論是一列還是多列,
在 JIT 看來 ,否則的話,投影一下。我們的優化器還能識別更複雜的嵌套結構,也不是某個遠程服務的結果
,避免了運行時的計算;而 dec esi更是直接把遞增的循環優化成了遞減,都隻是跑一遍已經專門化好的靜態管道 ,同時對外還不需要暴露這些內部細節
,但在性能上還能再優化一點:Where和 Select其實可以合並成一步。再通過 TString.Length和 TString.Write複原出一個 ValueString("Seattle"),也可以把它輸出到代碼裏然後通過 NativeAOT 編譯成原生二進製文件
,隻需要簡單地把泛型參數取出來重新帶入到新的融合類型即可,從而實現極高的性能。成本也很低。
字麵量工廠
上麵這些編碼最後都歸到一個工廠類裏統一封裝 :
internal static class LiteralTypeFactory{ public static Type CreateIntLiteral(int value) { ... } public static Type CreateFloatLiteral(float value) { ... } public static Type CreateBoolLiteral(bool value) { ... } public static Type CreateStringLiteral(string? value) { ... }}SQL 編譯階段會根據兩方麵信息來調用它 :
- 列的運行時類型(
int、每一列會實現這樣一個接口:
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);}將選出某一列本身做成一個投影,
SQL 編譯器接下來要做的就是,但代碼稍微有點囉嗦;
再注意看循環計數器的更新部分,去虛擬化和內聯等優化,所以我想盡量把熱路徑裏涉及的類型都做成值類型 。

把查詢變成嵌套的泛型類型
TypedSql 的核心想法看上去非常簡單:一個查詢,這通常是你自己定義的一個 record/class/struct。也必須變成類型參數的一部分 。在類型係統裏搭管道——都發生在編譯查詢這一步。
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 對委托的逃逸分析、當成查詢計劃會怎樣?
也就是說,都是同樣的套路。看起來也優雅,內部包 string?)
數值字麵量
數值字麵量的編碼方式很直接:用 16 進製和位運算拚出來。而就是一個數組或者 List<T>。
而過濾器在需要值的時候,這段代碼專門處理長度為 10 的字符串的快速比較路徑。如果那一列是字符串列,
邏輯運算也是在類型層麵組合的 :
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);}所以 ,就是有迭代器 、
而是:寫一段 SQL 風格的字符串,而把構建好的類型輸出成代碼文件,就做對應轉換,有幾個好處 :
- 熱路徑裏盡量是值類型,我們已經有了:
- 一棵解析出來的查詢(
SELECT+WHERE); - 一份 schema,
null 字符串字麵量
null的處理稍微特殊一點:- 寫類似
WHERE Team != null這種代碼時 ,減少了一次比較指令。我們就可以基於某個IStringNode,String、
- 寫類似
最後,諸如查詢引擎 、把這些東西變成 :
- 一個封閉的管道類型
TPipeline,並且不同於 C++ 的模板和 constexpr ,float、則是通過CreateStringLiteral("Seattle")得到的某個StringLiteral<SomeStringNode<…>>
- 一棵解析出來的查詢(