[C# WPF] なんとかしてWPFの描画を速くしたい「DrawRectangleでのPenの有無」
最近WPFのパフォーマンスチューニングに勤しんでいます。
300,000個ほどのオブジェクトを描画するデスクトップアプリを作っている中で、役に立ったり効果のあった話をまとめていきます。
基本的には速度低下を招くよろしくない実装の確認や、対策の紹介などしていきます。
今回は「Penを使ってはいけない」について検証します。
環境
- Visual Studio 2019 Community
- .NET Framework 4.7.2
確認すること
10,000個の四角形をDrawRectangle()で描画する際に「Penを指定する場合」と「Penにnullを設定する場合」での処理にかかる時間の違い
検証コード(Penを指定する場合)
MainWindow.xaml
<Window x:Class="Sample_Performance_Pen.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Sample_Performance_Pen"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800"
        Loaded="Window_Loaded"
        >
    <Canvas>
        <local:View x:Name="xView"/>
        <Label x:Name="xLabel" Background="White"/>
    </Canvas>
</Window>
MainWindow.xaml.cs
using System;
using System.Collections.Generic;
using System.Windows;
namespace Sample_Performance_Pen
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            xView.ResultLabel = xLabel;
        }
        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            // 適当なシードで乱数を生成する(四角形を適当な位置へ置くのに使用)
            var randH = new Random(17280489);
            var randV = new Random(399594);
            var points = new List<Point>();
            for (var i = 0; i < 10000; ++i)
            {
                points.Add(new Point(randH.Next(800), randV.Next(450)));
            }
            xView.Points = points;
            xView.InvalidateVisual();
        }
    }
}
View.cs
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace Sample_Performance_Pen
{
    class View : Control
    {
        public List<Point> Points;
        public Label ResultLabel;
        protected override void OnRender(DrawingContext drawingContext)
        {
            base.OnRender(drawingContext);
            if (Points == null || ResultLabel == null)
            {
                return;
            }
            var sw = new System.Diagnostics.Stopwatch();
            sw.Start();
            foreach (Point point in Points)
            {
                var rect = new Rect(point.X, point.Y, 5, 5);
                drawingContext.DrawRectangle(Brushes.CadetBlue, new Pen(Brushes.CadetBlue, 1), rect);
            }
            sw.Stop();
            ResultLabel.Content = "Result: " + sw.ElapsedMilliseconds.ToString() + " ms";
        }
    }
}
検証コード(Penにnullを設定する場合)
DrawRectangle()へのPen設定をnullに変えただけです。
View.cs
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace Sample_Performance_Pen
{
    class View : Control
    {
        public List<Point> Points;
        public Label ResultLabel;
        protected override void OnRender(DrawingContext drawingContext)
        {
            base.OnRender(drawingContext);
            if (Points == null || ResultLabel == null)
            {
                return;
            }
            var sw = new System.Diagnostics.Stopwatch();
            sw.Start();
            foreach (Point point in Points)
            {
                var rect = new Rect(point.X, point.Y, 5, 5);
                drawingContext.DrawRectangle(Brushes.CadetBlue, null, rect);
            }
            sw.Stop();
            ResultLabel.Content = "Result: " + sw.ElapsedMilliseconds.ToString() + " ms";
        }
    }
}
結果
Penあり

Penなし

9ms差が出ていますが分かり辛いので描画数を100,000個に増やしてみます。
Penあり

Penなし

167msほど差がでました。
まとめ
Penはやっぱり重いみたいですね。
MFCのときから重かった気がするので、Penはできるだけ使わないように実装を考えないといけないです。
BrushとPenが同じ色の場合には、Penにはnullを指定して描画するように調整しましょう。
おしまい。



ディスカッション
コメント一覧
古い記事へのコメントで恐縮なのですが、他の「なんとかしてWPFの描画を速くしたい」シリーズに共通ですが、
・Pen,Brush等でCanFreezeなものはFreezeする
・ループ内で毎回Pen、Brushインスタンスを生成せず、できるだけ使いまわす。
とするだけで相当パフォーマンスは改善されるかと思うのですが、いかがでしょう。
※特にFreezeはDrawXXX系には相当効果あります。
コメントありがとうございます。おっしゃる通りです。
「なんとかしてWPFの描画を速くしたい」シリーズは、ここを変えるとこのくらい変わりますよ、というのを検証した内容になっていますので、問題にしている箇所以外は最適化していませんでした。
とはいえ、new の削減や Freeze による恩恵があるのは確かなので、new の削減や Freeze の有無を比較した記事も追加しておこうと思います。
ありがとうございました!
返信ありがとうございます。 私もWPFのパフォーマンスにはいつも悩まされてます。。