2020년 11월 13일 금요일

C# List.Add()의 성능

 

C#에서 List<>의 성능은 어떨까 확인해봤습니다.

특히 List.Add에 관심이 좀 있는데요, 네트워크에서 쏟아지는 메시지를 저장하기 위해 List<>를 써볼까 생각하고 있기 때문입니다.

보통 List 타입은 객체의 레퍼런스를 저장하기 때문에  배열이 커지더라도 필요한 메모리가 비교적 작기 때문에 재할당에 걸리는 시간도 큰 문제가 되지는 않을 듯 합니다.

그래서 얼마나 나오는지 실제로 측정해보았습니다.

 

코드는 이렇습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
        void RunTest()
        {
            List<TestObject> list = new List<TestObject>();
            List<long> ticks = new List<long>();
 
            Debug.WriteLine($"Freq: {Stopwatch.Frequency}");
 
            Stopwatch w = new Stopwatch();
            w.Start();
 
            int count = 10000000;
            for (int i = 0; i < count; i++)
            {
                list.Add(new TestObject());
                ticks.Add(w.ElapsedTicks);
            }
 
            int file_count = count / 1000000;
            for (int i = 0; i < file_count; i++)
            {
                using (StreamWriter f = new StreamWriter($"out-{i}.csv"))
                {
                    for int j = 0; j < 1000000; j ++)
                    {
                        int index = i * 1000000 + j;
                        long l = ticks[index];
                        f.WriteLine($"{index},{l}");
                    }
                }
            }
        }

 

천만개의 객체를 생성해서 List에 추가할 때 걸리는 시간을 측정했습니다.

 

처음 백만개의 샘플을 추가할 때, 각각의 샘플을 추가하는데 걸리는 시간은 다음과 같습니다.

 

First 1M Samples.png

 

마지막 백만개의 샘플을 추가할 때 걸리는 시간은 다음과 같습니다.

 

Last 1M Samples.png

 

일반적으로는 0에 가까운 시간이 걸립니다만, 주기적으로 값이 크게 튀는 것이 보입니다.

List<>가 메모리를 재할당할 때마다 이렇게 튀는 것인데요, 주기가 동일한 것을 보면 List<>는 메모리의 크기를 일정한 단위로 늘이는 것으로 보입니다.

다른 프로그래밍 언어에서는 자주 커지는 리스트일 수록 점점 크게 잡는 점에 비하면 좀 흥미있는 동작이네요.

 

실제로 메모리를 할당하는 시간은 1~2ms 정도로 나타납니다. 천만개인 경우가 더 시간이 적게 걸리는 것은 JIT 최적화때문이 아닐까 싶습니다.

이렇게 보면 어떤 List<>를 사용할 때 Add() 함수가 갑자기 1~2ms 정도를 잡아먹을 수 있다는 것을 확인할 수 있습니다.

1~2ms가 중요한 실시간 Application에서는 충분히 유념해야 할 동작 특성입니다.

 

하지만 일반적인 Application에서는 별 문제가 될 정도는 아니겠네요.

일단은 별 생각 없이 써도 될 것 같습니다.

 

 

SharpPcap.MacAddress 에러 문제

 

얼마전부터 SharpPcap.MacAddress에 접근하면 예외가 발생하는 버그가 있었습니다.

github에서는 해결됐다고 하는데, nuget에 올라와있는 최신 버전인 5.3.0.0에서는 여전히 예외가 발생하는 것 같습니다.

제대로 해결될 때 까지, 간단히 사용할 수 있는 workaround로...

 

namespace SharpPcap
{
    public static class SharpPcapCaptureDeviceExtension
    {
        public static PhysicalAddress GetMacAddress(this ICaptureDevice device)
        {
            try
            {
                PhysicalAddress mac = device.MacAddress;
                if (mac == null)
                {
                    return SearchPhysicalAddress(device);
                }
                return mac;
            }
            catch
            {
                return SearchPhysicalAddress(device);
            }
        }

        public static PhysicalAddress SearchPhysicalAddress(ICaptureDevice device)
        {
            string name = device.Name;
            string uuid = name.Substring(12);

            NetworkInterface[] ifs = NetworkInterface.GetAllNetworkInterfaces();
            foreach (var i in ifs)
            {
                if (i.Id.ToString() == uuid)
                {
                    return i.GetPhysicalAddress();
                }
            }

            return null;
        }
    }
}

capture_device.MacAddress를 직접 사용하지 마시고, capture_device.GetMacAddress()를 사용하시면 됩니다.

 

5.3버전 부터는 안난다고 하는데, 왜인지 모르지만 저는 계속 에러가... ㅡ,.ㅡ;

 

C# enumearting Dictionary

 

이전 글에 이어서 오늘도 forech 구문...

 

이번에는 Dictionary  대상입니다.

 

저만 그런지 모르겠지만 Dictionary에 있는 항목을 foreach로 돌릴때는 다음과 같이 썼습니다.

 

Dictionary<string, string> TextByName = ...

 

foreach( var p in TextByName )

{

    string key = p.Key ;

    string value = p.Value ;

    Debug.WriteLine($"Text = {key}, Name = {value}");
}

 

그런데, 이게 귀찮아서 저번처럼 Enumerate를 쓰려고 작업을 하다가... 발견했습니다.

Dictionary는 그냥 되는 군요...

 

Dictionary<string, string> TextByName = ...

 

foreach((string text, string name) in TextByName)

{

    Debug.WriteLine($"Text = {text}, Name = {name}") ;

}

 

왜 되는걸까... 했는데요, .Net Core 2.0에서 되는 것 같습니다. dotNet framework에서는 컴파일 에러 납니다.

찾아봤습니다.

 

Deconstruct라는 놈이 나타납니다. 튜플( (a,b) 같은 놈들 )을 분해하는 놈들이라고 하네요. 다음 링크를 참고하시고...

https://docs.microsoft.com/ko-kr/dotnet/csharp/deconstruct

 

그래서 dotNet framework에서도 사용할 수 있도록 Dictionary<>의 Deconstruct를 만들어봅니다.

 

namespace System.Collections.Generic
{

    public static class EnumerableExtention
    {
        /// <summary>
        /// IEnumerable을 인덱스와 같이 열거한다.
        /// List<string> list = ...;
        /// foreach((int index, string value) in list) {}
        /// 와 같이 사용할 수 있다.
        /// </summary>
        /// <example>
        /// List<string> list = ...;
        /// foreach((int index, string value) in list)
        /// {
        ///     Debug.WriteLine($"index={index}, value={value}");
        /// }
        /// </example>
        /// <typeparam name="T"></typeparam>
        /// <param name="This"></param>
        /// <returns></returns>
        public static IEnumerable<(int, T)> Enumerate<T>(this IEnumerable<T> This)
        {
            int i = 0;
            foreach (T t in This)
            {
                yield return (i++, t);
            }
        }

        /// <summary>
        /// Dictionary를 foreach loop에서 처리할 때 key와 value를 튜플로 분해할 수 있도록 해 준다.
        /// Dictionary<string,string> dit = ...;
        /// foreach((string key, string value) in dict) { ... }
        /// 와 같이 사용할 수 있도록 해 준다.
        /// </summary>
        /// <typeparam name="TKey"></typeparam>
        /// <typeparam name="TValue"></typeparam>
        /// <param name="This"></param>
        /// <param name="key"></param>
        /// <param name="value"></param>
        public static void Deconstruct<TKey, TValue>(this KeyValuePair<TKey, TValue> This, out TKey key, out TValue value)
        {
            key = This.Key;
            value = This.Value;
        }
    }

}

지난 번 코드에 추가했습니다.

namespace도 그냥 System.Collections.Generic으로 해서 List나 Dictionary를 사용할 때는 저절로 쓸 수 있게 되도록 수정했습니다.

테스트해보니까 .Net Core에서도 별다른 충돌은 안 나는 것 같습니다.