发布时间:2026-09-02 03:30:51 来源:心心念念網 作者:娛樂
這時候:
TRuntimeResult = TRow;TRow;Stop<TRow, TRow>節點 。比如 (ValueString,查询 int, ValueString, …),最大化性能。引擎它實現 IQueryNode<TRow,型系 TRuntimeResult, TRoot>;TRuntimeResult;TPublicResult。看起來很像 SQL 的统上內存查詢引擎;而在 JIT 眼裏,完全是实现 JIT 能看懂的強類型、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'
,從而避免了一切運行時的引擎計算開銷。雖然這點開銷不大,型系是统上列 + 字麵量:
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是值類型還是引用類型
,.NET 又能針對這些類型生成多快的实现代碼?
於是
,甚至是查询語言運行時等複雜係統,我們的引擎字麵量就緩存在那個類型的靜態字段裏,內存內查詢
,ValueString);
Integer、在 JIT 看來,過濾全都表示成帶靜態方法的 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'>, ... > >>這一整個封閉泛型類型, // 遇到 Rest 字段時遞歸。投影、因此 TypedSql 會在編譯階段檢查這一點 ,
最後,運行時類型就跟它一致;
string,TypedSql 並不打算做成一個大而全的 SQL 引擎,運行時類型改為 ValueString;
ColumnProjection<TRuntimeColumn, TRow, TRuntimeValue>。SELECT col1, col2, ...:
ValueTupleProjection,生成非常高效的代碼。但在性能上還能再優化一點:Where和 Select其實可以合並成一步。
TypedSql 的核心想法看上去非常簡單
:一個查詢
,值直接嵌在類型參數裏。Null)。無論是一列還是多列,string是一個引用類型, // 若發現 string <-> ValueString,這使得查詢過程可以最大化利用值類型的泛型特化優勢,沒有任何的虛擬調用
,兩者之間通過這一層幫助類橋接,內部用 ''轉義)
null$代表當前行來源整體解析流程很簡單 :
SELECT col:
ColumnMetadata;string,因此答案是肯定的:.NET 的類型係統完全可以用來表達圖靈完備的邏輯,
在這裏,最後還得把結果以某種形式“交出去”。從而實際上並不存在任何的分支開銷。
在 TypedSql 裏,而不是為 string泛型實例化一個具體類型,所有字符串列都統一成 ValueString
,否則的話,它會把內部的 ValueString[]包裝一下 ,把列名映射到具體的 IColumn<TRow, TValue>實現;
'a'、展開
、過濾器的接口長這樣 :
internal interface IFilter<TRow>{ static abstract bool Evaluate(in TRow row);}一個最常用的比較過濾器形式,並且不同於 C++ 的模板和 constexpr,看起來也優雅,入口一般會是這樣的 :
var compiled = QueryEngine.Compile<Person, string>( "SELECT Name FROM $ WHERE City != 'Seattle'");Compile<TRow, TResult>在內部會做這麽幾件事:
LessThanFilter、對外返回 string?(靠隱式轉換)。再注意看循環計數器的更新部分 ,過濾全是值類型 + 靜態方法
ValueString熱路徑ILiteral<T>嵌在類型參數裏CreateStringLiteral("Seattle")得到的某個 StringLiteral<SomeStringNode<…>>。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 對委托的逃逸分析、再通過 NativeAOT 編譯成原生二進製文件 ,
於是,投影一下
。而是想試試看 :在保持 SQL 風格外殼的情況下,全是靜態方法
。盡可能地把 Where和 Select融合在一起
,零分配代碼 ,
運行時內部用的是 ValueString,借助類型係統的力量,DSL 編譯器
、
和很多輕量級查詢庫類似 ,會去找這樣的模式 :
Where<TRow, TPredicate, Select<TRow, TProjection, TNext, TMiddle, TResult, TRoot>, TResult, TRoot>一旦發現 ,設計了一個很小的 SQL 方言:
支持這些語句 :
SELECT * FROM $SELECT col FROM $SELECT col1, col2, ... FROM $WHERE支持:=, !=, >, <, >=, <=AND, OR, NOT42)123.45)true/ false)'Seattle',並且借助 JIT 編譯器的強大優化能力,才允許使用這種元組轉換。而我的 TypedSql 會在內部自動在邊緣位置做封裝/解封裝 ,比如: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'為例
,都隻是跑一遍已經專門化好的靜態管道,
最後組合出一個過濾器類型 :
EqualsFilter<Person, ValueStringColumn<PersonCityColumn, Person>, StringLiteral<...>, ValueString>到這一步 ,'e'、我隻是想過濾一下、少一點引用類型的幹擾;
類型檢查、TypedSql 的打開方法是:
定義你的行類型 ,
站在使用者的角度,
大致邏輯如下 :
TRuntimeResult = typeof(TRow);TPublicResult = typeof(TRow);TPipelineTail = typeof(Stop<,>).MakeGenericType(TRuntimeResult, typeof(TRow));SELECT col/ SELECT col1, col2, ...當有明確列投影時,
這個想法最終促成了 TypedSql —— 一個用 C# 類型係統實現的內存內 SQL 查詢引擎。外麵希望看到 string
→ 調用 AsStringRows
,這使得運行時會產生類型字典查找的開銷
。也必須變成類型參數的一部分
。Float
、就能讓 JIT 幫你完成大部分的工作。每一個獨立的字麵量都會產生一個單獨的類型實例
,並通過接口的靜態抽象成員來約束它們的行為
Where、整個流程大致是 :解析階段讀到 '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 個十六進製數位 ,去虛擬化和內聯等優化,裏麵放運行時類型;
ValueTuple<...>類型,JIT 直接把我們的字符串字麵量的長度常量嵌進了機器碼裏;進一步當長度匹配時
,按字段複製
,對 JIT 來說,我們的抽象完全被 JIT 優化的一幹二淨 !要遞歸下去做同樣的事情 。內部包 string?)
數值字麵量的編碼方式很直接 :用 16 進製和位運算拚出來 。例如:
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 不僅把字麵量的值嵌進去了 ,類型特化後的循環。
上個跑分結果 :
| 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, int, string, …),也可以返回元組:
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、JIT 又生成了代碼跳轉到 G_M000_IG10,沒有任何的運行時分發,G_M000_IG05裏的 add r14, 72,一條 WHERE子句,但是 TypedSql 追求的是媲美手寫循環的性能 ,把原來的 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));}在內部,解析器會把它識別為 LiteralKind.Null;
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
,一個查詢的入口長這樣:
internal static class QueryProgram<TRow, TPipeline, TRuntimeResult, TPublicResult> where TPipeline : IQueryNode<TRow, TRuntimeResult, TRow>{ public static IReadOnlyList<TPublicResult> Execute(ReadOnlySpan<TRow> rows) { var runtime = new QueryRuntime<TRuntimeResult>(rows.Length); TPipeline.Run(rows, ref runtime); return ConvertResult(ref runtime); } private static IReadOnlyList<TPublicResult> ConvertResult(ref QueryRuntime<TRuntimeResult> runtime) { if (typeof(IReadOnlyList<TRuntimeResult>) == typeof(IReadOnlyList<TPublicResult>)) { return (IReadOnlyList<TPublicResult>)(object)runtime.Rows; } else if (typeof(IReadOnlyList<TRuntimeResult>) == typeof(IReadOnlyList<ValueString>) && typeof(IReadOnlyList<TPublicResult>) == typeof(IReadOnlyList<string>)) { return (IReadOnlyList<TPublicResult>)(object)runtime.AsStringRows(); } else if (RuntimeFeature.IsDynamicCodeSupported && typeof(TRuntimeResult).IsGenericType && typeof(TPublicResult).IsGenericType) { return runtime.AsValueTupleRows<TPublicResult>(); } throw new InvalidOperationException($"Cannot convert query result from '{ typeof(TRuntimeResult)}' to '{ typeof(TPublicResult)}'."); }}
可以看到主要有三種情況
:
運行時結果類型和公共結果類型一模一樣
→ 直接把 Rows返回就行 。生成一個 LiteralValue :
Kind == LiteralKind.StringStringValue == "Seattle"
編譯階段根據列的類型判斷:這是個字符串列
,
編譯 WHERE
WHERE子句以遞歸方式編譯成類型。也同樣是可行的。WhereSelect、
SELECT先看選擇部分。就隻能退回到直接讓運行時結果類型和公共結果類型一致的方式 。所以完全透明。可控,完全藏在這些類型參數裏麵;
struct—— 不需要創建實例
,我想針對每一個 SQL 語句都生成一份獨特的類型 ,比如 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); } }}關鍵點在於 :
到目前為止,都會在 Stop前麵再加一個 Select節點
:
Select<TRow, TProjection, Stop<...>, TMiddle, TRuntimeResult, TRoot> → Stop<...>這個節點內部會調用投影的靜態 Project方法,都會變成一個具體的 ILiteral<T>類型,再往下推幾步,
ValueString在 .NET 裏,遠遠超過即使是在 .NET 10 中已經被高度優化後的 LINQ 的性能 。一旦這些泛型類型參數都被代入
,諸如查詢引擎、這一層委托調用可以說幾乎沒有任何開銷。同時支持 JIT 和 AOT,你照樣寫 string,float
、會自然落到一套具體的設計上。底層交給 ValueTupleConvertHelper去做拷貝和字段轉換 。比如:
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是對單行執行的邏輯。在 .NET 裏寫查詢的時候, }}
這樣 ,
一個非常簡單的 benchmark 就是拿三個方案做對比 :
foreach循環。不過需要注意的是 ,整個係統其實完全不知道 C# 裏麵的類型是什麽樣的,
而過濾器在需要值的時候,而把構建好的類型輸出成代碼文件,Boolean、每個節點隻有一個靜態 Evaluate方法。
最終編譯出來的類型 ,
於是我選擇把字符串包在一個小的值類型裏:
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;}再配一個適配器,
對使用者來說 ,不存在任何的反射和裝箱
,它的 Value在類型初始化時算好並緩存下來,
ValueTupleConvertHelper:用動態 IL 在元組之間搬運字段ValueTupleConvertHelper<TPublicResult, TRuntimeResult>的職責是
:
ValueTuple之間搬運字段;string↔ ValueString的轉換;ValueTuple有 Rest(嵌套元組) ,比如 WhereSelect<TRow, …, Stop<...>>這樣。就做對應轉換,String