Microsoft Agent Framework/C#:了解Workflows的几种不同模式

📅 发布时间:2026/7/13 20:20:05 👁️ 浏览次数:
Microsoft Agent Framework/C#:了解Workflows的几种不同模式
囟木让奶对业务代码是否有“侵入性”几乎没有不需要改写代码不需要 unsafe不需要特殊编译选项你写的是“好代码”JIT 会越来越聪明地帮你榨干性能。.NET 10 中的 JIT 编译器包括通过更好的代码生成和优化策略提高性能的重要增强功能。改进结构参数的代码生成.NET 的 JIT 编译器能够进行称为物理提升的优化其中结构的成员放置在寄存器而不是堆栈中从而消除了内存访问。 将结构传递给方法时此优化特别有用调用约定要求在寄存器中传递结构成员。.NET 10 改进了 JIT 编译器的内部表示形式以处理共享寄存器的值。 以前当需要将结构成员打包到单个寄存器中时JIT 将首先将值存储在内存中然后将其加载到寄存器中。 现在JIT 编译器可以将结构参数的优化成员直接存储到共享寄存器中从而消除不必要的内存操作。1234567891011121314151617181920212223struct Point{public long X;public long Y;public Point(long x, long y){X x;Y y;}}[MethodImpl(MethodImplOptions.NoInlining)]private static void Consume(Point p){Console.WriteLine(p.X p.Y);}private static void Main(){Point p new Point(10, 20);Consume(p);}在 x64 CPU架构上Point 的成员会分配到单独的寄存器并传递给 Consume。由于本地 p 的物理提升已启动因此最初不会在堆栈上分配任何内容。Program:Main() (FullOpts):mov edi, 10mov esi, 20tail.jmp [Program:Consume(ProgramPoint)]现在假设成员 Point 的类型已更改为 int 而不是 long。 由于 a int 为四个字节宽寄存器在 x64 上宽 8 个字节因此调用约定要求在一个寄存器中传递成员 Point 。以前JIT 编译器首先将值存储到内存中然后将八字节区块加载到寄存器中。在.NET 10中JIT 编译器现在可以直接将结构参数的提升成员放入共享寄存器中Program:Main() (FullOpts):mov rdi, 0x140000000Atail.jmp [Program:Consume(ProgramPoint)]这样就不需要中间内存存储从而生成更高效的程序集代码。改进了循环转化JIT 编译器可以提升循环的条件 while 并将循环体转换为 do-while 循环从而生成最终形状1234567if (loopCondition){do{// loop body} while (loopCondition);}此转换称为循环反转。 通过将条件移动到循环底部JIT 无需将分支到循环顶部来测试条件从而改进代码布局。 许多优化如循环克隆、循环展开和感应变量优化也依赖于循环反转来生成此形状来帮助分析。.NET 10 通过从词法分析实现切换到基于图形的循环识别实现来增强循环反转。此更改会考虑所有自然循环即只有单个入口点的循环并忽略以前被考虑的误报从而提高了精度。 这使得包含 for 和 while 语句的 .NET 程序具有了更高的优化潜力。数组接口方法反虚拟化.NET 10 的重点 领域 之一是减少常用语言功能的抽象开销。 为了追求此目标JIT 去虚拟化方法调用的能力已经扩展为涵盖数组接口方法。请考虑遍历一个数组的典型方法123456789static int Sum(int[] array){int sum 0;for (int i 0; i array.Length; i){sum array[i];}return sum;}此代码形式易于 JIT 优化主要是因为不存在虚拟调用。 相反JIT 可以专注于删除对数组访问的边界检查并应用 .NET 9 中添加的循环优化。 以下示例添加一些虚拟调用1234567891011static int Sum(int[] array){int sum 0;IEnumerable temp array;foreach (var num in temp){sum num;}return sum;}基础集合的类型是明确的JIT 应该能够将此代码片段转换为第一个代码片段。 但是数组接口的实现方式与“普通”接口不同因此 JIT 不知道如何对它们进行反虚拟化。 这意味着循环中的 foreach 枚举器调用仍然是虚拟的从而阻止了多个优化例如内联和堆栈分配。从 .NET 10 开始JIT 可以取消虚拟化和内联数组接口方法。 这是实现两者性能相等的诸多步骤中的第一步如.NET 10 去抽象计划中所述。内联改进.NET 10 中进行了各种内联改进。JIT 现在可以内联因以前的内联而适合实现非虚拟化的方法。 这种改进使 JIT 可以发现更多的优化机会例如进一步内联和去虚拟化。某些具有异常处理语义的方法尤其是具有 try-finally 块的方法也可以内联。为了更好地利用 JIT 的堆栈分配某些数组的能力内联器的启发式算法已进行调整以提高可能返回小型固定大小数组的候选项的收益。返回类型在内联过程中JIT 编译器现在会更新临时变量的类型这些变量用于存储返回值。 如果被调用方中的所有返回站点都生成相同的类型则使用此精确的类型信息来取消后续调用的虚拟化。 此增强功能补充了后期非虚拟化和数组枚举去抽象的改进。配置文件数据.NET 10 改进了 JIT 的内联策略以更好地利用配置文件数据。 在众多启发式算法中JIT 内联器不会对超过一定大小的方法进行内联以避免使调用方法变得臃肿。 当调用方拥有表明某个内联候选函数被频繁执行的配置文件数据时内联器会增加对该候选函数的大小容忍度。假设 JIT 将没有配置文件数据的被调用方 Callee 内联到具有配置文件数据的调用方 Caller 中。 这种不一致可能发生在被调用方过小而不值得进行检测或者被调用方被内联调用过于频繁从而导致调用次数不足。 如果 Callee 拥有自己的内联候选项JIT 此前并未将其纳入考虑范围因为 Callee 缺乏性能分析数据。 现在JIT 将识别到 Caller 拥有配置文件数据并因此放宽其大小限制但为了弥补精度损失这种放宽程度不及 Callee 拥有配置文件数据时。同样当 JIT 决定某个调用点不适合进行内联优化时它会用 NoInlining 标记该方法以避免未来再次尝试对该方法进行内联优化。 但是许多内联启发法对配置文件数据很敏感。 例如在缺少配置文件数据的情况下JIT 可能会认为某个方法太大而不值得进行内联。 但是当调用方足够热时JIT 可能愿意放宽其大小限制并内联调用。 在 .NET 10 中JIT 不再用 NoInlining 标记那些无利可图的内联函数以避免配置文件数据导致调用点的性能恶化。堆栈分配堆栈分配可减少 GC 必须跟踪的对象数并且还会解锁其他优化。 例如在对象被堆栈分配后JIT 可以考虑将其完全替换为其标量值。 因此堆栈分配是减少引用类型的抽象惩罚的关键。 .NET 10 为值类型和引用类型的小型数组添加堆栈分配。 它还包括对本地结构字段和委托的转义分析。 无法转义的对象可以在堆栈上分配。