

1亿个点是一场压力测试,它把为规模而生的图表库与为仪表盘而生的图表库区分开来。在 WinUI 上,这道分界线格外分明,因为大多数 WinUI 图表都是通过 XAML 层绘制的托管 .NET 控件,而 ProEssentials 在底层承载的是一个原生的 Direct3D 与 Direct2D C++ 引擎。
我们在五个 WinUI 库中绘制了同一个 1亿个点的信号:ProEssentials、Syncfusion、Telerik、DevExpress 和 ComponentOne。下面的代码是每个库中通往图表的最短且诚实的路径。真正的差异并不在于代码行数,而在于你的数据在送达屏幕途中经历了什么。
ProEssentials 就地读取你的 float 数组,并在 GPU 上重建几何图形。那四个托管套件绑定到一个对象集合,每个点对应一个对象,于是同样的 1亿个数值在绘制任何内容之前,就膨胀成一个数 GB 的托管堆。
Reproduce the WinUI numbers yourself: clone, build, run the WinUI 100M-point chart demo on GitHub.Prefer WPF or WinForms? See the WPF 100M-point demo and the WinForms 100M-point demo.
相同的数据、相同的机器、五个库。我们衡量的是代码行数、内存开销、能否渲染出来,以及显示是无损的还是经过降采样的近似。
| Parameter | Value |
|---|---|
| Data points | 100,000,000 (100 million) |
| Data type | float (4 bytes per value) — 400 MB raw data |
| Chart type | 2-D line chart, single series, sequential X-axis |
| Pattern | Sine wave with random noise (realistic signal data) |
| Platform | WinUI 3 (.NET 10), Windows 11, mid-range GPU |
| What we measure | Lines of code, memory overhead, render time, data fidelity |
下面这个模式在所有五个库中反复出现:加载数据的代码,远比配置图表的代码重要。
图表通过 UseDataAtLocation 读取你已有的 float 数组,因此绝不会产生第二份拷贝。全部 1亿个数值都送往 GPU,在那里 Filter2D3D compute shader 会先执行一遍保守的 min/max 预过滤,然后由最终的着色器构建场景。每一个点都会被处理。当数据比显示区域更密集时,过滤器会为每个像素列保留 50 对 min/max 值,即每像素 100 个绘制点,因此不会丢弃任何尖峰,密集区域会渲染成真正的实心填充,与完整数据集所产生的图像完全一致。
// ProEssentials — Plot 100 Million Points (WinUI 3, ~15 lines)
// NuGet: Install-Package ProEssentials.Chart.Net10.WinUI
// Control: PesgoWinUI (named Pesgo1 in XAML). .NET 10, x64 and native ARM64.
// 1. Allocate your data — this is the ONLY copy that will ever exist
float[] yData = new float[100_000_000];
for (int i = 0; i < yData.Length; i++)
yData[i] = (float)Math.Sin(i * 0.0001) + (float)(rand.NextDouble() * 0.1);
// 2. Configure the chart
Pesgo1.PeData.Subsets = 1;
Pesgo1.PeData.Points = 100_000_000;
Pesgo1.PePlot.Method = SGraphPlottingMethod.Line;
Pesgo1.PeConfigure.RenderEngine = RenderEngine.Direct3D;
Pesgo1.PeData.ComputeShader = true; // build chart geometry on the GPU
Pesgo1.PeData.Filter2D3D = true; // lossless GPU min/max pre-filter
Pesgo1.PeData.StagingBufferY = true; // required with ComputeShader
// 3. Zero-copy: point the chart at your existing array — no duplication
Pesgo1.PeData.Y.UseDataAtLocation(yData, yData.Length);
// 4. Rebuild vertices on the GPU and render
Pesgo1.PeFunction.Force3dxVerticeRebuild = true;
Pesgo1.PeFunction.ReinitializeResetImage();
Pesgo1.Invalidate();呈现同样是原生的。引擎渲染到自己的 DXGI flip-model composition swap chain 中,并通过 ISwapChainPanelNative::SetSwapChain 交给 XAML 宿主,于是 DWM 直接把图表合成到你的可视化树中,没有中间位图,也没有逐帧的 CPU 拷贝。它还通过 SetMaximumFrameLatency(1) 限制呈现队列,这正是为什么一张 1亿个点的图表在拖动时依然能实时跟随,而不会在你前面缓冲一堆帧。
图表额外占用的内存只有一个很小的 GPU 暂存缓冲区。你的 400 MB 数据仍然是那 400 MB,而同一个 PesgoWinUI 控件无论处理 10,000 个点还是 1亿个点,都无需特殊的序列类型。
接下来的四个示例都忠实于各厂商文档记载的 API,并且能够编译。但它们做不到的,是真的为你画出一张图表。把它们中的任何一个指向 1亿个数值,现实的结果要么是在分配循环的某处抛出 OutOfMemoryException,要么是等待时间长到你不等它跑完就先杀掉了调试器。我们附上这些代码,是因为它能把差异说清楚,而不是指望有人真的以这种规模去运行。每段代码真正有启发性的部分,不是底部的图表配置,而是顶部那个为每个数据点分配一个对象的循环。
在阅读性能宣传时,这一点值得记住。这四家都把自己的图表宣传为高性能,而在他们确实公布了数字的那些平台上,这一说法是公允的。问题在于,那些并不是 WinUI 的数字。
关于这些数字的说明。这四家厂商都没有专门针对其 WinUI 图表发布大数据基准测试。Syncfusion 的百万点基准测试是 WPF 的。DevExpress 最大的那些数字依赖于 SwiftPlotSeriesView,这是一个仅限 WinForms 的序列视图。ComponentOne 公布的 Direct2D 数字是在 .NET 6 的 WinForms 版本上约 5 ms 绘制 50,000 个点。Telerik 对大型数据集的建议是在绑定之前先把数据聚合缩减。我们自己并未对他们的 WinUI 控件做过基准测试,因此不会替他们臆造性能上限;我们能说的是,目前尚不存在任何一家针对 WinUI 大数据性能的公开实证。
作为参照,这里是我们自己的极限。ProEssentials 的 Direct2D 路径在约 300 万个点时开始吃力,而这已经是在零拷贝加载、没有逐点对象开销的前提下。真正把它推到 1亿个点的是 Direct3D compute shader 路径。而一个为每个点分配对象的托管 XAML 控件,起点远在其后。
Syncfusion 的 fast series 会把折线光栅化到一个 WriteableBitmap 上,相比默认路径这是一项实实在在的优化。问题出在数据模型上:图表绑定到一个对象集合,因此 1亿个点就意味着 1亿次内存分配,而这远在位图路径运行之前就已发生。
// Syncfusion — Plot 100 Million Points (WinUI, SfCartesianChart)
// NuGet: Install-Package Syncfusion.Chart.WinUI
// 1. Syncfusion binds to objects — one per point
public class DataPoint { public double X { get; set; } public double Y { get; set; } }
// 2. Build the collection — 100M objects is ~2.4 GB+ with object headers
var data = new List<DataPoint>(100_000_000);
for (int i = 0; i < 100_000_000; i++)
data.Add(new DataPoint { X = i, Y = Math.Sin(i * 0.0001) + rand.NextDouble() * 0.1 });
// This allocation alone risks OutOfMemoryException; the practical ceiling is well below 100M.
// 3. Fast bitmap series — rasterizes the line to a WriteableBitmap
var series = new FastLineBitmapSeries {
ItemsSource = data,
XBindingPath = "X",
YBindingPath = "Y"
};
chart.Series.Add(series);值得留意 Syncfusion 那个百万点数字的来源:他们为此公布的基准测试是 WPF 的,而非 WinUI。无论如何,决定上限的是为每个点分配一个对象的模型,而不是光栅化器。
Telerik 通过 Composition ContainerVisualsFactory 来绘制其序列,这让普通图表保持流畅。与 Syncfusion 一样,它绑定到对象,而其针对大数据的官方建议是:在把数据交给图表之前先进行降采样。
// Telerik — Plot 100 Million Points (WinUI, RadCartesianChart)
// NuGet: Install-Package Telerik.WinUI.Controls (license key required)
// 1. Telerik binds to objects — one per point
public class DataPoint { public double X { get; set; } public double Y { get; set; } }
// 2. Build the collection (~2.4 GB+ at 100M objects)
var data = new ObservableCollection<DataPoint>();
for (int i = 0; i < 100_000_000; i++)
data.Add(new DataPoint { X = i, Y = Math.Sin(i * 0.0001) + rand.NextDouble() * 0.1 });
// 3. Line series — drawn through the Composition ContainerVisualsFactory
var series = new LineSeries {
ItemsSource = data,
CategoryBinding = new PropertyNameDataPointBinding { PropertyName = "X" },
ValueBinding = new PropertyNameDataPointBinding { PropertyName = "Y" }
};
// For large data, Telerik expects you to downsample before binding.
radChart.Series.Add(series);先降采样意味着图表永远看不到全部 1亿个点,这是一种合理的策略,只是并非无损。
DevExpress 的 WinUI ChartControl 通过 argument 和 value 成员来绑定 DataSource。对于大型数据集,DevExpress 建议开启数据采样,让控件只渲染一个有代表性的子集,而非每一个点。
// DevExpress — Plot 100 Million Points (WinUI, ChartControl)
// NuGet: DevExpress WinUI packages (private feed, credentials required)
// 1. DevExpress binds to objects — one per point
public class DataPoint { public double Argument { get; set; } public double Value { get; set; } }
// 2. Build the collection (~2.4 GB+ at 100M objects)
var data = new List<DataPoint>(100_000_000);
for (int i = 0; i < 100_000_000; i++)
data.Add(new DataPoint { Argument = i, Value = Math.Sin(i * 0.0001) + rand.NextDouble() * 0.1 });
// 3. Line series on the ChartControl — bind DataSource + data members
var series = new LineSeries();
series.DataSource = data;
series.ArgumentDataMember = "Argument";
series.ValueDataMember = "Value";
chartControl.Series.Add(series);
// DevExpress recommends data sampling to keep large sets responsive.采样能让 UI 保持响应,但在这一切发生之前,同样存在为每个点分配一个对象的问题。
ComponentOne 是唯一提供 Direct2D 渲染模式的竞品,相比其默认路径确实是一项实实在在的优化。但它仍是一个绑定到对象集合的托管控件,因此内存表现与其他产品一致。在吞吐量方面,ComponentOne 为 Direct2D 公布的具体数字是约 5 ms 绘制 50,000 个点,而且是在 .NET 6 的 WinForms 版本上测得的,并非 WinUI。
// ComponentOne (MESCIUS) — Plot 100 Million Points (WinUI, FlexChart)
// NuGet: Install-Package C1.WinUI.Chart
// 1. FlexChart binds to objects — one per point
public class DataPoint { public double X { get; set; } public double Y { get; set; } }
// 2. Build the collection (~2.4 GB+ at 100M objects)
var data = new List<DataPoint>(100_000_000);
for (int i = 0; i < 100_000_000; i++)
data.Add(new DataPoint { X = i, Y = Math.Sin(i * 0.0001) + rand.NextDouble() * 0.1 });
// 3. Bind the chart, then switch on the Direct2D render mode for large data
flexChart.ItemsSource = data;
flexChart.BindingX = "X";
flexChart.Series.Add(new Series { Binding = "Y" });
flexChart.RenderMode = RenderMode.Direct2D; // required to draw millions of pointsDirect2D 模式是托管 WinUI 图表最接近原生方案的一步,在做客观对比时值得了解。
内存那几行是简单的算术,而非基准测试:1亿个 float 就是 400 MB,而 1亿个被绑定的对象在绘制第一个像素之前,就已背负数 GB 的对象头和引用。
| Factor | ProEssentials | Syncfusion | Telerik | DevExpress | ComponentOne |
|---|---|---|---|---|---|
| Renders 100M in real time? | ✅ Yes — natively | ❌ OOM well before 100M | ⚠️ Aggregate first | ⚠️ Sample first | ⚠️ Direct2D mode (50k published) |
| Rendering | Native Direct3D compute | WriteableBitmap | Composition visuals | Managed XAML | Direct2D (optional) |
| Data model | Zero-copy float[] pointer | Object-per-point | Object-per-point | Object-per-point | Object-per-point |
| Memory: your data | 400 MB | 400 MB | 400 MB | 400 MB | 400 MB |
| Memory: library overhead | ~0 MB | ~2,400 MB+ | ~2,400 MB+ | ~2,400 MB+ | ~2,400 MB+ |
| Total memory | ~400 MB | OOM risk | ~2,800 MB (or downsampled) | ~2,800 MB (or sampled) | ~2,800 MB |
| Special series type? | No — same control | FastLineBitmapSeries | Sampling settings | Sampling settings | Direct2D render mode |
| Full-fidelity display | Lossless GPU min/max | Bitmap raster | Downsampled subset | Sampled subset | Direct2D raster |
| Native ARM64 | Yes — native binary | Managed | Managed | Managed | Managed |
在这种规模下,决定一切的数字不是每秒帧数,而是你的数据在送达屏幕途中被拷贝了多少次。
ProEssentials 对数据零次拷贝。它直接读取你已经分配好的 float 数组,并在 GPU 上重建图像。而绑定到集合的托管图表会把每个数值拷贝进一个带有对象头和引用的对象里,这正是数 GB 内存的来源。
在几千个点时无人会察觉。而到了 1亿个点,这一个设计决策就是 400 MB 的应用与内存溢出应用之间的区别。
| Data Model | Overhead (100M pts) | Used By |
|---|---|---|
| Zero-copy pointer | ~0 MB | ProEssentials |
| Array copy (float) | ~400 MB | — |
| Object-per-point | ~2,400 MB+ | Syncfusion, Telerik, DevExpress, ComponentOne |
再快的渲染,也救不了一个为每个点都分配一个对象的数据模型。托管套件之所以求助于降采样和位图快速路径,正是因为其对象模型撑不住 1亿个数据点。而 ProEssentials 从一开始就不会制造这个问题。
这五个库都能画出漂亮的折线图。但只有一个能无损、实时地绘制 1亿个数据点,且无需你对数据降采样、切换序列类型,或忍受数 GB 的堆内存。这正是一个就地读取你数据的原生引擎所带来的回报。
如果你的 WinUI 应用只绘制数千个数据点,那么手头已有的任意套件都够用。但若要绘制数百万乃至数亿个数据点,数据模型就是决定成败的关键,而 ProEssentials 正是为此而生。
竞品的渲染方式与数据绑定 API 均引自各厂商自己的官方文档:
Syncfusion (WinUI SfCartesianChart)
Telerik (UI for WinUI RadChart)
DevExpress (WinUI ChartControl)
ComponentOne / MESCIUS (FlexChart for WinUI)
我们的首要目标是通过为您的机构和终端用户提供最简单、最专业的服务,达成您的成功。
ProEssentials是由需要自定义图表组件的专业电气工程师创立的。加入使用ProEssentials的顶级工程公司名单。
感谢您成为ProEssentials的客户,也感谢您研究ProEssentials图表引擎。