首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >做好毫秒级的C#执行器要考虑哪些因素?

做好毫秒级的C#执行器要考虑哪些因素?

作者头像
郑子铭
发布2025-08-06 17:48:30
发布2025-08-06 17:48:30
2200
举报

目标

在「冷启动」场景下,把一段未知、可能不可信的 C# 代码编译并执行到 ≤ 100 ms(理想 < 20 ms),同时保证安全、可伸缩、跨平台。


一开始我看到.NET 10.0新版本有单文件执行功能,感觉可能有希望,我试了一下:

先写了一个 hello-world.cs

代码语言:javascript
复制
Console.WriteLine("Hello World");

是的,它还是一个顶级表达式的代码,现在 C# 都是没问题的。

然后再(在 pwsh 里面跑):

代码语言:javascript
复制
measure-command { dotnet hello-world.cs }

发现:

代码语言:javascript
复制
PS C:\Users\sdfly> measure-command { dotnet run ./hello-world.cs }

Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 1
Milliseconds      : 90
Ticks             : 10909927
TotalDays         : 1.26272303240741E-05
TotalHours        : 0.000303053527777778
TotalMinutes      : 0.0181832116666667
TotalSeconds      : 1.0909927
TotalMilliseconds : 1090.9927

速度是 1090 毫秒,看起来离毫秒级有点远,然后再多跑几遍看看效果:

代码语言:javascript
复制
PS C:\Users\sdfly> measure-command { dotnet run ./hello-world.cs }

Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 0
Milliseconds      : 456
Ticks             : 4568788
TotalDays         : 5.28794907407407E-06
TotalHours        : 0.000126910777777778
TotalMinutes      : 0.00761464666666667
TotalSeconds      : 0.4568788
TotalMilliseconds : 456.8788

嗯……还行,但是还离我理想中的速度 < 100 ms 甚至 < 20 ms 差得有点远。

但如果你看 LINQPad,在那里面跑代码你几乎感受不到运行时间——相比 dotnet run file.cs

如图,在 LINQPad 中,编译加执行,还是带优化的,总结也只需要 2 毫秒(怀疑这个时间并没有带编译时间)——但无论如何,LINQPad 里面执行代码几乎是无感的,这是为什么呢?—— 因为 dotnet run 是基于 msbuild 的,但 LINQPad 是基于 Roslyn Scripting API 的,而且启动 LINQPad.exe 后,进程常驻,CLR 已经加载完毕,JIT 过的 Hot 方法保留在内存里。

同样的道理,可以改成不用 dotnet run,而是使用 Roslyn Scripting API:

代码语言:javascript
复制
var opts = ScriptOptions.Default
    .AddReferences(typeof(object).Assembly)   // mscorlib/System.Runtime
    .AddImports("System");                    // 免写 using

await CSharpScript.EvaluateAsync(
    "Console.WriteLine(\"warm-up\");", opts); // 预热

for (int i = 0; i < 10; i++)
{
    var sw = Stopwatch.StartNew();
    var d = await CSharpScript.EvaluateAsync($"Console.WriteLine({i});", opts);
    Console.WriteLine($"耗时:{sw.Elapsed.TotalMilliseconds:F2} ms,结果:{d}");
}

输出如下:

代码语言:javascript
复制
warm-up
0
耗时:7.24 ms,结果:
1
耗时:6.74 ms,结果:
2
耗时:8.15 ms,结果:
3
耗时:10.05 ms,结果:
4
耗时:9.64 ms,结果:
5
耗时:19.61 ms,结果:
6
耗时:9.59 ms,结果:
7
耗时:8.26 ms,结果:
8
耗时:9.74 ms,结果:
9
耗时:8.84 ms,结果:

可见可以跑到 10 ms 以内了,这是一个惊人的提升,如果想要整体跑到毫秒级别的速度,必须要使用 Roslyn Scripting API。

看起来蛮好,可以,代价是什么呢?代价有如下:

  1. Roslyn Scripting API 你需要自己管理 NuGet,如果有一些第三方引用,则需要自己想办法
  2. Roslyn Scripting API 不支持依赖于 Source Generator/Incremental Generator 的东西,比如 [GeneratedRegex],再比如 [LibraryImport],不知道大家有没有用过,我觉得还是蛮有用的,但确实也不会常用——其实也不是完全不支持,要支持的话需要自己再写额外的代码做后处理——当然后处理的时间也需要算在内

我觉得作为以接入大语言模型的 MCP 为目的的话,放弃这两个功能是没关系的。

除此之外还有一点问题:

  1. 要保证执行是未受信任的未知代码的话,还需要保证安全性,安全性的话,还要跑 docker,我亲测跑个 docker 就需要大约 3 秒
  2. 客户的代码关注的是冷启动,不能用热启动的速度来代表实际客户会体验到的性能

对于安全性,我其实之前有过几次写 Online Judger 的经验,我和 Windows Job Object 和一些进程级权限打过交道,但我觉得搞起来还是复杂了点。

安全性一般要求:

  1. 限制代码不能访问用户对象,比如当前电脑的按钮、窗口什么的,不能让客户发一份代码过来电脑就中毒了
  2. 限制代码不能访问其它内核对象,比如不能打开进程、读取内存、结束进程什么的
  3. 限制代码的 CPU 使用率(比如只允许 2 核)、CPU 使用总量(比如只允许 2 核满载时跑 5 秒钟——相当于 10 核秒),最大内存占用
  4. 限制代码能执行的最长时间,比如不能让它跑超过 30 秒之类的
  5. 限制代码能访问的磁盘路径,作为一个与大模型对接的 MCP,我觉得有必要允许它可以输出一些文件,这应该会很有趣,但不对输出到不允许它输出的地方
  6. (考虑)限制代码能使用的总网络流量,比如不能超过 200 MB 之类的

那有人问,在执行代码前搞一个大语言模型扫描一下代码,判断判断,不就行了吗?——当然不能,因为我上面说了,我对这个速度的期望是毫秒级的,而大语言模型的实际响应速度可能是分钟级的,除此之外有些隐藏比较深的代码我觉得大语言模型不一定能“看”得出来有危险。

最后我还是觉得走 docker 比较方便,但要用 docker 的话,就必要解决它冷启动性能不足的问题。

我的想法是:

  1. docker 提前启动、提前预热、提前待命
  2. 为了适应多线程的访问需求,Host 进程还需要同时提前启动和维护多个 docker
  3. 每个 docker 使用可能是一次性的,用完就扔掉(感觉有点不经济,也许最后我可能仍然会考虑回到使用 Job Object——但还是那句话,docker 是最简单的)
  4. 另外我还需要考虑多平台的兼容性,走 docker 的话当然是用 Linux,但 Windows 也有 docker,我知道虽然可能没几个人做 windows 的 docker,但我觉得如果同时兼容的话应该会很有趣
  5. Host 进程可能也可能不和 Docker 服务器在同一台机子里面,它们之前会用 docker api 进行管理

整个系统的架构可能是:

  1. Host 进程
  2. Docker 集群

它们之前的关系是: Host 进程是一个 web 服务器,它接受用户 HTTP 请求 Docker 进程也是一个 web 服务器,它接受来自 Host 进程的 HTTP 请求 Host 进程通过 Docker API 访问和管理 Docker 集群 Host 进程自动分发用户请求到 Docker 群集 它们并不会使用 Docker Compose 来启动,因为 docker compose 只有有限的几个 Docker,我心中想的是 Docker 群集的数量是由 Host 进程管理的——但转念一想,由 Docker Compose 来管理总 Docker 的数量可能也是一个好主意,这样 Host 就可以少管一件事了(到时候我会和大语言模型讨论一下)

我现在正在做一些 Docker Runner 的代码,是的,已经新建文件夹了 目前测出来的数据是 Docker Runner 跑一个 Hello World 出来需要大约 60 ms(冷启动),如图:

数值变化在 30 ms ~ 100 ms 不等。

目前就是这样,明天继续看,感谢大家看到这里,有兴趣的朋友欢迎大家加入我的《.NET骚操作》QQ群:495782587

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-08-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 DotNet NB 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 目标
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档