本文收藏我给 WPF 做的性能测试。在你开始认为 WPF 的性能存在问题的时候,不妨来这篇博客里找找看我做过的测试。我记录的测试都是比较纯净的测试项目,没有业务逻辑的干扰,写法也正常,可以更加真实反映 WPF 的性能,减少因为奇怪的业务逻辑以及逗比的写法的影响

资源字典

大量 Geometry 资源对启动的影响

在资源字典里面存放了 5k 个 Geometry 对象,资源字典加入到 App.Xaml 的资源字典里面,意味着启动时就会加载这些资源。根据 WPF 对资源对象创建的定义,可以了解到,在 WPF 里面不会立刻创建资源对象,只有在资源对象首次被使用时才会被创建。也就是说加入到 App.Xaml 的资源字典的 5k 个 Geometry 对象将只会被记录到 App 的资源字典里面,但没有实际创建出来

实际测试性能大概是在我电脑上加载只需 50 毫秒左右

以上测试代码放在githubgitee 欢迎访问

可以通过如下方式获取本文的源代码,先创建一个名为 JojeryiheenelNearfinelwhea 的空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 1d3883ac7feba5dd7752e1edccd33f943c02f7f9

以上使用的是 gitee 的源,如果 gitee 不能访问,请替换为 github 的源。请在命令行继续输入以下代码

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin 1d3883ac7feba5dd7752e1edccd33f943c02f7f9

获取代码之后,进入 JojeryiheenelNearfinelwhea 文件夹

大量资源图片对启动的影响

我创建了 100 张图片,将这些图片作为资源的存在,接着写一个 资源字典 引用这 100 张图片。这 100 张图片都属于不同的图片,最后构建出来的 DLL 文件大概有 300 MB 这么大

将资源字典同样在 App.xaml 里引用加入,测量 App 的 InitializeComponent 时间发现近乎没有受到图片数量的影响。在我的设置上 Debug 模式下仅不到百毫秒即可完成,即使我是放在机械硬盘上运行的

我编写的测试代码放在githubgitee 欢迎访问

可以通过如下方式获取本文的源代码,先创建一个名为 WhirawahereRallcobaiwe 的空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 9c660568dfadef19c3393a42ca0925b9e72cd749

以上使用的是 gitee 的源,如果 gitee 不能访问,请替换为 github 的源。请在命令行继续输入以下代码

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin 9c660568dfadef19c3393a42ca0925b9e72cd749

获取代码之后,进入 WhirawahereRallcobaiwe 文件夹。里面包含一个生成测试图片和测试代码的项目和一个用来测试启动性能的 WPF 项目

以下是生成测试图片的代码

                    WriteableBitmap writeableBitmap = new WriteableBitmap(1024, 1024, 96, 96, PixelFormats.Pbgra32, null);

                    writeableBitmap.Lock();
                    unsafe
                    {
                        var length = writeableBitmap.PixelWidth * writeableBitmap.PixelHeight *
                           writeableBitmap.Format.BitsPerPixel / 8;
                        var backBuffer = (byte*) writeableBitmap.BackBuffer;
                        for (int i = 0; i + 4 < length; i = i + 4)
                        {
                            Span<byte> span = new Span<byte>(backBuffer, length);
                            span = span.Slice(i, 4);
                            Random.Shared.NextBytes(span);
                        }
                    }

                    writeableBitmap.Unlock();

                    PngBitmapEncoder encoder = new PngBitmapEncoder();
                    encoder.Frames.Add(BitmapFrame.Create(writeableBitmap));

                    var file = $"{fileName}.png";

                    using var fileStream = File.OpenWrite(file);
                    encoder.Save(fileStream);

可以看到是采用随机的像素的方式生成的图片,如此即可让每个图片保证是不同的。从以上测试项目可以了解到,假定在启动过程中 WPF 框架是做了图片加载和解析的工作的,那 WPF 的启动时间绝对不可能有这么快,我猜测在许多年内都无法在机械盘上不到百毫秒内完成 100 张 1024x1024 的不同图片的解码。换句话说就是 WPF 的资源字典里面定义的引用确实是延迟加载的,在使用到的时候才真正创建对象,这符合我阅读 WPF 源代码所了解的

一千个半透明矩形做动画

WPF 动画性能测试应用 一千个半透明矩形做动画

画10万个矩形

测试代码放在githubgitee 欢迎访问

可以通过如下方式获取本文的源代码,先创建一个名为 NarlearcefearNuyikallair 的空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 4983492acb47c040ecb80b7417f7cf364d1e3e19

以上使用的是 gitee 的源,如果 gitee 不能访问,请替换为 github 的源。请在命令行继续输入以下代码

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin 4983492acb47c040ecb80b7417f7cf364d1e3e19

获取代码之后,进入 NarlearcefearNuyikallair 文件夹

带刷新率的绘制一万矩形版本的测试代码放在 githubgitee 欢迎访问

视频播放性能

WPF 模拟 WPFMediaKit 的 D3D 配置用来测试4k性能

WriteableBitmap 的拷贝性能

写 WriteableBitmap 的时候,调用 WritePixels 方法,进行一次 CPU->CPU 的内存拷贝。在 WPF 框架层,还会从 WriteableBitmap 执行一次 CPU->CPU 的内存拷贝,让数据到达渲染线程。最终在渲染阶段,需要来一次 CPU->GPU 的拷贝,从而在界面渲染出来。详细请参阅 dotnet 读 WPF 源代码笔记 WriteableBitmap 的渲染和更新是如何实现

本次测试内容就是一整个界面都是 Image 控件,这个 Image 控件带一个和窗口一样大(去掉标题栏哈)的 WriteableBitmap 对象,每次渲染都向其赋值新的预设内存数组。从而进行测试 WriteableBitmap 的拷贝和渲染性能

核心代码如下,以下代码不计性能代价,甚至可以完全跑满 GPU 资源。预计在 4K 的核心卡点在于内存带宽和 GPU 资源上

    public MainWindow()
    {
        InitializeComponent();
        Loaded += MainWindow_Loaded;
        _bufferEnumerator = GetBuffer().GetEnumerator();
    }

    private void MainWindow_Loaded(object sender, RoutedEventArgs e)
    {
        Image.Width = ActualWidth;
        Image.Height = ActualHeight;

        var width = (int)ActualWidth;
        var height = (int)ActualHeight;

        var writeableBitmap =
            new WriteableBitmap(width, (int)ActualHeight, 96, 96, PixelFormats.Bgra32, null);
        _writeableBitmap = writeableBitmap;
        Image.Source = writeableBitmap;

        for (int i = 0; i < 60; i++)
        {
            var buffer = new byte[4 * width * height];
            PixelBufferList.Add(buffer);
            FillBuffer(buffer, i);
        }

        Redraw();
    }

    private void FillBuffer(byte[] buffer, int index)
    {
        for (var i = 0; i < buffer.Length; i += 4)
        {
            buffer[i] = (byte)(10 * index);
            buffer[i + 1] = 0;
            buffer[i + 2] = 0xF1;
            buffer[i + 3] = 0xFF;

            Random.Shared.NextBytes(buffer.AsSpan(i, 3));

            buffer[i + 3] = 0xFF;
        }

        Debug.Assert(_writeableBitmap != null);
        var width = _writeableBitmap.PixelWidth;
        var height = _writeableBitmap.PixelHeight;

        for (int heightIndex = 100; heightIndex < Math.Min(height, 300); heightIndex++)
        {
            for (int widthIndex = 50; widthIndex < Math.Min(width, (index + 1) * (width / 60)); widthIndex++)
            {
                var span = buffer.AsSpan(heightIndex * width * 4 + widthIndex * 4, 3);
                span[0] = 0xFF;
                span[1] = 0x00;
                span[2] = 0x00;
            }
        }
    }

    private List<byte[]> PixelBufferList { get; } = [];
    private WriteableBitmap? _writeableBitmap;

    private readonly IEnumerator<byte[]> _bufferEnumerator;

    private IEnumerable<byte[]> GetBuffer()
    {
        while (true)
        {
            foreach (var buffer in PixelBufferList)
            {
                yield return buffer;
            }
        }
    }

    protected override void OnRender(DrawingContext drawingContext)
    {
        Dispatcher.InvokeAsync(Redraw, DispatcherPriority.Input);
    }

    private void Redraw()
    {
        Debug.Assert(_writeableBitmap != null);

        _bufferEnumerator.MoveNext();
        var buffer = _bufferEnumerator.Current;
        _writeableBitmap.Lock();
        _writeableBitmap.WritePixels(new Int32Rect(0, 0, _writeableBitmap.PixelWidth, _writeableBitmap.PixelHeight),
            buffer, 4 * _writeableBitmap.PixelWidth, 0);
        _writeableBitmap.Unlock();

        InvalidateVisual();
    }

以上代码放在 githubgitee 上,可以使用如下命令行拉取代码。我整个代码仓库比较庞大,使用以下命令行可以进行部分拉取,拉取速度比较快

先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin b14c7cca5ad1fae96cbb3a60ca9977d872fb8941

以上使用的是国内的 gitee 的源,如果 gitee 不能访问,请替换为 github 的源。请在命令行继续输入以下代码,将 gitee 源换成 github 源进行拉取代码。如果依然拉取不到代码,可以发邮件向我要代码

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin b14c7cca5ad1fae96cbb3a60ca9977d872fb8941

获取代码之后,进入 WPFDemo/NebalnefaichallkereQalayhearchal 文件夹,即可获取到源代码。欢迎拉取代码进行测试

如对框架底层的 WriteableBitmap 更新渲染感兴趣,请参阅 dotnet 读 WPF 源代码笔记 WriteableBitmap 的渲染和更新是如何实现WPF 从 WriteableBitmap 里获取到渲染线程使用的 IWICBitmap 对象


本文会经常更新,请阅读原文: https://blog.lindexi.com/post/WPF-%E6%80%A7%E8%83%BD%E6%B5%8B%E8%AF%95.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

如果你想持续阅读我的最新博客,请点击 RSS 订阅,推荐使用RSS Stalker订阅博客,或者收藏我的博客导航

知识共享许可协议 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名林德熙(包含链接: https://blog.lindexi.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系

微软最具价值专家


无盈利,不卖课,做纯粹的技术博客

以下是广告时间

推荐关注 Edi.Wang 的公众号

欢迎进入 Eleven 老师组建的 .NET 社区

以上广告全是友情推广,无盈利