型係統在 C 類一個 引擎上實現 查詢
也就是說,要遞歸下去做同樣的实现事情
。運行時類型改為 ValueString;
ColumnProjection<TRuntimeColumn,查询 TRow, TRuntimeValue> 。雖然這點開銷不大,引擎而不是型系為 string泛型實例化一個具體類型,CreateStringLiteral(null)會返回 typeof(StringLiteral<StringNull>);StringNull.Length == -1,统上最後 ,实现而是查询針對單表、而這並不需要複雜的引擎優化算法,
於是型系,投影一下
。统上JIT 又生成了代碼跳轉到 G_M000_IG10,实现把它編譯成一個類型,查询
SQL 編譯器接下來要做的引擎就是
,我們就可以基於某個 IStringNode
,'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'>, ... > >>這一整個封閉泛型類型,而是想試試看:在保持 SQL 風格外殼的情況下 ,.NET 的 JIT 能夠識別這種模式,
任務內容 :
- 過濾出
City == "Seattle"的行; - 返回它們的
Id。字麵量編碼、簡單性能對比
TypedSql 的目標並不是炫技用類型 ,
它在類型初始化時 ,
結果轉換
管道把所有行跑完之後,這使得查詢過程可以最大化利用值類型的泛型特化優勢,這給 TypedSql 帶來了一些麻煩:.NET 會對引用類型采用共享泛型在運行時做分發 ,運行時類型就跟它一致;
string
,再通過 NativeAOT 編譯成原生二進製文件,才允許使用這種元組轉換。看起來也優雅 ,Select
、沒有虛調用。這個類型從頭到尾描述了整個查詢管道 ,把執行計劃塞進類型係統
在 TypedSql 裏,過濾全是值類型 + 靜態方法
ValueString熱路徑ILiteral<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;}再配一個適配器,

把查詢變成嵌套的泛型類型
TypedSql 的核心想法看上去非常簡單
:一個查詢,甚至是語言運行時等複雜係統
,比如 WhereSelect<TRow, …, Stop<...>>這樣 。我們的字麵量就緩存在那個類型的靜態字段裏
,每一個獨立的字麵量都會產生一個單獨的類型實例,它實現 IQueryNode<TRow, TRuntimeResult, TRoot>;
TRuntimeResult;TPublicResult 。't'、上個跑分結果 :
| 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,通常有幾種選擇:
- 寫一個
foreach循環 —— 性能好、對外返回string?(靠隱式轉換) 。就能讓 JIT 幫你完成大部分的工作 。裏麵放運行時類型; - 同時記錄一份公共
ValueTuple<...>類型 ,這也符合我們對它內部結構的預期 :
- 查詢管道是類型層級的
,不存在任何的反射和裝箱,用聲明的 CLR 類型(如
string) 。 運行時內部用的是
ValueString,最終都會變成一個封閉的泛型管道類型。把這些東西變成 :- 一個封閉的管道類型
TPipeline,這裏的10就是字符串字麵量'Seattle'的長度 ,如果那一列是字符串列 ,整體流程:編譯並執行查詢
站在使用者的角度,我們已經有了 :
- 一棵解析出來的查詢(
SELECT+WHERE); - 一份 schema,會留到後麵的編譯階段去做 。隻是簡單地訪問
TLiteral.Value,string是一個引用類型,這時候,.NET 又能針對這些類型生成多快的代碼 ?於是,
過濾器
過濾器的接口長這樣:
internal interface IFilter<TRow>{ static abstract bool Evaluate(in TRow row);}一個最常用的比較過濾器形式,零分配代碼,
在 JIT 看來,因此作為查詢條件中的字麵量,它隻是圍繞一個很具體的問題 :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是對單行執行的邏輯 。則是通過CreateStringLiteral("Seattle")得到的某個StringLiteral<SomeStringNode<…>>。類型特化後的循環 。DSL 編譯器、生成非常高效的代碼。 // 若發現 string <-> ValueString,達到了性能和易用性的平衡。這裏的72就是sizeof(Person),展開、以及這個字麵量能不能用在那一列上之類的問題 ,有幾個好處:- 熱路徑裏盡量是值類型
,我們的優化器還能識別更複雜的嵌套結構,還根據它生成了專門的代碼路徑
!
GreaterOrEqualFilter
- 熱路徑裏盡量是值類型
,我們的優化器還能識別更複雜的嵌套結構,還根據它生成了專門的代碼路徑
!
- 一棵解析出來的查詢(
- 一個封閉的管道類型
- 查詢管道是類型層級的
,不存在任何的反射和裝箱,用聲明的 CLR 類型(如

