啰嘿嗦轮嘿阵 發表於 2025-11-24 00:54:00

TypedSql:在 C# 类型系统上实现一个 SQL 查询引擎

<h2 id="前言">前言</h2>
<p>在 .NET 里写查询的时候,很多场景下数据其实早就都在内存里了:不是数据库连接,也不是某个远程服务的结果,而就是一个数组或者 <code>List&lt;T&gt;</code>。我只是想过滤一下、投影一下。这时候,通常有几种选择:</p>
<ul>
<li>写一个 <code>foreach</code> 循环 —— 性能好、可控,但代码稍微有点啰嗦;</li>
<li>用 LINQ —— 写起来舒服,看起来也优雅,就是有迭代器、委托带来的那点开销;</li>
<li>要么干脆极端一点:把数据塞进数据库,再写真正的 SQL(这听起来就有点反直觉……)</li>
</ul>
<p>但是我想尝试一条完全不同的思路:<strong>如果我们把 C# 的类型系统本身,当成查询计划会怎样?</strong></p>
<p>也就是说,不是像平时那样:</p>
<ul>
<li>在运行时构建一棵表达式树,</li>
<li>再拿着这棵树去解释执行整个查询;</li>
</ul>
<p>而是:写一段 SQL 风格的字符串,把它编译成一个<strong>类型</strong>,这个类型从头到尾描述了整个查询管道,然后所有实际运行时的逻辑都走静态方法。</p>
<p>这个想法最终促成了 TypedSql —— 一个用 C# 类型系统实现的内存内 SQL 查询引擎。</p>
<p><img src="https://img2024.cnblogs.com/blog/1590449/202511/1590449-20251124114128790-2072136532.jpg" alt="1000016101" loading="lazy"></p>
<h2 id="把查询变成嵌套的泛型类型">把查询变成嵌套的泛型类型</h2>
<p>TypedSql 的核心想法看上去非常简单:一个查询,其实可以是一串嵌套的泛型类型,比如 <code>WhereSelect&lt;TRow, …, Stop&lt;...&gt;&gt;</code> 这样。</p>
<p>顺着这个想法,再往下推几步,会自然落到一套具体的设计上。</p>
<h3 id="把执行计划塞进类型系统">把执行计划塞进类型系统</h3>
<p>在 TypedSql 里,每一个编译好的查询,最终都会变成一个<strong>封闭的泛型管道类型</strong>。<br>
这个管道是由一些基础节点拼出来的,比如:</p>
<ul>
<li><code>Where&lt;TRow, TPredicate, TNext, TResult, TRoot&gt;</code></li>
<li><code>Select&lt;TRow, TProjection, TNext, TMiddle, TResult, TRoot&gt;</code></li>
<li><code>WhereSelect&lt;TRow, TPredicate, TProjection, TNext, TMiddle, TResult, TRoot&gt;</code></li>
<li><code>Stop&lt;TResult, TRoot&gt;</code></li>
</ul>
<p>每个节点都实现了同一个接口:</p>
<pre><code class="language-csharp">internal interface IQueryNode&lt;TRow, TResult, TRoot&gt;
{
    static abstract void Run(ReadOnlySpan&lt;TRow&gt; rows, scoped ref QueryRuntime&lt;TResult&gt; runtime);

    static abstract void Process(in TRow row, scoped ref QueryRuntime&lt;TResult&gt; runtime);
}
</code></pre>
<p>这里可以简单理解成:</p>
<ul>
<li><code>Run</code> 是外面那一圈大循环(整体遍历);</li>
<li><code>Process</code> 是对单行执行的逻辑。</li>
</ul>
<p>比如 <code>Where</code> 节点大概长这样:</p>
<pre><code class="language-csharp">internal readonly struct Where&lt;TRow, TPredicate, TNext, TResult, TRoot&gt;
    : IQueryNode&lt;TRow, TResult, TRoot&gt;
    where TPredicate : IFilter&lt;TRow&gt;
    where TNext : IQueryNode&lt;TRow, TResult, TRoot&gt;
{
    public static void Run(ReadOnlySpan&lt;TRow&gt; rows, scoped ref QueryRuntime&lt;TResult&gt; runtime)
    {
      for (var i = 0; i &lt; rows.Length; i++)
      {
            Process(in rows, ref runtime);
      }
    }

    public static void Process(in TRow row, scoped ref QueryRuntime&lt;TResult&gt; runtime)
    {
      if (TPredicate.Evaluate(in row))
      {
            TNext.Process(in row, ref runtime);
      }
    }
}
</code></pre>
<p>关键点在于:</p>
<ul>
<li>管道的<strong>形状</strong>,完全藏在这些类型参数里面;</li>
<li>每个节点是一个只有静态方法的 <code>struct</code> —— 不需要创建实例,没有虚调用。</li>
</ul>
<p>对 JIT 来说,一旦这些泛型类型参数都被代入,这就是一张普通的静态调用图而已。</p>
<h3 id="列和投影">列和投影</h3>
<p>查询总得运行在某种行类型 <code>TRow</code> 上,这通常是你自己定义的一个 record/class/struct。</p>
<p>每一列会实现这样一个接口:</p>
<pre><code class="language-csharp">internal interface IColumn&lt;TRow, TValue&gt;
{
    static abstract string Identifier { get; }

    static abstract TValue Get(in TRow row);
}
</code></pre>
<p>举个简单的例子:</p>
<pre><code class="language-csharp">internal readonly struct PersonNameColumn : IColumn&lt;Person, string&gt;
{
    public static string Identifier =&gt; "Name";

    public static string Get(in Person row) =&gt; row.Name;
}
</code></pre>
<p>而投影(<code>SELECT</code> 后面那部分)则实现:</p>
<pre><code class="language-csharp">internal interface IProjection&lt;TRow, TResult&gt;
{
    static abstract TResult Project(in TRow row);
}
</code></pre>
<p>将选出某一列本身做成一个投影,可以这么写:</p>
<pre><code class="language-csharp">internal readonly struct ColumnProjection&lt;TColumn, TRow, TValue&gt;
    : IProjection&lt;TRow, TValue&gt;
    where TColumn : IColumn&lt;TRow, TValue&gt;
{
    public static TValue Project(in TRow row) =&gt; TColumn.Get(row);
}
</code></pre>
<p>多列选择时,TypedSql 会构造专门的投影,把结果拼成 <code>ValueTuple</code>:</p>
<pre><code class="language-csharp">internal readonly struct ValueTupleProjection&lt;TRow, TColumn1, TValue1&gt;
    : IProjection&lt;TRow, ValueTuple&lt;TValue1&gt;&gt;
    where TColumn1 : IColumn&lt;TRow, TValue1&gt;
{
    public static ValueTuple&lt;TValue1&gt; Project(in TRow row)
      =&gt; new(TColumn1.Get(row));
}

// … 一直到 7 列,然后通过一个“Rest”再递归挂一个 IProjection
</code></pre>
<p>还是同样的模式:全是 <code>struct</code>,全是静态方法。</p>
<h3 id="过滤器">过滤器</h3>
<p>过滤器的接口长这样:</p>
<pre><code class="language-csharp">internal interface IFilter&lt;TRow&gt;
{
    static abstract bool Evaluate(in TRow row);
}
</code></pre>
<p>一个最常用的比较过滤器形式,是列 + 字面量:</p>
<pre><code class="language-csharp">internal readonly struct EqualsFilter&lt;TRow, TColumn, TLiteral, TValue&gt; : IFilter&lt;TRow&gt;
    where TColumn : IColumn&lt;TRow, TValue&gt;
    where TLiteral : ILiteral&lt;TValue&gt;
    where TValue : IEquatable&lt;TValue&gt;, IComparable&lt;TValue&gt;
{
   
    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 &amp;&amp; right is null) return true;
            if (left is null || right is null) return false;
            return left.Equals(right);
      }
    }
}
</code></pre>
<p>这里我们通过判断 <code>TValue</code> 是值类型还是引用类型,来分别处理 <code>null</code> 的情况。.NET 的 JIT 能够识别这种模式,并且为值类型和引用类型分别特化并生成不同的代码路径,从而实际上并不存在任何的分支开销。</p>
<p><code>GreaterThanFilter</code>、<code>LessThanFilter</code>、<code>GreaterOrEqualFilter</code>、<code>LessOrEqualFilter</code>、<code>NotEqualFilter</code> 等等,都是同样的套路。</p>
<p>逻辑运算也是在类型层面组合的:</p>
<pre><code class="language-csharp">internal readonly struct AndFilter&lt;TRow, TLeft, TRight&gt; : IFilter&lt;TRow&gt;
    where TLeft : IFilter&lt;TRow&gt;
    where TRight : IFilter&lt;TRow&gt;
{
    public static bool Evaluate(in TRow row)
      =&gt; TLeft.Evaluate(in row) &amp;&amp; TRight.Evaluate(in row);
}

internal readonly struct OrFilter&lt;TRow, TLeft, TRight&gt; : IFilter&lt;TRow&gt;
    where TLeft : IFilter&lt;TRow&gt;
    where TRight : IFilter&lt;TRow&gt;
{
    public static bool Evaluate(in TRow row)
      =&gt; TLeft.Evaluate(in row) || TRight.Evaluate(in row);
}

internal readonly struct NotFilter&lt;TRow, TPredicate&gt; : IFilter&lt;TRow&gt;
    where TPredicate : IFilter&lt;TRow&gt;
{
    public static bool Evaluate(in TRow row)
      =&gt; !TPredicate.Evaluate(in row);
}
</code></pre>
<p>所以,一条 <code>WHERE</code> 子句,最终就会变成一棵<strong>泛型过滤器类型树</strong>,每个节点只有一个静态 <code>Evaluate</code> 方法。</p>
<h3 id="值类型特化版字符串valuestring">值类型特化版字符串:<code>ValueString</code></h3>
<p>在 .NET 里,<code>string</code> 是一个<strong>引用类型</strong>,这给 TypedSql 带来了一些麻烦:.NET 会对引用类型采用共享泛型在运行时做分发,而不是为 <code>string</code> 泛型实例化一个具体类型,这使得运行时会产生类型字典查找的开销。虽然这点开销不大,但是 TypedSql 追求的是媲美手写循环的性能,所以我想尽量把热路径里涉及的类型都做成值类型。</p>
<p>于是我选择把字符串包在一个小的值类型里:</p>
<pre><code class="language-csharp">internal readonly struct ValueString(string? value) : IEquatable&lt;ValueString&gt;, IComparable&lt;ValueString&gt;
{
    public readonly string? Value = value;

    public int CompareTo(ValueString other)
      =&gt; string.Compare(Value, other.Value, StringComparison.Ordinal);

    public bool Equals(ValueString other)
    {
      return string.Equals(Value, other.Value, StringComparison.Ordinal);
    }

    public override string? ToString() =&gt; Value;

    public static implicit operator ValueString(string value) =&gt; new(value);

    public static implicit operator string?(ValueString value) =&gt; value.Value;
}
</code></pre>
<p>再配一个适配器,把原来的 <code>string</code> 列变成 <code>ValueString</code> 列:</p>
<pre><code class="language-csharp">internal readonly struct ValueStringColumn&lt;TColumn, TRow&gt;
    : IColumn&lt;TRow, ValueString&gt;
    where TColumn : IColumn&lt;TRow, string&gt;
{
    public static string Identifier =&gt; TColumn.Identifier;

    public static ValueString Get(in TRow row)
      =&gt; new(TColumn.Get(in row));
}
</code></pre>
<p>在内部,所有字符串列都统一成 <code>ValueString</code>,有几个好处:</p>
<ul>
<li>热路径里尽量是值类型,少一点引用类型的干扰;</li>
<li>避开了泛型共享带来的类型字典查找开销。</li>
</ul>
<p>对使用者来说,你照样写 <code>string</code>,而我的 TypedSql 会在内部自动在边缘位置做封装/解封装,所以完全透明。</p>
<h2 id="实现一个-sql-子集">实现一个 SQL 子集</h2>
<p>TypedSql 并不打算做成一个大而全的 SQL 引擎,而是针对<strong>单表、内存内</strong>查询,设计了一个很小的 SQL 方言:</p>
<p>支持这些语句:</p>
<ul>
<li><code>SELECT * FROM $</code></li>
<li><code>SELECT col FROM $</code></li>
<li><code>SELECT col1, col2, ... FROM $</code></li>
<li><code>WHERE</code> 支持:
<ul>
<li>比较:<code>=</code>, <code>!=</code>, <code>&gt;</code>, <code>&lt;</code>, <code>&gt;=</code>, <code>&lt;=</code></li>
<li>布尔:<code>AND</code>, <code>OR</code>, <code>NOT</code></li>
<li>括号</li>
</ul>
</li>
<li>字面量支持:
<ul>
<li>整数(如 <code>42</code>)</li>
<li>浮点数(如 <code>123.45</code>)</li>
<li>布尔(<code>true</code> / <code>false</code>)</li>
<li>单引号字符串(<code>'Seattle'</code>,内部用 <code>''</code> 转义)</li>
<li><code>null</code></li>
</ul>
</li>
<li>列名大小写不敏感</li>
<li><code>$</code> 代表当前行来源</li>
</ul>
<p>整体解析流程很简单:</p>
<ol>
<li>先把 SQL 字符串切成 token;</li>
<li>再构建一棵小 AST,包含:
<ul>
<li><code>ParsedQuery</code>:整体查询</li>
<li><code>Selection</code>:<code>SelectAll</code> 或者列名列表</li>
<li><code>WhereExpression</code>:筛选表达式
<ul>
<li><code>ComparisonExpression</code>:比较</li>
<li><code>AndExpression</code>:与</li>
<li><code>OrExpression</code>:或</li>
<li><code>NotExpression</code>:非</li>
</ul>
</li>
<li><code>LiteralValue</code>:字面量
<ul>
<li><code>LiteralKind.Integer</code> + <code>IntValue</code></li>
<li><code>LiteralKind.Float</code> + <code>FloatValue</code></li>
<li><code>LiteralKind.Boolean</code> + <code>BoolValue</code></li>
<li><code>LiteralKind.String</code> + <code>StringValue</code>(<code>string?</code>)</li>
<li><code>LiteralKind.Null</code></li>
</ul>
</li>
</ul>
</li>
</ol>
<p>在这个阶段,整个系统其实<strong>完全不知道 C# 里面的类型</strong>是什么样的,列又是什么,只是单纯看作 SQL 结构。</p>
<p>类型检查、以及这个字面量能不能用在那一列上之类的问题,会留到后面的编译阶段去做。</p>
<h2 id="把字面量变成类型--包括字符串">把字面量变成类型 —— 包括字符串</h2>
<p>在这里,我想针对每一个 SQL 语句都生成一份独特的类型,因此作为查询条件中的字面量,也必须变成类型参数的一部分。</p>
<p>于是,在 TypeSql 中,所有的字面量类型都实现同一个接口:</p>
<pre><code class="language-csharp">internal interface ILiteral&lt;T&gt;
{
    static abstract T Value { get; }
}
</code></pre>
<p>适用范围包括:</p>
<ul>
<li>整数(<code>int</code>)</li>
<li>浮点数(<code>float</code>)</li>
<li>字符(<code>char</code>)</li>
<li>布尔(<code>bool</code>)</li>
<li>字符串(这里是 <code>ValueString</code>,内部包 <code>string?</code>)</li>
<li>……未来还可以扩展更多</li>
</ul>
<h3 id="数值字面量">数值字面量</h3>
<p>数值字面量的编码方式很直接:<strong>用 16 进制和位运算拼出来</strong>。</p>
<p>先来一组 <code>IHex</code> 接口和 <code>Hex0</code>–<code>HexF</code> struct:</p>
<pre><code class="language-csharp">internal interface IHex { static abstract int Value { get; } }

internal readonly struct Hex0 : IHex { public static int Value =&gt; 0; }
// ...
internal readonly struct HexF : IHex { public static int Value =&gt; 15; }
</code></pre>
<p>然后,一个整型字面量长这样:</p>
<pre><code class="language-csharp">internal readonly struct Int&lt;H7, H6, H5, H4, H3, H2, H1, H0&gt; : ILiteral&lt;int&gt;
    where H7 : IHex
    // ...
    where H0 : IHex
{
    public static int Value
      =&gt; (H7.Value &lt;&lt; 28)
         | (H6.Value &lt;&lt; 24)
         | (H5.Value &lt;&lt; 20)
         | (H4.Value &lt;&lt; 16)
         | (H3.Value &lt;&lt; 12)
         | (H2.Value &lt;&lt;8)
         | (H1.Value &lt;&lt;4)
         |H0.Value;
}
</code></pre>
<p>浮点数也是一样的 8 个十六进制数位,只不过最后用 <code>Unsafe.BitCast&lt;int, float&gt;</code> 转回 <code>float</code>:</p>
<pre><code class="language-csharp">internal readonly struct Float&lt;H7, H6, H5, H4, H3, H2, H1, H0&gt; : ILiteral&lt;float&gt;
    where H7 : IHex
    // ...
{
    public static float Value
      =&gt; Unsafe.BitCast&lt;int, float&gt;(
               (H7.Value &lt;&lt; 28)
             | (H6.Value &lt;&lt; 24)
             | (H5.Value &lt;&lt; 20)
             | (H4.Value &lt;&lt; 16)
             | (H3.Value &lt;&lt; 12)
             | (H2.Value &lt;&lt;8)
             | (H1.Value &lt;&lt;4)
             |H0.Value);
}
</code></pre>
<p>字符则是 4 个十六进制数位:</p>
<pre><code class="language-csharp">internal readonly struct Char&lt;H3, H2, H1, H0&gt; : ILiteral&lt;char&gt;
    where H3 : IHex
    // ...
{
    public static char Value
      =&gt; (char)((H3.Value &lt;&lt; 12)
                | (H2.Value &lt;&lt;8)
                | (H1.Value &lt;&lt;4)
                |H0.Value);
}
</code></pre>
<h3 id="字符串字面量类型的链表">字符串字面量:类型的链表!</h3>
<p>字符串字面量就比较有趣了。</p>
<p>这里我选择在类型层面构建一条<strong>字符链表</strong>,用接口 <code>IStringNode</code> 来描述:</p>
<pre><code class="language-csharp">internal interface IStringNode
{
    static abstract int Length { get; }
    static abstract void Write(Span&lt;char&gt; destination, int index);
}
</code></pre>
<p>有三个实现:</p>
<ul>
<li><code>StringEnd</code>:字符串的结尾(长度 0);</li>
<li><code>StringNull</code>:表示 null 字符串(长度 -1);</li>
<li><code>StringNode&lt;TChar, TNext&gt;</code>:当前一个字符 + 剩余部分。</li>
</ul>
<pre><code class="language-csharp">internal readonly struct StringEnd : IStringNode
{
    public static int Length =&gt; 0;
    public static void Write(Span&lt;char&gt; destination, int index) { }
}

internal readonly struct StringNull : IStringNode
{
    public static int Length =&gt; -1;
    public static void Write(Span&lt;char&gt; destination, int index) { }
}

internal readonly struct StringNode&lt;TChar, TNext&gt; : IStringNode
    where TChar : ILiteral&lt;char&gt;
    where TNext : IStringNode
{
    public static int Length =&gt; 1 + TNext.Length;

    public static void Write(Span&lt;char&gt; destination, int index)
    {
      destination = TChar.Value;
      TNext.Write(destination, index + 1);
    }
}
</code></pre>
<p>有了这样的类型链表,我们就可以基于某个 <code>IStringNode</code>,构造出真正的 <code>ValueString</code>:</p>
<pre><code class="language-csharp">internal readonly struct StringLiteral&lt;TString&gt; : ILiteral&lt;ValueString&gt;
    where TString : IStringNode
{
    public static ValueString Value =&gt; Cache.Value;

    private static class Cache
    {
      public static readonly ValueString Value = Build();

      private static ValueString Build()
      {
            var length = TString.Length;
            if (length &lt; 0) return new ValueString(null);
            if (length == 0) return new ValueString(string.Empty);

            var chars = new char;
            TString.Write(chars.AsSpan(), 0);
            return new string(chars, 0, length);
      }
    }
}
</code></pre>
<p><code>StringLiteral&lt;TString&gt;</code> 就是一个 <code>ILiteral&lt;ValueString&gt;</code>,它的 <code>Value</code> 在类型初始化时算好并缓存下来,所以只需要计算一次,后续访问都是直接读静态字段,非常高效。</p>
<h4 id="把字符串塞进类型">把字符串塞进类型</h4>
<p><code>LiteralTypeFactory.CreateStringLiteral</code> 负责把字符串字面量转换成这样一个类型:</p>
<pre><code class="language-csharp">public static Type CreateStringLiteral(string? value)
{
    if (value is null)
    {
      return typeof(StringLiteral&lt;StringNull&gt;);
    }

    var type = typeof(StringEnd);
    for (var i = value.Length - 1; i &gt;= 0; i--)
    {
      var charType = CreateCharType(value); // Char&lt;...&gt;
      type = typeof(StringNode&lt;,&gt;).MakeGenericType(charType, type);
    }

    return typeof(StringLiteral&lt;&gt;).MakeGenericType(type);
}
</code></pre>
<p>比如我们有一个字面量 <code>'Seattle'</code>,整个流程大致是:</p>
<ol>
<li>
<p>解析阶段读到 <code>'Seattle'</code>,生成一个 <code>LiteralValue</code>:</p>
<ul>
<li><code>Kind == LiteralKind.String</code></li>
<li><code>StringValue == "Seattle"</code></li>
</ul>
</li>
<li>
<p>编译阶段根据列的类型判断:这是个字符串列,于是对应的运行时类型是 <code>ValueString</code>。</p>
</li>
<li>
<p>调用 <code>CreateStringLiteral("Seattle")</code>:</p>
<ul>
<li>
<p>初始 <code>type = typeof(StringEnd)</code>;</p>
</li>
<li>
<p>从右到左遍历每个字符:</p>
<ul>
<li><code>'e'</code> → 得到一个 <code>Char&lt;…&gt;</code> 类型(4 个十六进制数位对应 Unicode)
<ul>
<li><code>type = StringNode&lt;Char&lt;'e'&gt;, StringEnd&gt;</code></li>
</ul>
</li>
<li><code>'l'</code> 再往前:
<ul>
<li><code>type = StringNode&lt;Char&lt;'l'&gt;, StringNode&lt;Char&lt;'e'&gt;, StringEnd&gt;&gt;</code></li>
</ul>
</li>
<li>一直重复:<code>'t'</code>、<code>'t'</code>、<code>'a'</code>、<code>'e'</code>、<code>'S'</code>……</li>
</ul>
</li>
<li>
<p>最终得到类似这样一个类型:</p>
<pre><code class="language-csharp">StringNode&lt;Char&lt;'S'&gt;,
StringNode&lt;Char&lt;'e'&gt;,
    StringNode&lt;Char&lt;'a'&gt;,
      StringNode&lt;Char&lt;'t'&gt;,
      StringNode&lt;Char&lt;'t'&gt;,
          StringNode&lt;Char&lt;'l'&gt;,
            StringNode&lt;Char&lt;'e'&gt;, StringEnd&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;
</code></pre>
</li>
</ul>
</li>
<li>
<p>最后再用 <code>StringLiteral&lt;&gt;</code> 把它包起来:</p>
<pre><code class="language-csharp">StringLiteral&lt;
StringNode&lt;Char&lt;'S'&gt;,
    StringNode&lt;Char&lt;'e'&gt;,
      ...
    &gt;
&gt;
&gt;
</code></pre>
</li>
</ol>
<p>这一整个封闭泛型类型,就是字面量 <code>'Seattle'</code> 的类型版本。</p>
<p>而过滤器在需要值的时候,只是简单地访问 <code>TLiteral.Value</code>,再通过 <code>TString.Length</code> 和 <code>TString.Write</code> 复原出一个 <code>ValueString("Seattle")</code>,其中复原通过静态类型的缓存完成,借助类型系统的力量,每一个独立的字面量都会产生一个单独的类型实例,我们的字面量就缓存在那个类型的静态字段里,从而避免了一切运行时的计算开销。</p>
<h4 id="null-字符串字面量">null 字符串字面量</h4>
<p><code>null</code> 的处理稍微特殊一点:</p>
<ul>
<li>写类似 <code>WHERE Team != null</code> 这种代码时,解析器会把它识别为 <code>LiteralKind.Null</code>;</li>
<li>对字符串列来说,<code>CreateStringLiteral(null)</code> 会返回 <code>typeof(StringLiteral&lt;StringNull&gt;)</code>;</li>
<li><code>StringNull.Length == -1</code>,于是 <code>StringLiteral&lt;StringNull&gt;.Value</code> 直接返回 <code>new ValueString(null)</code>。</li>
</ul>
<p>这样一来,<code>null</code> 和 <code>""</code> 在类型层面和运行时都可以被区分开。</p>
<h3 id="字面量工厂">字面量工厂</h3>
<p>上面这些编码最后都归到一个工厂类里统一封装:</p>
<pre><code class="language-csharp">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) { ... }
}
</code></pre>
<p>SQL 编译阶段会根据两方面信息来调用它:</p>
<ul>
<li>列的运行时类型(<code>int</code>、<code>float</code>、<code>bool</code>、<code>ValueString</code>);</li>
<li>字面量的种类(<code>Integer</code>、<code>Float</code>、<code>Boolean</code>、<code>String</code>、<code>Null</code>)。</li>
</ul>
<p>最终的效果就是:<strong>WHERE 子句里每一个字面量,都会变成一个具体的 <code>ILiteral&lt;T&gt;</code> 类型,值直接嵌在类型参数里。</strong></p>
<h2 id="搭好整个管道类型">搭好整个管道类型</h2>
<p>到目前为止,我们已经有了:</p>
<ul>
<li>一棵解析出来的查询(<code>SELECT</code> + <code>WHERE</code>);</li>
<li>一份 schema,把列名映射到具体的 <code>IColumn&lt;TRow, TValue&gt;</code> 实现;</li>
<li>一套机制,把字面量变成 <code>ILiteral&lt;T&gt;</code> 类型。</li>
</ul>
<p>SQL 编译器接下来要做的就是,把这些东西变成:</p>
<ul>
<li>一个封闭的管道类型 <code>TPipeline</code>,它实现 <code>IQueryNode&lt;TRow, TRuntimeResult, TRoot&gt;</code>;</li>
<li>一个运行时结果类型 <code>TRuntimeResult</code>;</li>
<li>一个对外公开的结果类型 <code>TPublicResult</code>。</li>
</ul>
<h3 id="编译-select">编译 <code>SELECT</code></h3>
<p>先看选择部分。</p>
<h4 id="select-"><code>SELECT *</code></h4>
<p>最简单的情况就是:<code>SELECT * FROM $</code>。</p>
<p>这时候:</p>
<ul>
<li>运行时结果类型 = 行类型本身:<code>TRuntimeResult = TRow</code>;</li>
<li>公共结果类型也是 <code>TRow</code>;</li>
<li>管道尾部就是一个 <code>Stop&lt;TRow, TRow&gt;</code> 节点。</li>
</ul>
<p>大致逻辑如下:</p>
<pre><code class="language-csharp">TRuntimeResult = typeof(TRow);
TPublicResult = typeof(TRow);
TPipelineTail = typeof(Stop&lt;,&gt;).MakeGenericType(TRuntimeResult, typeof(TRow));
</code></pre>
<h4 id="select-col--select-col1-col2-"><code>SELECT col</code> / <code>SELECT col1, col2, ...</code></h4>
<p>当有明确列投影时,步骤稍微多一点:</p>
<ul>
<li>
<p><code>SELECT col</code>:</p>
<ul>
<li>根据列名解析出对应的 <code>ColumnMetadata</code>;</li>
<li>决定它的运行时值类型:
<ul>
<li>如果列类型本身不是 <code>string</code>,运行时类型就跟它一致;</li>
<li>如果是 <code>string</code>,运行时类型改为 <code>ValueString</code>;</li>
</ul>
</li>
<li>构建一个 <code>ColumnProjection&lt;TRuntimeColumn, TRow, TRuntimeValue&gt;</code>。</li>
</ul>
</li>
<li>
<p><code>SELECT col1, col2, ...</code>:</p>
<ul>
<li>分别解析每一列;</li>
<li>构造一个 <code>ValueTupleProjection</code>,返回一个 <code>ValueTuple&lt;...&gt;</code>,里面放运行时类型;</li>
<li>同时记录一份公共 <code>ValueTuple&lt;...&gt;</code> 类型,用声明的 CLR 类型(如 <code>string</code>)。</li>
</ul>
</li>
</ul>
<p>最后,无论是一列还是多列,都会在 <code>Stop</code> 前面再加一个 <code>Select</code> 节点:</p>
<pre><code class="language-text">Select&lt;TRow, TProjection, Stop&lt;...&gt;, TMiddle, TRuntimeResult, TRoot&gt; → Stop&lt;...&gt;
</code></pre>
<p>这个节点内部会调用投影的静态 <code>Project</code> 方法,再把结果转交给 <code>Stop.Process</code> 处理。</p>
<h3 id="编译-where">编译 <code>WHERE</code></h3>
<p><code>WHERE</code> 子句以递归方式编译成类型。</p>
<h4 id="布尔结构">布尔结构</h4>
<p>给定一个解析后的 <code>WhereExpression</code> 树:</p>
<ul>
<li><code>A AND B</code> → <code>AndFilter&lt;TRow, TA, TB&gt;</code>;</li>
<li><code>A OR B</code> → <code>OrFilter&lt;TRow, TA, TB&gt;</code>;</li>
<li><code>NOT A</code> → <code>NotFilter&lt;TRow, TA&gt;</code>。</li>
</ul>
<p>编译器做的事情,大概是对这棵树一层层往下调自己的方法:</p>
<pre><code class="language-csharp">Type BuildPredicate&lt;TRow&gt;(WhereExpression expr)
{
    return expr switch
    {
      ComparisonExpression cmpExpr =&gt; BuildComparisonPredicate&lt;TRow&gt;(cmpExpr),
      AndExpression andExpr =&gt; typeof(AndFilter&lt;,,&gt;).MakeGenericType(typeof(TRow), BuildPredicate&lt;TRow&gt;(andExpr.Left), BuildPredicate&lt;TRow&gt;(andExpr.Right)),
      OrExpression orExpr =&gt; typeof(OrFilter&lt;,,&gt;).MakeGenericType(typeof(TRow), BuildPredicate&lt;TRow&gt;(orExpr.Left), BuildPredicate&lt;TRow&gt;(orExpr.Right)),
      NotExpression notExpr =&gt; typeof(NotFilter&lt;,&gt;).MakeGenericType(typeof(TRow), BuildPredicate&lt;TRow&gt;(notExpr.Expression)),
      _ =&gt; throw …
    };
}
</code></pre>
<h4 id="比较表达式">比较表达式</h4>
<p>每一个叶子比较表达式,比如:</p>
<pre><code class="language-sql">City = 'Seattle'
Salary &gt;= 180000
Team != null
</code></pre>
<p>都会变成一个具体的过滤器类型:</p>
<pre><code class="language-csharp">Type BuildComparisonPredicate&lt;TRow&gt;(ComparisonExpression comparison)
{
    var rowType = typeof(TRow);
    var column = SchemaRegistry&lt;TRow&gt;.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      =&gt; typeof(EqualsFilter&lt;,,,&gt;),
      ComparisonOperator.GreaterThan   =&gt; typeof(GreaterThanFilter&lt;,,,&gt;),
      ComparisonOperator.LessThan      =&gt; typeof(LessThanFilter&lt;,,,&gt;),
      ComparisonOperator.GreaterOrEqual=&gt; typeof(GreaterOrEqualFilter&lt;,,,&gt;),
      ComparisonOperator.LessOrEqual   =&gt; typeof(LessOrEqualFilter&lt;,,,&gt;),
      ComparisonOperator.NotEqual      =&gt; typeof(NotEqualFilter&lt;,,,&gt;),
      _ =&gt; throw …
    };

    return filterDefinition.MakeGenericType(
      rowType, runtimeColumnType, literalType, runtimeColumnValueType);
}
</code></pre>
<p>以 <code>City = 'Seattle'</code> 为例,如果那一列是字符串列,那么:</p>
<ul>
<li>运行时列类型是:<code>ValueStringColumn&lt;PersonCityColumn, Person&gt;</code>;</li>
<li>运行时值类型是:<code>ValueString</code>;</li>
<li>字面量类型,则是通过 <code>CreateStringLiteral("Seattle")</code> 得到的某个 <code>StringLiteral&lt;SomeStringNode&lt;…&gt;&gt;</code>。</li>
</ul>
<p>最后组合出一个过滤器类型:</p>
<pre><code class="language-csharp">EqualsFilter&lt;Person,
             ValueStringColumn&lt;PersonCityColumn, Person&gt;,
             StringLiteral&lt;...&gt;,
             ValueString&gt;
</code></pre>
<p>到这一步,我们就可以把一个 <code>Where</code> 节点挂到管道上了:</p>
<pre><code class="language-csharp">Where&lt;TRow, TPredicate, TNext, TRuntimeResult, TRoot&gt; → ...
</code></pre>
<h3 id="把-where-和-select-融合起来">把 <code>Where</code> 和 <code>Select</code> 融合起来</h3>
<p>直接这么拼出来的管道是正确的,但在性能上还能再优化一点:<br>
<code>Where</code> 和 <code>Select</code> 其实可以合并成一步。</p>
<p>TypedSql 里有一个很小的优化器,会去找这样的模式:</p>
<ul>
<li><code>Where&lt;TRow, TPredicate, Select&lt;TRow, TProjection, TNext, TMiddle, TResult, TRoot&gt;, TResult, TRoot&gt;</code></li>
</ul>
<p>一旦发现,就把它替换成:</p>
<pre><code class="language-csharp">WhereSelect&lt;TRow, TPredicate, TProjection, TNext, TMiddle, TResult, TRoot&gt;
</code></pre>
<p>这个融合节点的实现如下:</p>
<pre><code class="language-csharp">internal readonly struct WhereSelect&lt;TRow, TPredicate, TProjection, TNext, TMiddle, TResult, TRoot&gt;
    : IQueryNode&lt;TRow, TResult, TRoot&gt;
    where TPredicate : IFilter&lt;TRow&gt;
    where TProjection : IProjection&lt;TRow, TMiddle&gt;
    where TNext : IQueryNode&lt;TMiddle, TResult, TRoot&gt;
{
    public static void Run(ReadOnlySpan&lt;TRow&gt; rows, scoped ref QueryRuntime&lt;TResult&gt; runtime)
    {
      for (var i = 0; i &lt; rows.Length; i++)
      {
            Process(in rows, ref runtime);
      }
    }

    public static void Process(in TRow row, scoped ref QueryRuntime&lt;TResult&gt; runtime)
    {
      if (TPredicate.Evaluate(in row))
      {
            var projected = TProjection.Project(in row);
            TNext.Process(in projected, ref runtime);
      }
    }
}
</code></pre>
<p>于是像下面这种常见的查询:</p>
<pre><code class="language-sql">SELECT Name FROM $ WHERE City = 'Seattle'
</code></pre>
<p>最终就会是:</p>
<pre><code class="language-text">WhereSelect&lt;...&gt; → Stop&lt;...&gt;
</code></pre>
<p>也就是说:<strong>一个循环里完成过滤和投影</strong>,不需要再分两趟。并且,我们的优化器还能识别更复杂的嵌套结构,尽可能地把 <code>Where</code> 和 <code>Select</code> 融合在一起,减少中间步骤,提升性能。而这并不需要复杂的优化算法,只需要简单地把泛型参数取出来重新带入到新的融合类型即可,实现起来非常简单。</p>
<h2 id="结果转换">结果转换</h2>
<p>管道把所有行跑完之后,最后还得把结果以某种形式“交出去”。</p>
<p>一个查询的入口长这样:</p>
<pre><code class="language-csharp">internal static class QueryProgram&lt;TRow, TPipeline, TRuntimeResult, TPublicResult&gt;
    where TPipeline : IQueryNode&lt;TRow, TRuntimeResult, TRow&gt;
{
    public static IReadOnlyList&lt;TPublicResult&gt; Execute(ReadOnlySpan&lt;TRow&gt; rows)
    {
      var runtime = new QueryRuntime&lt;TRuntimeResult&gt;(rows.Length);
      TPipeline.Run(rows, ref runtime);

      return ConvertResult(ref runtime);
    }

    private static IReadOnlyList&lt;TPublicResult&gt; ConvertResult(ref QueryRuntime&lt;TRuntimeResult&gt; runtime)
    {
      if (typeof(IReadOnlyList&lt;TRuntimeResult&gt;) == typeof(IReadOnlyList&lt;TPublicResult&gt;))
      {
            return (IReadOnlyList&lt;TPublicResult&gt;)(object)runtime.Rows;
      }
      else if (typeof(IReadOnlyList&lt;TRuntimeResult&gt;) == typeof(IReadOnlyList&lt;ValueString&gt;) &amp;&amp; typeof(IReadOnlyList&lt;TPublicResult&gt;) == typeof(IReadOnlyList&lt;string&gt;))
      {
            return (IReadOnlyList&lt;TPublicResult&gt;)(object)runtime.AsStringRows();
      }
      else if (RuntimeFeature.IsDynamicCodeSupported &amp;&amp; typeof(TRuntimeResult).IsGenericType &amp;&amp; typeof(TPublicResult).IsGenericType)
      {
            return runtime.AsValueTupleRows&lt;TPublicResult&gt;();
      }

      throw new InvalidOperationException($"Cannot convert query result from '{typeof(TRuntimeResult)}' to '{typeof(TPublicResult)}'.");
    }
}
</code></pre>
<p>可以看到主要有三种情况:</p>
<ol>
<li>
<p><strong>运行时结果类型和公共结果类型一模一样</strong><br>
→ 直接把 <code>Rows</code> 返回就行。</p>
</li>
<li>
<p><strong>运行时内部用的是 <code>ValueString</code>,外面希望看到 <code>string</code></strong><br>
→ 调用 <code>AsStringRows</code>,它会把内部的 <code>ValueString[]</code> 包装一下,对外返回 <code>string?</code>(靠隐式转换)。</p>
</li>
<li>
<p><strong>两边都是某种 <code>ValueTuple</code> 形状</strong><br>
→ 用 <code>AsValueTupleRows&lt;TPublicResult&gt;()</code>,底层交给 <code>ValueTupleConvertHelper</code> 去做拷贝和字段转换。</p>
</li>
</ol>
<h3 id="valuetupleconverthelper用动态-il-在元组之间搬运字段"><code>ValueTupleConvertHelper</code>:用动态 IL 在元组之间搬运字段</h3>
<p><code>ValueTupleConvertHelper&lt;TPublicResult, TRuntimeResult&gt;</code> 的职责是:</p>
<ul>
<li>在两个兼容形状的 <code>ValueTuple</code> 之间搬运字段;</li>
<li>识别并处理 <code>string</code> ↔ <code>ValueString</code> 的转换;</li>
<li>如果 <code>ValueTuple</code> 有 <code>Rest</code>(嵌套元组),要递归下去做同样的事情。</li>
</ul>
<p>它在类型初始化时,会生成一个 <code>DynamicMethod</code> 来做拷贝:</p>
<pre><code class="language-csharp">internal static class ValueTupleConvertHelper&lt;TPublicResult, TRuntimeResult&gt;
{
    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&lt;TRuntimeResult, TPublicResult&gt;(ref Unsafe.AsRef(in source));
      }
      else
      {
            _helper.Invoke(ref dest, in source);
      }
    }

    static ValueTupleConvertHelper()
    {
      // 构造 DynamicMethod 和 IL,按字段复制,
      // 若发现 string &lt;-&gt; ValueString,就做对应转换,
      // 遇到 Rest 字段时递归。
    }
}
</code></pre>
<p>这样,运行时内部可以用一个对自己更舒服的元组类型,比如 <code>(ValueString, int, ValueString, …)</code>,而外面看到的则是 <code>(string, int, string, …)</code>,两者之间通过这一层帮助类桥接,成本也很低。这使得查询过程可以最大化利用值类型的泛型特化优势,同时对外还不需要暴露这些内部细节,达到了性能和易用性的平衡。</p>
<p>不过需要注意的是,这一块用到了动态代码生成,所以在一些受限环境(比如 AOT)下可能无法使用,因此 TypedSql 会在编译阶段检查这一点,确保只有在支持动态代码的环境下,才允许使用这种元组转换。否则的话,就只能退回到直接让运行时结果类型和公共结果类型一致的方式。</p>
<h2 id="整体流程编译并执行查询">整体流程:编译并执行查询</h2>
<p>站在使用者的角度,入口一般会是这样的:</p>
<pre><code class="language-csharp">var compiled = QueryEngine.Compile&lt;Person, string&gt;(
    "SELECT Name FROM $ WHERE City != 'Seattle'");
</code></pre>
<p><code>Compile&lt;TRow, TResult&gt;</code> 在内部会做这么几件事:</p>
<ol>
<li>解析 SQL,生成 <code>ParsedQuery</code>;</li>
<li>把 SQL 编译成:
<ul>
<li>管道类型 <code>TPipeline</code>;</li>
<li><code>TRuntimeResult</code>;</li>
<li><code>TPublicResult</code>;</li>
</ul>
</li>
<li>检查 <code>TPublicResult</code> 是否和你指定的 <code>TResult</code> 一致;</li>
<li>构造 <code>QueryProgram&lt;TRow, TPipeline, TRuntimeResult, TPublicResult&gt;</code> 这个类型;</li>
<li>找到它的静态方法 <code>Execute(ReadOnlySpan&lt;TRow&gt;)</code>;</li>
<li>把它变成一个委托,塞进 <code>CompiledQuery&lt;TRow, TResult&gt;</code>。</li>
</ol>
<p><code>CompiledQuery&lt;TRow, TResult&gt;</code> 本身只是包了一个委托:</p>
<pre><code class="language-csharp">private readonly Func&lt;ReadOnlySpan&lt;TRow&gt;, IReadOnlyList&lt;TResult&gt;&gt; _entryPoint
    = executeMethod.CreateDelegate&lt;Func&lt;ReadOnlySpan&lt;TRow&gt;, IReadOnlyList&lt;TResult&gt;&gt;&gt;();
</code></pre>
<p>然后对外暴露:</p>
<pre><code class="language-csharp">public IReadOnlyList&lt;TResult&gt; Execute(ReadOnlySpan&lt;TRow&gt; rows)
    =&gt; _entryPoint(rows);
</code></pre>
<p>得益于 .NET 10 对委托的逃逸分析、去虚拟化和内联等优化,这一层委托调用可以说几乎没有任何开销。</p>
<p>在 JIT 看来,一旦 <code>Compile</code> 做完这些准备工作,以后每次 <code>Execute</code> 就只是:</p>
<ul>
<li>一次直接的静态调用;</li>
<li>调入一个所有类型参数已经封死的泛型方法;</li>
<li>这个方法里面再调用一串全是 <code>struct</code> 和静态方法组成的管道。</li>
</ul>
<p>最终编译出来的类型,你既可以直接拿去执行,也可以把它输出到代码里然后通过 NativeAOT 编译成原生二进制文件,一套代码同时支持 JIT 和 AOT!</p>
<h2 id="使用和性能测试">使用和性能测试</h2>
<h3 id="快速上手">快速上手</h3>
<p>和很多轻量级查询库类似,TypedSql 的打开方法是:</p>
<ol>
<li>
<p>定义你的行类型,例如:</p>
<pre><code class="language-csharp">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);
</code></pre>
</li>
<li>
<p>为每一列实现一个 <code>IColumn&lt;Person, TValue&gt;</code>;</p>
</li>
<li>
<p>把这些列注册到 <code>Person</code> 对应的 schema 里;</p>
</li>
<li>
<p>然后就可以编译并运行查询,例如:</p>
<pre><code class="language-csharp">// 编译一次
var wellPaidManagers = QueryEngine.Compile&lt;Person, Person&gt;(
    """
    SELECT * FROM $
    WHERE Department = 'Engineering'
    AND IsManager = true
    AND YearsAtCompany &gt;= 5
    AND Salary &gt; 170000
    AND Country = 'US'
    """);

// 针对不同数据集多次执行
var result = wellPaidManagers.Execute(allPeople.AsSpan());
</code></pre>
</li>
</ol>
<p>要是你只需要一部分列,也可以返回元组:</p>
<pre><code class="language-csharp">var seniorTitles = QueryEngine.Compile&lt;Person, (string Name, string City, string Level)&gt;(
    """
    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}]");
}
</code></pre>
<p>所有重活——解析 SQL、字面量编码、在类型系统里搭管道——都发生在编译查询这一步。<br>
之后每次 <code>.Execute</code>,都只是跑一遍已经专门化好的静态管道,没有任何的运行时分发,没有任何的虚拟调用,不存在任何的反射和装箱,完全是 JIT 能看懂的强类型、零分配代码,从而实现极高的性能。</p>
<h3 id="简单性能对比">简单性能对比</h3>
<p>TypedSql 的目标并不是炫技用类型,而是想试试看:在保持 SQL 风格外壳的情况下,我们能让生成的代码离一个手写循环有多近。</p>
<p>一个非常简单的 benchmark 就是拿三个方案做对比:</p>
<ul>
<li>一条 TypedSql 查询;</li>
<li>一条等价的 LINQ 查询;</li>
<li>一段手写的 <code>foreach</code> 循环。</li>
</ul>
<p>任务内容:</p>
<ul>
<li>过滤出 <code>City == "Seattle"</code> 的行;</li>
<li>返回它们的 <code>Id</code>。</li>
</ul>
<p>TypedSql 编译出来的类型大概是这样:</p>
<pre><code class="language-csharp">QueryProgram&lt;
    Person,
    WhereSelect&lt;
      Person,
      EqualsFilter&lt;
            Person,
            ValueStringColumn&lt;PersonCityColumn, Person&gt;,
            'Seattle',
            ValueString
      &gt;,
      ColumnProjection&lt;PersonIdColumn, Person, Int32&gt;,
      Stop&lt;Int32, Person&gt;,
    Int32,
    Int32,
    Person&gt;,
Int32,
Int32
&gt;
</code></pre>
<p>让我们来看看 RyuJIT 为我们的查询方案生成了什么样的机器码:</p>
<pre><code class="language-nasm">G_M000_IG01:                ; prologue
       push   r15
       push   r14
       push   rdi
       push   rsi
       push   rbp
       push   rbx
       sub      rsp, 40
       mov      rbx, rcx

G_M000_IG02:                ; 分配结果数组
       mov      esi, dword ptr
       mov      edx, esi
       mov      rcx, 0x7FFE71F29558
       call   CORINFO_HELP_NEWARR_1_VC
       mov      rdi, rax
       xor      ebp, ebp
       mov      rbx, bword ptr
       test   esi, esi
       jle      SHORT G_M000_IG06

G_M000_IG03:                ; 初始化循环变量
       xor      r14d, r14d

G_M000_IG04:                ; 循环体
       lea      r15, bword ptr
       mov      rcx, gword ptr
       mov      rdx, 0x16EB0400D30
       mov      rdx, gword ptr
       mov      rdx, gword ptr
       cmp      rcx, rdx
       je       G_M000_IG12
       test   rcx, rcx
       je       SHORT G_M000_IG05
       test   rdx, rdx
       je       SHORT G_M000_IG05
       mov      r8d, dword ptr
       cmp      r8d, dword ptr
       je       SHORT G_M000_IG08

G_M000_IG05:                ; 更新循环计数器
       add      r14, 72
       dec      esi
       jne      SHORT G_M000_IG04

G_M000_IG06:                ; 产生结果对象
       mov      rcx, 0x7FFE72227600
       call   CORINFO_HELP_NEWSFAST
       mov      rbx, rax
       lea      rcx, bword ptr
       mov      rdx, rdi
       call   CORINFO_HELP_ASSIGN_REF
       mov      dword ptr , ebp
       mov      rax, rbx

G_M000_IG07:                ; epilogue
       add      rsp, 40
       pop      rbx
       pop      rbp
       pop      rsi
       pop      rdi
       pop      r14
       pop      r15
       ret

G_M000_IG08:                ; 字符串长度比较
       lea      rax, bword ptr
       add      rdx, 12
       mov      ecx, dword ptr
       add      ecx, ecx
       mov      r8d, ecx
       cmp      r8, 10
       je       SHORT G_M000_IG10

G_M000_IG09:                ; 字符串内容慢速比较
       mov      rcx, rax
       call   
       jmp      SHORT G_M000_IG11

G_M000_IG10:                ; 字符串内容快速比较
       mov      rcx, qword ptr
       mov      rax, qword ptr
       mov      r8, qword ptr
       xor      rcx, r8
       xor      rax, qword ptr
       or       rcx, rax
       sete   al
       movzx    rax, al

G_M000_IG11:                ; 处理比较结果
       test   eax, eax
       je       SHORT G_M000_IG05

G_M000_IG12:                ; 把匹配的 Id 写入结果数组
       mov      ecx, dword ptr
       lea      rax, bword ptr
       lea      edx,
       mov      r15d, edx
       movsxd   rdx, ebp
       mov      dword ptr , ecx
       mov      ebp, r15d
       jmp      G_M000_IG05
</code></pre>
<p>注意看 <code>G_M000_IG08</code> 的 <code>r8, 10</code>,这里的 <code>10</code> 就是字符串字面量 <code>'Seattle'</code> 的长度,JIT 直接把我们的字符串字面量的长度常量嵌进了机器码里;进一步当长度匹配时,JIT 又生成了代码跳转到 <code>G_M000_IG10</code>,这段代码专门处理长度为 10 的字符串的快速比较路径。也就是说,JIT 不仅把字面量的值嵌进去了,还根据它生成了专门的代码路径!</p>
<p>再注意看循环计数器的更新部分,<code>G_M000_IG05</code> 里的 <code>add r14, 72</code>,这里的 <code>72</code> 就是 <code>sizeof(Person)</code>,JIT 直接把行类型的大小常量也嵌进去了,避免了运行时的计算;而 <code>dec esi</code> 更是直接把递增的循环优化成了递减,减少了一次比较指令。</p>
<p>上述代码的逻辑等价于:</p>
<pre><code class="language-csharp">int length = elements.Length;
Span&lt;int&gt; values = new int;
int count = 0;

for (int i = length - 1; i &gt;= 0; i--)
{
    var elem = elements;
    var city = elem.City;
    if (city == null)
      continue;

    if (city.Length == 10 &amp;&amp; city == "Seattle")
    {
      values = elem.Id;
      count++;
    }
}

return values[..count];
</code></pre>
<p>看到了吗?跟你手写的循环几乎一模一样!我们的抽象完全被 JIT 优化的一干二净!</p>
<p>上个跑分结果:</p>
<table>
<thead>
<tr>
<th>Method</th>
<th style="text-align: right">Mean</th>
<th style="text-align: right">Error</th>
<th style="text-align: right">StdDev</th>
<th style="text-align: right">Gen0</th>
<th style="text-align: right">Code Size</th>
<th style="text-align: right">Allocated</th>
</tr>
</thead>
<tbody>
<tr>
<td>TypedSql</td>
<td style="text-align: right">10.953 ns</td>
<td style="text-align: right">0.0250 ns</td>
<td style="text-align: right">0.0195 ns</td>
<td style="text-align: right">0.0051</td>
<td style="text-align: right">111 B</td>
<td style="text-align: right">80 B</td>
</tr>
<tr>
<td>Linq</td>
<td style="text-align: right">27.030 ns</td>
<td style="text-align: right">0.1277 ns</td>
<td style="text-align: right">0.1067 ns</td>
<td style="text-align: right">0.0148</td>
<td style="text-align: right">3,943 B</td>
<td style="text-align: right">232 B</td>
</tr>
<tr>
<td>Foreach</td>
<td style="text-align: right">9.429 ns</td>
<td style="text-align: right">0.0417 ns</td>
<td style="text-align: right">0.0326 ns</td>
<td style="text-align: right">0.0046</td>
<td style="text-align: right">407 B</td>
<td style="text-align: right">72 B</td>
</tr>
</tbody>
</table>
<p>可以看到:<strong>TypedSql 在时间和分配上无限逼近 <code>foreach</code>,远远超过即使是在 .NET 10 中已经被高度优化后的 LINQ 的性能。</strong></p>
<p>这也符合我们对它内部结构的预期:</p>
<ul>
<li>查询管道是类型层级的,结构在编译期就定死</li>
<li>列、投影、过滤全是值类型 + 静态方法</li>
<li>字符串统一走 <code>ValueString</code> 热路径</li>
<li>字面量则通过 <code>ILiteral&lt;T&gt;</code> 嵌在类型参数里</li>
<li>所有这些都让 JIT 能够把代码特化、展开、内联,最终生成和手写循环几乎一样的机器码</li>
</ul>
<h2 id="尾声">尾声</h2>
<p>TypedSql 只是一个简单的内存查询引擎实验。它只是围绕一个很具体的问题:<strong>C# 的类型系统到底能让我们把多少查询逻辑搬过去,.NET 又能针对这些类型生成多快的代码?</strong></p>
<p>于是,在 TypeSql 中,我们实现了:</p>
<ul>
<li>把列、投影、过滤全都表示成带静态方法的 <code>struct</code>,并通过接口的静态抽象成员来约束它们的行为</li>
<li>把它们组合成一串嵌套的泛型管道节点(<code>Where</code>、<code>Select</code>、<code>WhereSelect</code>、<code>Stop</code>)</li>
<li>把数字和字符串字面量都编码成类型(<code>ILiteral&lt;T&gt;</code>)</li>
</ul>
<p>最后得到的是一个小小的、看起来很像 SQL 的内存查询引擎;而在 JIT 眼里,它其实就是一套可以进行高度优化的、类型特化后的循环。</p>
<p>因此答案是肯定的:<strong>.NET 的类型系统完全可以用来表达图灵完备的逻辑,并且借助 JIT 编译器的强大优化能力,生成非常高效的代码。</strong></p>
<p>展望未来的应用,诸如查询引擎、DSL 编译器、甚至是语言运行时等复杂系统,都可以通过类似的方式来实现,从而在保持灵活性的同时,最大化性能。而你甚至不需要实现任何的代码生成后端,只要利用好 C# 的泛型和静态成员,就能让 JIT 帮你完成大部分的工作。而把构建好的类型输出成代码文件,再通过 NativeAOT 编译成原生二进制文件,也同样是可行的。编写一次,同时支持 JIT 和 AOT,两全其美。并且不同于 C++ 的模板和 constexpr,我们的引擎是完全支持来自外部的动态输入的,而不需要在编译时确定一切!</p>
<p>本项目的代码已经开源在 GitHub 上,欢迎点赞和 Star:https://github.com/hez2010/TypedSql</p><br><br>
来源:https://www.cnblogs.com/hez2010/p/19261972/turning-csharp-type-system-into-a-query-engine
頁: [1]
查看完整版本: TypedSql:在 C# 类型系统上实现一个 SQL 查询引擎