更复杂的代码,为何跑得快了10倍?一次Draw Call优化引发的思考
大家好,最近我挖了一个新的开源项目坑:N-Body 模拟,这是一个纯粹由兴趣驱动的项目,旨在通过编程模拟天体间的万有引力,并欣赏由物理规律所生成的优美图形。
在这个项目中,有一个核心环节是绘制天体的运行轨迹。轨迹本质上是一条由无数个点连接而成的曲线。为了高效存储这些点,我使用了一个 CircularBuffer<T>
,即环形缓冲区。它的内部实现相当经典:一个数组加上两个指针,分别标记数据的有效起止位置,非常适合存储这种定长的流式数据。
初遇瓶颈:当轨迹长到令人抓狂
最初,我选择使用 Direct2D 的 DrawLine
方法来逐段绘制轨迹。代码的逻辑非常直观,就是遍历轨迹点,然后两两相连画线:
for (int i = 0; i < _lastSnapshot.Stars.Length; ++i)
{
StarSnapshot star = _lastSnapshot.Stars[i];
StarUIProps prop = _uiProps[i];
// 遍历每两个相邻的点,并绘制一条线段
prop.TrackingHistory.Enumerate2((Vector2 from, Vector2 to, int i) =>
{
// 根据点的位置计算一个渐变透明度
float alpha = 1.0f * i / (prop.TrackingHistory.Count - 1);
Color4 color = new Color4(prop.Color.R, prop.Color.G, prop.Color.B, alpha);
// 调用DrawLine API
ctx.DrawLine(from, to, XResource.GetColor(color), 0.02f);
});
}
在轨迹点不多的时候,这套方案跑得非常欢快。然而,当用户希望看到更长、更华丽的轨迹时,问题就暴露了。当点的数量达到 10万 个级别时,界面开始出现肉眼可见的卡顿和掉帧。很显然,性能瓶颈出现了,优化迫在眉睫。
量化问题:用数据说话
为了精准定位问题,我进行了一次简单的性能测试。我使用 Stopwatch
来记录在轨迹点数达到10万个时,整个绘制过程的耗时。
protected override void OnDraw(ID2D1DeviceContext ctx)
{
// ... 其他绘制准备工作 ...
Stopwatch sw = Stopwatch.StartNew();
DrawCore(ctx); // 核心绘制逻辑
// 当轨迹点达到10万时,打印耗时
if (_uiProps[0].TrackingHistory.Count == 100000)
{
sw.Elapsed.TotalMilliseconds.Dump();
}
// ... 其他效果处理 ...
}
测试结果相当不乐观,连续几次的耗时输出如下:
50.0262
51.7592
51.0839
50.7521
50.838
平均耗时稳定在 50毫秒 左右!这是一个什么概念?为了保证流畅的用户体验(比如 60 FPS),每一帧的渲染时间必须控制在 16.67毫秒 以内。现在 50 毫秒的耗时,意味着帧率已经掉到了 20 FPS 以下,卡顿是必然的结果。
柳暗花明:一次调用胜过十万次
既然 DrawLine
的循环调用是瓶颈,那么优化的思路就应该是减少调用的次数。在和朋友讨论后,我决定尝试使用 ID2D1PathGeometry
来重构绘制逻辑。
ID2D1PathGeometry
允许我们先在内存中构建一个完整的几何路径,然后一次性地将其提交给 GPU 进行绘制。新的代码如下:
// 先绘制轨迹
for (int i = 0; i < _lastSnapshot.Stars.Length; ++i)
{
StarSnapshot star = _lastSnapshot.Stars[i];
StarUIProps prop = _uiProps[i];
if (prop.TrackingHistory.Count < 2) continue;
// 1. 创建一个路径几何对象
using ID2D1PathGeometry1 path = XResource.Direct2DFactory.CreatePathGeometry();
// 2. 打开路径并获取一个"画笔" (GeometrySink)
using ID2D1GeometrySink sink = path.Open();
// 3. 定义路径的起点
sink.BeginFigure(prop.TrackingHistory.First!.Value, FigureBegin.Hollow);
// 4. 将所有的点批量添加到路径中
prop.TrackingHistory.Enumerate((pt, index) =>
{
if (index > 0) { sink.AddLine(pt); }
});
// 5. 结束并关闭路径定义
sink.EndFigure(FigureEnd.Open);
sink.Close();
// 6. 一次性将整个路径绘制出来
ctx.DrawGeometry(path, XResource.GetColor(prop.Color), 0.02f);
}
改完代码后,我怀着忐忑的心情再次运行性能测试,结果让我大吃一惊:
6.8739
6.4511
6.436
6.0901
5.9227
平均耗时骤降到了 6毫秒 左右!性能几乎提升了 10倍!
刨根问底:为什么“更重”的代码跑得更快?
这个结果一度让我非常困惑。从代码表面上看,使用 ID2D1PathGeometry
的版本涉及到了更多的 API 调用:CreatePathGeometry
、Open
、BeginFigure
、AddLine
、EndFigure
、Close
,还有多个 using
语句。这套操作看起来比一个简单的 DrawLine
调用要“重”得多。
我曾经误以为,DrawLine
是一个非常底层的、直接的绘制指令,而 ID2D1PathGeometry
是一个更上层、更抽象的封装,性能可能会更差。
但真正的关键在于理解 Draw Call
(绘制调用)的成本。
每一次 ctx.DrawLine
的调用,都是一次 CPU 到 GPU 的通信,我们称之为 Draw Call
。这是一个相对昂贵的操作,因为它涉及到状态切换、数据传输和驱动程序开销。在我最初的实现中,绘制10万个点的轨迹,就意味着产生了 10万次 Draw Call
!
而使用 ID2D1PathGeometry
的方案,虽然在 CPU 端看起来代码更复杂,但所有的路径构建工作(AddLine
等)都只在内存中进行,不涉及与 GPU 的直接交互。直到最后调用 ctx.DrawGeometry
时,这 10 万个点的几何数据才被打包好,一次性地提交给 GPU。
这就相当于,我们将 10万次零散的 Draw Call
合并成了一次重量级的 Draw Call
。GPU 一次性接收所有数据,然后高效地完成光栅化。虽然单次传输的数据量变大了,但完全避免了 99999 次昂贵的通信开销。这正是性能提升近10倍的根本原因。
总结
这次优化经历让我深刻体会到,在性能优化的世界里,找到瓶颈所在,远比知道如何优化更重要。
- 表象具有欺骗性:API 调用的多寡并不直接等同于性能开销。看起来“重”的代码,可能因为更符合底层硬件的工作原理而快如闪电。
- 理解原理是关键:如果不理解
Draw Call
的成本,我可能会在其他地方(比如数据存储、颜色计算)浪费大量时间,而这些地方的优化对于整体性能来说可能只是杯水车薪。只有理解了“CPU与GPU通信是昂贵的”这一原理,才能找到正确的优化方向。 - 量化驱动优化:没有性能测试的数据支撑,所有的优化都只是猜测。通过
Stopwatch
精准量化,我们能清晰地看到优化的效果,并确认我们的方向是正确的。
性能问题往往不是因为计算机“算得慢”,而是因为我们在用一种“低效的方式”让它去算。理解其工作原理,才能让它发挥出真正的威力。
感谢您阅读到这里!如果感觉本文对您有帮助,请不吝 评论 和 点赞,这也是我持续创作的最大动力!
也欢迎加入我的 .NET骚操作 QQ群:495782587,一起交流 .NET 和 AI 的各种有趣玩法!
更复杂的代码,为何跑得快了10倍?一次Draw Call优化引发的思考的更多相关文章
- 编写更少量的代码:使用apache commons工具类库
Commons-configuration Commons-FileUpload Commons DbUtils Commons BeanUtils Commons CLI Commo ...
- 面试 6:拓展性更好的代码,更容易拿到 Offer
今天给大家带来的是 <剑指 Offer>习题:调整数组顺序使奇数位于偶数前面,纯 Java 实现希望大家多加思考. 面试题:输入一个整型数组,实现一个函数来调整该数组中的数字的顺序,使 ...
- 面试官:如何写出让 CPU 跑得更快的代码?
前言 代码都是由 CPU 跑起来的,我们代码写的好与坏就决定了 CPU 的执行效率,特别是在编写计算密集型的程序,更要注重 CPU 的执行效率,否则将会大大影响系统性能. CPU 内部嵌入了 CPU ...
- git 常用命令,上传,下载,更新线上代码
git 常用命令以及推荐git新建上传个人博客 $ git clone //本地如果无远程代码,先做这步,不然就忽略 $ git status //查看本地自己修改了多少文件 $ git add . ...
- 通过利用immutability的能力编写更安全和更整洁的代码
通过利用immutability的能力编写更安全和更整洁的代码 原文:Write safer and cleaner code by leveraging the power of "Imm ...
- VS Code Java 更新 – 全新Gradle for Java插件,更方便的代码操作, 1.0 语言支持发布
大家好,欢迎来到 9 月版的 Visual Studio Code Java 更新.在这篇文章中,我们将分享我们最新的Gradle插件,更加方便的代码操作(Getter/Setter等等),以及最近的 ...
- 性能测试记录: ZZ 只改5行代码获得10倍吞吐量提升
首先得找台足够性能的机器来测试,性能不足时代码运行会出现各种奇怪的现象,导致浪费时间 文章: https://www.jianshu.com/p/4cd8596352ad 只改了5行代码吞吐量提升 ...
- 程序员需要经纪人吗?10x 最好的程序员其生产力相当于同行的 10 倍~
原文地址 10x 起源于技术界一个流行的说法,即最好的程序员是超级明星,其生产力相当于同行的 10 倍: Google 园区以好玩的设施闻名:小憩舱.球坑.按摩.干洗.随便吃到饱的自助餐.(为了拍人才 ...
- Web 应用性能提升 10 倍的 10 个建议
转载自http://blog.jobbole.com/94962/ 提升 Web 应用的性能变得越来越重要.线上经济活动的份额持续增长,当前发达世界中 5 % 的经济发生在互联网上(查看下面资源的统计 ...
- 使用Apache Spark 对 mysql 调优 查询速度提升10倍以上
在这篇文章中我们将讨论如何利用 Apache Spark 来提升 MySQL 的查询性能. 介绍 在我的前一篇文章Apache Spark with MySQL 中介绍了如何利用 Apache Spa ...
随机推荐
- Qt图像处理技术六:拉普拉斯锐化
Qt图像处理技术六:拉普拉斯锐化 效果图 源码 由该公式得到下方卷积核 使用到的卷积核: //都把QImage转化为rgb888更好运算 QImage LaplaceSharpen(const QIm ...
- FileChooser文件保存样例
FileChooser fc = new FileChooser();fc.setTitle("请选择文件保存位置");fc.setInitialDirectory($原始文件位置 ...
- 把多个文件打包压缩成tar.gz文件并解压的Java实现
压缩文件 在Java中,可以 使用GZIPOutputStream创建gzip(gz)压缩文件,它在commons-compress下面,可以通过如下的maven坐标引入: <depende ...
- TINYINT[M]、INT[M]和BIGINT[M]中M值的意义
TINYINT[(M)] [UNSIGNED] [ZEROFILL] A very small integer. The signed range is -128 to 127. The unsign ...
- 【UEFI】HOB 从概念到代码
总述 使用 HOB 的原因是因为,在 PEI 阶段内存尚未完全初始化,到了 DXE 阶段才完整初始化了内存,所以无法通过简单的内存地址传递数据,并且我们仍然有一些对于内存空间存储的需求,因此发明了 H ...
- 几种简单的springboot启动后启动一条死循环线程方式
前言 之前有测试 # 启动类加 @EnableAsync # 方法上加注解 @Async @PostConstruct 但是依旧会卡主主线程,所有另辟蹊径 第一种 在启动类上加注解 @EnableAs ...
- Docker Compose部署随机图API
Docker Compose部署随机图API 平时我们部署博客的时候,为了考虑美观会考虑使用随机图来作为文章的封面,现在有很多大佬愿意提供随机图API,通过API我们可以很方便地部署随机图,不必自己寻 ...
- 直播预约丨《袋鼠云大数据实操指南》No.4:数据服务API实战解读,助力企业数字化跃迁
近年来,新质生产力.数据要素及数据资产入表等新兴概念犹如一股强劲的浪潮,持续冲击并革新着企业数字化转型的观念视野,昭示着一个以数据为核心驱动力的新时代正稳步启幕. 面对这些引领经济转型的新兴概念,为了 ...
- 数据安全新战场,EasyMR为企业筑起“安全防线”
2020年1月,时间跨度长达14年的,微软2.5亿条客户服务和支持记录在网上泄露: 同年4月,微盟发生史上最贵"删库跑路"事件,造成微盟市值一夜之间缩水约24亿港币: 今年7月,网 ...
- .Net Web API 002 Program和WeatherForecastController
创建工程后,工程主要包含了Program.cs和WeatherForecastController.cs两个代码文件,还有一个WeatherForecast.cs文件,该文件定义的天气情况数据结构替, ...