본문 바로가기
Utils

C#, 커스텀 밴치마크 라이브러리 (함수 테스트 도구)

by Client. DJ 2022. 1. 16.
반응형

본문 예제 코드 결과

간혹 작업 중, '어떤 방식이 효율이 있는지?' 또는 '구글링이나 공부를 통해서 알게된 부분들이 실제로 속도가 어떤지?'가 궁금할 때가 있습니다. 간단하게 코드를 만들어서 테스트하고 싶지만, 매번 새로운 프로젝트를 따로 만들거나 해야하는 번거로움이 있습니다.

위와 같은 고충을 해결하기 위해, 간단하게 작은 라이브러리를 만들었습니다. 기존 MS에서도 지원하는 Benchmark를 모방한 커스텀 밴치마크 스크립트입니다. 굳이 모방을 한 이유는 라이브러리 따로 추가를 해야하고, 릴리즈 모드에서만 작동되고 하는 번거로움이 있으며, 간단하게 실행속도 및 빠른지의 비교에 관련된 부분이 미흡하여 따로 작성하였습니다.

 

밴치마크

Benchmark.cs 스크립트

더보기
더보기
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;

public class Benchmark
{
    public delegate object Method();
    private class ExecuteRecord
    {
        public static int methodLength = 15;
        public static int secondsLength = 15;
        public static int rankLength = 5;
        public static int memoryLength = 10;

        public string methodName;
        public double ticks;
        public short rank;
        public long memory;
        public string message;

        public bool IsError => !string.IsNullOrEmpty(message);

        public string GetSeconds()
        {
            return $"{1000 * 1000 * 1000 * ticks / Stopwatch.Frequency:F3} ns";
        }

        public string GetMemory()
        {
            int mok = 0;
            long tempMem = memory < 8192 ? 8192 : memory;
            while (tempMem > 1024)
            {
                tempMem /= 1024;
                mok++;
            }
            switch (mok)
            {
                case 0:
                    return $"{tempMem} B";
                case 1:
                    return $"{tempMem} KB";
                case 2:
                    return $"{tempMem} MB";
                case 3:
                    return $"{tempMem} GB";
                case 4:
                    return $"{tempMem} TB";
                default:
                    return $"{memory} B";
            }
        }

        public static void Clear()
        {
            methodLength = 15;
            secondsLength = 15;
            rankLength   = 5;
            memoryLength = 10;
        }
    }

    private static List<ExecuteRecord> _executeRecords = new List<ExecuteRecord>();

    /// <summary>
    /// 밴치마크 시작 및 기록
    /// </summary>
    /// <param name="method"></param>
    /// <returns></returns>
    public static string Start(Action method)
    {
        return _start(method.Method.Name, method);
    }

    ///// <summary>
    ///// 밴치마크 시작 및 기록 (.Net Core 3.1에서는 주석 풀어도 이상 없음)
    ///// </summary>
    ///// <param name="method"></param>
    ///// <returns></returns>
    //public static string Start(Method method)
    //{
    //    return _start(method.Method.Name, () => method());
    //}

    /// <summary>
    /// 메소드 실행 기록 가져오기
    /// </summary>
    /// <returns></returns>
    public static string GetRecord()
    {
        StringBuilder sb = new StringBuilder(1024);

        // Title
        sb.Append("\n[Benchmark Record]\n\n");
        sb.Append("Method".PadLeft(ExecuteRecord.methodLength)).Append(" |").Append("Run-time".PadLeft(ExecuteRecord.secondsLength)).Append(" |").Append("Memory".PadLeft(ExecuteRecord.memoryLength)).Append(" |").Append("Rank".PadLeft(ExecuteRecord.rankLength)).Append(" |").Append('\n');
        for (int i = 0; i < ExecuteRecord.methodLength; i++)
        {
            sb.Append('=');
        }
        sb.Append(" |");
        for (int i = 0; i < ExecuteRecord.secondsLength; i++)
        {
            sb.Append('=');
        }
        sb.Append(":|");
        for (int i = 0; i < ExecuteRecord.memoryLength; i++)
        {
            sb.Append('=');
        }
        sb.Append(":|");
        for (int i = 0; i < ExecuteRecord.rankLength; i++)
        {
            sb.Append('=');
        }
        sb.Append(":|");
        sb.Append('\n');

        // Rank calculation
        for (int i = 0; i < _executeRecords.Count; i++)
        {
            _executeRecords[i].rank = 1;
            for (int j = 0; j < _executeRecords.Count; j++)
            {
                if (_executeRecords[i].ticks > _executeRecords[j].ticks)
                    _executeRecords[i].rank++;
            }
        }

        for (int i = 0; i < _executeRecords.Count; i++)
        {
            if (_executeRecords[i].IsError)
            {
                for (int j = 0; j < _executeRecords.Count; j++)
                {
                    _executeRecords[j].rank--;
                }
                _executeRecords[i].rank = -1;
            }
        }

        // Contents
        for (int i = 0; i < _executeRecords.Count; i++)
        {
            if (_executeRecords[i].IsError)
            {
                sb.Append(_executeRecords[i].methodName.PadLeft(ExecuteRecord.methodLength)).Append(" |").Append(_executeRecords[i].message);
            }
            else
            {
                sb.Append(_executeRecords[i].methodName.PadLeft(ExecuteRecord.methodLength)).Append(" |").Append(_executeRecords[i].GetSeconds().PadLeft(ExecuteRecord.secondsLength)).Append(" |").Append($"{_executeRecords[i].GetMemory()}".PadLeft(ExecuteRecord.memoryLength)).Append(" |").Append(_executeRecords[i].rank.ToString().PadLeft(ExecuteRecord.rankLength)).Append(" |");
                if (_executeRecords[i].rank == 1)
                    sb.Append(" << Best!");
            }
            sb.Append('\n');
        }

        // Desciption
        sb.Append("\n * Legends * " +
            "\n - Method: Method name." +
            "\n - Result: Executed result content." +
            "\n - Rank: Rank from all method. " +
            "\n");

        // Return Record
        return sb.ToString();
    }

    /// <summary>
    /// 밴치마크 기록 초기화
    /// </summary>
    public static void Clear()
    {
        _executeRecords.Clear();
        ExecuteRecord.Clear();
    }

    #region internal Method

    private static Stopwatch _stopwatch = null;
    private static int MAX_LOOP_PROCCESS_COUNT = 50;

    static Benchmark()
    {
        _initialize();
    }

    private static void _initialize()
    {
        _stopwatch = Stopwatch.StartNew();
        Clear();
    }

    private static string _start(string methodName, Action method, int testCnt = 100000)
    {
        // Ready
        string message = string.Empty;
        bool isSuccess = true;

        // Start

        // 1. 함수 반복 실행으로 속도가 안정화될 때까지 반복
        double currentSeconds = 0;
        for (int i = 0; i < MAX_LOOP_PROCCESS_COUNT; i++)
        {
            _stopwatch.Restart();
            try
            {
                method.Invoke(); //method?.Invoke();
            }
            catch (Exception e)
            {
                message = "[Error] " + e.Message;
                isSuccess = false;
                break;
            }
            _stopwatch.Stop();

            double prevSeconds = currentSeconds;
            currentSeconds = _stopwatch.ElapsedTicks;

            // 오차 범위가 좁혀졌을때 반복 중지
            if (Math.Abs(prevSeconds - currentSeconds) < 10)
                break;
        }

        // 2. 함수 반복 실행으로 평균값 산정
        double ticks = 0f;
        long memory = 0;
        if (isSuccess)
        {
            for (int i = 0; i < testCnt; i++)
            {
                _stopwatch.Restart();
                method.Invoke();
                _stopwatch.Stop();
                ticks += _stopwatch.ElapsedTicks;
            }

            ticks = ticks / testCnt;

            GC.Collect();
            long beforeMem = GC.GetTotalMemory(false);
            method.Invoke();
            long afterMem = GC.GetTotalMemory(false);
            memory = afterMem - beforeMem;
        }

        // Caching Result Row
        _addRecord(methodName, ticks, memory, message);

        return message;
    }

    private static void _addRecord(string methodName, double ticks, long memory, string message = "")
    {
        if (ExecuteRecord.methodLength < methodName.Length)
            ExecuteRecord.methodLength = methodName.Length + 5;

        _executeRecords.Add(new ExecuteRecord { methodName = methodName, ticks = ticks, memory = memory, message = message });
    }

    #endregion
}

 

예제

다음과 같이 테스트 코드 여러개를 선언하고, Benchmark.Start()에 넣어줍니다. 함수를 호출 이후 걸리는 시간을 기록합니다. 반환값으로도 즉시 받을 수 있으며, 전체 기록을 확인하여 순위를 확인할 수 있습니다.

using System;

namespace ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Benchmark.Start(LoopTest1);
            Benchmark.Start(LoopTest2);
            Benchmark.Start(OutOfRangeException);
            Benchmark.Start(LoopTest3);
            Benchmark.Start(LoopTest4);
            Console.WriteLine(Benchmark.GetRecord());
        }

        static void OutOfRangeException()
        {
            int[] a = new int[1];
            a[2] = 20;
        }

        static void LoopTest1()
        {
            for (int i = 0; i < 1999; i++)
            {

            }
        }

        static void LoopTest2()
        {
            for (int i = 0; i < 9999; i++)
            {

            }
        }

        static void LoopTest3()
        {
            for (int i = 0; i < 999; i++)
            {

            }
        }

        static void LoopTest4()
        {
            for (int i = 0; i < 9999; i++)
            {

            }
        }
    }
}


위와 같이 실행하면 아래와 같은 결과가 나옵니다.

 [Benchmark Record] 
 
                   Method |       Run-time |    Memory | Rank |
 ======================== |===============:|==========:|=====:|
                LoopTest1 |    3076.890 ns |      8 KB |    2 |
                LoopTest2 |   15447.643 ns |      8 KB |    4 |
      OutOfRangeException |[Error] Index was outside the bounds of the array.
                LoopTest3 |    1552.600 ns |      8 KB |    1 | << Best!
                LoopTest4 |   15243.588 ns |      8 KB |    3 |

 * Legends *
 - Method: Method name.
 - Result: Executed result content.
 - Rank: Rank from all method.

좀 더 가독성있고, 쉽게 구분할 수 있으며, 간단하게 함수를 만들어서 테스트할 수 있습니다. :)
쓰레드로 따로 결과를 가져오고 싶다면, 적당한 타이밍에 Benchmark.GetRecord()로 캐싱된 string을 읽어오시면 됩니다.

반응형

댓글