【対決】ユーザーコントロールとカスタムコントロールの速度比較

C#

こんにちは、働くC#プログラマーのさんさめです。

WPFで独自のレイアウトを持ったコントロールを作る場合、
ユーザーコントロールとして作る方法と、
カスタムコントロールとして作る方法があります。

たとえば、こちらの記事や、あちらの記事ではその違いについて書かれており、
パフォーマンスにも言及しています。

ユーザーコントロールの方が遅く、
カスタムコントロールの方が速いそうです。

さんさめ
さんさめ

実際どんなもんなんだろう…?
気にするほどなのか…?

このような疑問が出てきたので、
簡単なサンプルを作って実際に試してみました。

結論から言うと、
「シンプルなコントロールの場合、
カスタムコントロールの構築は
ユーザーコントロールの約5倍高速
ただし、別の場所で処理がかかる」
という結果になりました。

以下、条件や比較に使ったコードなど詳細を記録として残しておきます。

スポンサーリンク

比較に使ったコントロール

まず、比較に使ったコントロールを紹介します。

「ボタンの横に任意のコメントを付けられる」
というとてもシンプルなコントロールです。

xamlでは以下のように書いています。

さんさめ
さんさめ

前回の記事で、
カスタムコントロールへの移植サンプル
として使ったものですな

これを、ユーザーコントロール版と
カスタムコントロール版、それぞれ用意しました。

ユーザーコントロール版のコード詳細

ユーザーコントロール版のxamlは以下の通りです。

<UserControl x:Class="WpfCustomControlLibrary1.UserButtonWithComment"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:WpfCustomControlLibrary1">
    <DockPanel>
        <TextBlock DockPanel.Dock="Right"
                                   Text="{Binding Comment, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:UserButtonWithComment}}}"/>
        <Button Content="{Binding ButtonText, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:UserButtonWithComment}}}"
                                Command="{Binding Command, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:UserButtonWithComment}}}"/>
    </DockPanel>
</UserControl>

ビハインドは以下のようになっています。

public partial class UserButtonWithComment : UserControl
{
    public UserButtonWithComment()
    {
        InitializeComponent();
    }

    public ICommand Command
    {
        get { return (ICommand)GetValue(CommandProperty); }
        set { SetValue(CommandProperty, value); }
    }

    // Using a DependencyProperty as the backing store for Command.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty CommandProperty =
        DependencyProperty.Register("Command", typeof(ICommand), typeof(UserButtonWithComment), new PropertyMetadata(null));

    public string Comment
    {
        get { return (string)GetValue(CommentProperty); }
        set { SetValue(CommentProperty, value); }
    }

    // Using a DependencyProperty as the backing store for Comment.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty CommentProperty =
        DependencyProperty.Register("Comment", typeof(string), typeof(UserButtonWithComment), new PropertyMetadata(string.Empty));

    public string ButtonText
    {
        get { return (string)GetValue(ButtonTextProperty); }
        set { SetValue(ButtonTextProperty, value); }
    }

    // Using a DependencyProperty as the backing store for ButtonText.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ButtonTextProperty =
        DependencyProperty.Register("ButtonText", typeof(string), typeof(UserButtonWithComment), new PropertyMetadata(string.Empty));
}

カスタムコントロール版のコード詳細

カスタムコントロール版のxamlは以下です。
独立した専用ファイルというわけではなく、
Generic.xamlにStyleだけ書いてあります。

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfCustomControlLibrary1">

    <Style TargetType="{x:Type local:CustomButtonWithComment}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate>
                    <DockPanel>
                        <TextBlock DockPanel.Dock="Right"
                                   Text="{Binding Comment, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:CustomButtonWithComment}}}"/>
                        <Button Content="{Binding ButtonText, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:CustomButtonWithComment}}}"
                                Command="{Binding Command, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:CustomButtonWithComment}}}"/>
                    </DockPanel>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

コードは以下の通りです。
ユーザーコントロール版とほぼ一緒ですね。

public class CustomButtonWithComment : Control
{
    static CustomButtonWithComment()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomButtonWithComment), new FrameworkPropertyMetadata(typeof(CustomButtonWithComment)));
    }

    public ICommand Command
    {
        get { return (ICommand)GetValue(CommandProperty); }
        set { SetValue(CommandProperty, value); }
    }

    // Using a DependencyProperty as the backing store for Command.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty CommandProperty =
        DependencyProperty.Register("Command", typeof(ICommand), typeof(CustomButtonWithComment), new PropertyMetadata(null));

    public string Comment
    {
        get { return (string)GetValue(CommentProperty); }
        set { SetValue(CommentProperty, value); }
    }

    // Using a DependencyProperty as the backing store for Comment.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty CommentProperty =
        DependencyProperty.Register("Comment", typeof(string), typeof(CustomButtonWithComment), new PropertyMetadata(string.Empty));

    public string ButtonText
    {
        get { return (string)GetValue(ButtonTextProperty); }
        set { SetValue(ButtonTextProperty, value); }
    }

    // Using a DependencyProperty as the backing store for ButtonText.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ButtonTextProperty =
        DependencyProperty.Register("ButtonText", typeof(string), typeof(CustomButtonWithComment), new PropertyMetadata(string.Empty));
}

比較コードの詳細

さて、この2つのコントロールをそれぞれ
配置したWindowを用意します。

といっても、1個や2個置いただけでは、
誤差レベルになってしまいそうなので、
どーんと2000個置いてみます。

xamlは以下の通りです…
と、言いたいところですが、
画像で雰囲気だけ感じ取ってください(笑)

まずは、カスタムコントロールの方。

次に、ユーザーコントロールの方です。
行番号から異常さが伝わってきます。

そして、それぞれのビハインドに次のようなコードを仕込みます。

public MainWindowUser()
{
    var sw = new Stopwatch();
    sw.Start();
    InitializeComponent();

    sw.Stop();
    this.Title = $"{nameof(MainWindowUser)} - {sw.ElapsedMilliseconds.ToString()} ミリ秒";
}

要は、「InitializeComponentにどれくらいかかるか?」
を計測しているわけです。

これを、App.xaml.csに以下のように記述して
2つのウィンドウを出して比較してみます。

protected override void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);

    var view = new Window();
    view.Show();
    var sw = new Stopwatch();
    var customView = new MainWindowCustom();
    sw.Start();
    customView.Show();
    Debug.WriteLine($"Custom Show : {sw.ElapsedMilliseconds}ミリ秒");
    var userView = new MainWindowUser();
    sw.Restart();
    userView.Show();
    Debug.WriteLine($"User Show : {sw.ElapsedMilliseconds}ミリ秒");
}

インスタンス生成後の、
WindowのShowにかかる時間を測定しています。

始めにおもむろに空のWindowを
作ってShowしているのには理由があります。

最初、それぞれのMainWindowを作るだけの
コードにしていたのですが、
先に作ったWindowは処理時間が不当に長く
下駄をはかされている感じでした。

初めて生成したWindowに対して、
WPFフレームワーク側が何か行っているのでしょうか?

なんとなく、Application.ShutdownMode
に関連していそう(MainWindowと位置付けている?)な気もしますが、
詳細は調べていません。

さて、実行結果は以下のようになりました。

本当はたくさんやった方が良いのでしょうが、
正確な比較がしたいわけではなかったので、
数回試して傾向が変わらないことを確認して満足しました。

ユーザーコントロールとカスタムコントロールそれぞれ負荷の考察

InitializeComponentに関しては、
カスタムコントロールの方が5倍ほど高速です。

パフォーマンスプロファイラーで見てみましたが、
「解析」の部分がそれに相当するみたいですね。

カスタム版の解析は97ミリ秒
ユーザー版の解析は570ミリ秒。時間かかってます

つまり、xamlパースについては、
圧倒的にカスタムコントロールに軍配が上がります。

一方で、Showについては
なぜかユーザーコントロールの方が速いです。
5倍低速だった遅れを完全に取り戻してます。

レイアウトを合計すると約1.8秒
レイアウトを合計すると約1.2秒。600ミリ速い

しかし、やってることは
StackPanelにひたすら平置きしているだけなので、
原因があるとしたら、要素のサイズが
より下側の要素から先に決定しているとかでしょうか。

カスタムコントロールの場合、
レイアウトが定まるのがTemplateが適用されてからなので、
それによりレイアウト再計算が走っているのかもしれません。

だとすると、
レイアウト再計算が走らないような配置方法だったら、
カスタムコントロールの長所を
最大限に生かせるのかもしれません。

さんさめ
さんさめ

より詳しく調査が必要そう…

まとめ

まとめです。

  • ユーザーコントロールとカスタムコントロールに
    パフォーマンスの差があるか調べた
  • カスタムコントロールの構築は
    ユーザーコントロールの約5倍高速
  • InitializeComponentは速くなったが、
    レイアウト計算が遅くなり結果はトントン
  • レイアウト計算が走らないような配置なら
    利点がありそう

最後までお読みいただき、ありがとうございました。

関連記事

この記事を読んで、
カスタムコントロールとの比較をしてみたくなった方は、
【WPF】ユーザーコントロールをカスタムコントロールに変える手順
を参考にすれば既存ユーザーコントロールの移植ができます。
ぜひ、あなたの知見をいただければと思います。

コメント

タイトルとURLをコピーしました