Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 2개 있습니다.)

WPF DataGrid의 데이터 바인딩 시 리플렉션의 부하는 어느 정도일까요?

최근에 아래와 같은 질문이 있었는데요.

컬럼이 많은 데이터그리드에서 정렬 할 때 속도가 느립니다.
; https://www.sysnet.pe.kr/3/0/3608
; https://www.sysnet.pe.kr/3/0/3609
; https://www.sysnet.pe.kr/3/0/3610

WPF의 경우 DataGrid에 바인딩된 요소를 접근하는 과정에 리플렉션을 사용하게 됩니다. (범용적인 데이터 바인딩이니 이는 어쩔 수 없는 부분입니다.) 이를 확인하는 방법은 해당 속성을 접근하는 get 코드에 BP를 걸고 Call Stack을 보면 됩니다.

datagrid_strong_type_access_1.png

모든 값을 리플렉션으로 접근하는 것은 느릴 수 있을텐데, 과연 얼마나 느릴까요? ^^ 궁금해졌습니다.

테스트를 쉽게 하기 위해 "컬럼이 많은 데이터그리드에서 정렬 할 때 속도가 느립니다." 글에 포함된 예제를 변형했는데요. 우선, xaml은 다음과 같이 구성하고,

<Window x:Class="DataGridTestProj.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:DataGridTestProj"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="50" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <Button Grid.Row="0" Click="Button_Click" />
        <DataGrid x:Name="dg" Grid.Row="1" AutoGenerateColumns="False" IsReadOnly="True"
                  VirtualizingPanel.IsVirtualizing="True" VirtualizingPanel.VirtualizationMode="Recycling"
                  EnableColumnVirtualization="True" EnableRowVirtualization="True">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Text1"  Binding="{Binding Text1}" Width="150"/>
                ...[2~32까지 생략]...
                <DataGridTextColumn Header="Text33" Binding="{Binding Text33}" Width="150"/>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>

xaml.cs는 Button_Click 시 정렬하는 코드와 렌더링 부하 측정 코드를 추가했습니다.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Windows;
using System.Windows.Threading;

namespace DataGridTestProj
{
    public partial class MainWindow : Window
    {
        List<Item> list;
        bool reverse = false;

        public MainWindow()
        {
            InitializeComponent();

            list = MakeList();
            dg.ItemsSource = list;
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            reverse = !reverse;

            dg.ItemsSource = null;

            list.Sort((e1, e2) => e1.Text1.CompareTo(e2.Text1) * ((reverse == true) ? 1 : -1));

            dg.ItemsSource = list;

            Stopwatch st = new Stopwatch();
            st.Start();
            Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action<object>(CheckTime), st);
        }

        private void CheckTime(object value)
        {
            Stopwatch st = value as Stopwatch;
            st.Stop();

            System.Diagnostics.Trace.WriteLine("DataGrid.RenderTime: " + st.ElapsedMilliseconds);
        }

        public List<Item> MakeList()
        {
            Random rnd = new Random();

            List<Item> list = new List<Item>();

            for (int i = 0; i < 1133; i++)
            {
                list.Add(
                    new Item(
                    rnd.Next(1, 13232323).ToString(),
                    // ...[생략]...
                    rnd.Next(1, 13232323).ToString()
                    )
                );
            }

            return list;
        }
    }
}

public class Item
{
    public Item(String Text1 = "", 
                //...[생략]... 
                String Text33 = "")
    {
        this.Text1 = Text1;
        //...[생략]...
        this.Text33 = Text33;
    }

    public String Text1 { get; set; }
    //...[생략]...
    public String Text33 { get; set; }
}

이렇게 하고 윈도우를 최대화시켜 테스트 해보면, 제 컴퓨터에서 Button을 누를 때 마다 Output 창에 다음과 같은 결과를 볼 수 있었습니다.

DataGrid.RenderTime: 365
DataGrid.RenderTime: 356
DataGrid.RenderTime: 365
DataGrid.RenderTime: 369
DataGrid.RenderTime: 340

이제 같은 예제를 리플렉션이 아닌 strong 타입으로 접근하도록 바꿔야 하는데요. 이것이 가능하려면 DataGrid의 Columns에 정의된 DataGridTextColumn을 사용자 정의 타입으로 바꿔야 합니다.

<Window x:Class="DataGridTestProj.MainWindow"
        xmlns:local="clr-namespace:DataGridTestProj"
        ...[생략]...>
    <Grid>
        ...[생략]...
        <DataGrid x:Name="dg" ...[생략]...>
            
            <DataGrid.Columns>
                <local:myDataGridTextColumn Header="Text1" Field="Text1"  Width="150"/>
                ...[생략]...
                <local:myDataGridTextColumn Header="Text33" Field="Text33"     Width="150"/>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>

당연히 myDataGridTextColumn 클래스를 구현해야겠지요. ^^

public class myDataGridTextColumn : DataGridTextColumn
{
    public static readonly DependencyProperty FieldProperty =
            DependencyProperty.Register("Field", typeof(string),
            typeof(myDataGridTextColumn), new FrameworkPropertyMetadata(null));

    public string Field
    {
        get { return (string)GetValue(FieldProperty); }
        set { SetValue(FieldProperty, value); }
    }

    protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
    {
        FrameworkElement fe = base.GenerateElement(cell, dataItem);

        TextBlock tb = fe as TextBlock;

        string fieldValue = null;
        Item item = (dataItem as Item);
        switch (Field)
        {
            case "Text1":
                fieldValue = item.Text1;
                break;

            // ...[case 문 생략]....

            case "Text33":
                fieldValue = item.Text33;
                break;
        }

        tb.Text = fieldValue;

        return fe;
    }
}

GenerateElement 메서드를 재정의해 object로 넘어온 Item 타입 인스턴스의 내부 필드를 리플렉션이 아닌 직접 접근하는 방식으로 바꿀 수 있습니다. 이렇게 하고 실행했더니... Button을 누를 때마다 다음과 같은 결과를 보였습니다.

DataGrid.RenderTime: 339
DataGrid.RenderTime: 310
DataGrid.RenderTime: 332
DataGrid.RenderTime: 309
DataGrid.RenderTime: 316

350ms 대에서 310ms 수준으로 떨어졌는데 미세하게 빨라졌다는 느낌을 받게 됩니다. 그렇지만, 예상과는 달리 리플렉션이 과히 무거운 수준은 아님을 알 수 있습니다. 사실, 리플렉션을 사용해도 MethodInfo에 대한 cache나 LCG(Lightweight Code Generator)같은 것을 곁들이면 일반 메서드 호출과 거의 차이가 없기 때문에 마이크로소프트에서 최적화를 잘 했던 것으로 보입니다.




그럼 과연, 어디서 렌더링 타임이 잡아먹는 걸까요?

이런 경우, 의심할만한 요소라면 MeasureOverride, LayoutOverride 정도가 될 것입니다. 이를 확인하기 위해 DataGrid 내의 VirtualizingStackPanel을 교체해 볼 수 있습니다.

<DataGrid x:Name="itemGridView" Grid.Row="1" ItemsSource="{Binding list}" AutoGenerateColumns="False" 
                    VirtualizingPanel.IsVirtualizing="True"
                    VirtualizingPanel.VirtualizationMode="Recycling"
            >
    <DataGrid.ItemsPanel>
        <ItemsPanelTemplate>
            <local:myVirtualizingStackPanel />
        </ItemsPanelTemplate>
    </DataGrid.ItemsPanel>

    ...[생략]...
</DataGrid>

public class myVirtualizingStackPanel : VirtualizingStackPanel
{
    protected override Size MeasureOverride(Size availableSize)
    {
        Stopwatch st = new Stopwatch();
        st.Start();
        Size size = base.MeasureOverride(availableSize);
        st.Stop();

        System.Diagnostics.Trace.WriteLine("myVirtualizingStackPanel.MeasureOverride: " + st.ElapsedMilliseconds);
        return size;
    }

    protected override Size ArrangeOverride(Size arrangeBounds)
    {
        Stopwatch st = new Stopwatch();
        st.Start();
        Size size = base.ArrangeOverride(arrangeBounds);
        st.Stop();

        System.Diagnostics.Trace.WriteLine("myVirtualizingStackPanel.ArrangeOverride: " + st.ElapsedMilliseconds);
        return size;
    }
}

이렇게 교체하고 실행한 결과는 다음과 같습니다.

myVirtualizingStackPanel.MeasureOverride: 427
myVirtualizingStackPanel.ArrangeOverride: 164
DataGrid.RenderTime: 660

427 + 164 == 591이니까 DataGrid의 전체 렌더링 시간의 주요 부하로 보입니다. 그런데, 이상하군요. 왜 300 수준에서 600 수준으로 늘어난 것일까요? 그것은 DataGrid 스스로 내부 VirtualizingStackPanel을 사용하면서 EnableColumnVirtualization, EnableRowVirtualization 속성을 통해 Column과 Row에까지 가상화를 하는 최적화를 수행하지만, 사용자가 설정한 VirtualizingStackPanel에는 이 역할을 수행하지 않기 때문입니다. 즉, 이전에는 나름대로의 최적화를 좀 더 수행할 수 있었던 것입니다. (참고로, 사용자 정의 VirtualizingStackPanel을 지정한 상태에서 EnableColumnVirtualization, EnableRowVirtualization 속성을 True로 설정하면 UI가 엉망으로 나옵니다. 이거 정상으로 나오게 하는 방법이 있을까요? ^^)




결론을 내리면, 행/열이 많은 DataGrid의 렌더링을 최적화하려면 내부 StackPanel에 대한 MeasureOverride, ArrangeOverride를 개선한 사용자 정의 패널을 만들어야 합니다. 가령, 행/열의 변화가 없다면 별도로 Measure/Arrange 작업을 할 필요없이 곧바로 현재 보유중인 FrameworkElement의 텍스트만 교체하는 코드가 동작하도록 해야겠지요. 아마 작업이 쉽진 않을 것입니다.

DataGrid를 꼭 써야 하는 것이 아니라면, 이런 경우 차라리 ListView를 선택하는 것이 더 좋은 방법일 수 있습니다.

<ListView Name="dg" Grid.Row="1" VirtualizingPanel.IsVirtualizing="True" VirtualizingPanel.VirtualizationMode="Recycling">
    <ListView.View>
        <GridView>
            <GridViewColumn  Header="Text1"  DisplayMemberBinding="{Binding Text1}" Width="150"/>
            ...[생략]...
        </GridView>
    </ListView.View>
</ListView>

그럼, 리플렉션을 이용한 속성 접근임에도 불구하고 280대로 렌더링 시간이 내려가서 체감상 거의 느리다는 느낌이 들지 않습니다.

ListView.RenderTime: 286
ListView.RenderTime: 290
ListView.RenderTime: 287
ListView.RenderTime: 296
ListView.RenderTime: 284

물론, DataGrid에서 기본 제공하는 header를 눌러 정렬하는 기능이 제공되지 않지만 이것은 다음의 글을 보고 구현해 주면 됩니다.

How-to: ListView with column sorting
; http://www.wpf-tutorial.com/listview-control/listview-how-to-column-sorting/

(첨부한 파일은 위의 예제 코드를 포함합니다.)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 7/10/2021]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 16  17  18  [19]  20  21  22  23  24  25  26  27  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
13156정성태11/7/20226806.NET Framework: 2064. C# - Mutex와 Semaphore/SemaphoreSlim 차이점파일 다운로드1
13155정성태11/4/20226306디버깅 기술: 183. TCP 동시 접속 (연결이 아닌) 시도를 1개로 제한한 서버
13154정성태11/3/20225780.NET Framework: 2063. .NET 5+부터 지원되는 GC.GetGCMemoryInfo파일 다운로드1
13153정성태11/2/20227072.NET Framework: 2062. C# - 코드로 재현하는 소켓 상태(SYN_SENT, SYN_RECV)
13152정성태11/1/20225692.NET Framework: 2061. ASP.NET Core - DI로 추가한 클래스의 초기화 방법 [1]
13151정성태10/31/20225792C/C++: 161. Windows 11 환경에서 raw socket 테스트하는 방법파일 다운로드1
13150정성태10/30/20225856C/C++: 160. Visual Studio 2022로 빌드한 C++ 프로그램을 위한 다른 PC에서 실행하는 방법
13149정성태10/27/20225775오류 유형: 825. C# - CLR ETW 이벤트 수신이 GCHeapStats_V1/V2에 대해 안 되는 문제파일 다운로드1
13148정성태10/26/20225783오류 유형: 824. msbuild 에러 - error NETSDK1005: Assets file '...\project.assets.json' doesn't have a target for 'net5.0'. Ensure that restore has run and that you have included 'net5.0' in the TargetFramew
13147정성태10/25/20224873오류 유형: 823. Visual Studio 2022 - Unable to attach to CoreCLR. The debugger's protocol is incompatible with the debuggee.
13146정성태10/24/20225710.NET Framework: 2060. C# - Java의 Xmx와 유사한 힙 메모리 최댓값 제어 옵션 HeapHardLimit
13145정성태10/21/20225997오류 유형: 822. db2 - Password validation for user db2inst1 failed with rc = -2146500508
13144정성태10/20/20225812.NET Framework: 2059. ClrMD를 이용해 윈도우 환경의 메모리 덤프로부터 닷넷 모듈을 추출하는 방법파일 다운로드1
13143정성태10/19/20226355오류 유형: 821. windbg/sos - Error code - 0x000021BE
13142정성태10/18/20225170도서: 시작하세요! C# 12 프로그래밍
13141정성태10/17/20226836.NET Framework: 2058. [in,out] 배열을 C#에서 C/C++로 넘기는 방법 - 세 번째 이야기파일 다운로드1
13140정성태10/11/20226215C/C++: 159. C/C++ - 리눅스 환경에서 u16string 문자열을 출력하는 방법 [2]
13139정성태10/9/20226023.NET Framework: 2057. 리눅스 환경의 .NET Core 3/5+ 메모리 덤프로부터 모든 닷넷 모듈을 추출하는 방법파일 다운로드1
13138정성태10/8/20227339.NET Framework: 2056. C# - await 비동기 호출을 기대한 메서드가 동기로 호출되었을 때의 부작용 [1]
13137정성태10/8/20225684.NET Framework: 2055. 리눅스 환경의 .NET Core 3/5+ 메모리 덤프로부터 닷넷 모듈을 추출하는 방법
13136정성태10/7/20226262.NET Framework: 2054. .NET Core/5+ SDK 설치 없이 dotnet-dump 사용하는 방법
13135정성태10/5/20226502.NET Framework: 2053. 리눅스 환경의 .NET Core 3/5+ 메모리 덤프를 분석하는 방법 - 두 번째 이야기
13134정성태10/4/20225235오류 유형: 820. There is a problem with AMD Radeon RX 5600 XT device. For more information, search for 'graphics device driver error code 31'
13133정성태10/4/20225560Windows: 211. Windows - (commit이 아닌) reserved 메모리 사용량 확인 방법 [1]
13132정성태10/3/20225454스크립트: 42. 파이썬 - latexify-py 패키지 소개 - 함수를 mathjax 식으로 표현
13131정성태10/3/20228173.NET Framework: 2052. C# - Windows Forms의 데이터 바인딩 지원(DataBinding, DataSource) [2]파일 다운로드1
... 16  17  18  [19]  20  21  22  23  24  25  26  27  28  29  30  ...