Wednesday, December 18, 2013

Использование Stopwatch

В данной статье раскрываются возможные способы замера производительности выполнения кода, написанного на языке C#. Рассмотрим такой класс, как Stopwatch из пространства System.Diagnostics, а также способы его использования. Иногда при разработке ПО приходится писать одновременно несколько идентичных по действию, но различных по реализации функций. Задача состоит в выборе для последующей разработки функции, которая будет выполнятся быстрее остальных. В таком случае на помощь придёт класс Stopwatch. Данный класс позволяет замерить время выполнения конкретного участка кода. Для примера рассмотрим разные способы чтения с архива с помощью библиотеки SharpCompress. Пример основан на проблеме с распаковкой файлов с 7zip архива, с которым мы столкнулись в реальном проекте. 
class Program
{
    static void Main(string[] args)
    {
        var st = new Stopwatch();
        st.Start();
        TestExtractArchiveFactory();
        st.Stop();
        var resultArchiveFactory = st.Elapsed.TotalSeconds;

        st.Restart();
        TestExtractZipArchive();
        st.Stop();
        var resultSevenZipArchive = st.Elapsed.TotalSeconds;

        Console.WriteLine("Использование ArchiveFactory {0} сек", resultArchiveFactory);
        Console.WriteLine("Использование SevenZipArchive {0} сек", resultSevenZipArchive);
        Console.ReadLine();
    }

    public static void TestExtractZipArchive()
    {
        try
        {
            var path = Path.Combine(Directory.GetCurrentDirectory(), "IocContainersDemo.7z");
            var extractDirectory = CreteTempDirectory();
            using (Stream stream = File.OpenRead(path))
            using (var archive = SevenZipArchive.Open(stream))
            {
                var reader = archive.ExtractAllEntries();
                while (reader.MoveToNextEntry())
                {
                    if (!reader.Entry.IsDirectory)
                    {
                        reader.WriteEntryToDirectory(extractDirectory, ExtractOptions.ExtractFullPath | ExtractOptions.Overwrite);
                    }
                }
            }
        }
        finally
        {
            DeleteTempDirectory();
        }
    }

    public static void TestExtractArchiveFactory()
    {
        try
        {
            var path = Path.Combine(Directory.GetCurrentDirectory(), "IocContainersDemo.7z");
            var extractDirectory = CreteTempDirectory();
            using (Stream stream = File.OpenRead(path))
            using (var archive = ArchiveFactory.Open(stream))
            {
                using (var reader = archive.ExtractAllEntries())
                {
                    while (reader.MoveToNextEntry())
                    {
                        if (!reader.Entry.IsDirectory)
                        {
                            reader.WriteEntryToDirectory(extractDirectory, ExtractOptions.ExtractFullPath | ExtractOptions.Overwrite);
                        }
                    }
                }
            }
        }
        finally
        {
            DeleteTempDirectory();
        }
           
    }

    public static string CreteTempDirectory()
    {
        var path = Path.Combine(Directory.GetCurrentDirectory(), "TestArchives");
        if (!Directory.Exists(path))
            Directory.CreateDirectory(path);

        return path;
    }

    public static void DeleteTempDirectory()
    {
        var path = Path.Combine(Directory.GetCurrentDirectory(), "TestArchives");
        if (Directory.Exists(path))
            Directory.Delete(path, true);
    }
}
Основная работа с классом Stopwatch основывается на методах Start() и Stop(). Для простых примеров такой подход с созданием одного объекта Stopwatch; использование его в одной функции и вызов разных функций из этой одной функции является непрактичным. Поэтому рекомендуется написать класс-обертку, который будет иметь одну функцию и будет использован для замера времени выполнения кода. Этот класс может быть очень простым, как, например, приведенный ниже.
public static class StopwatchHelper
{
    public static double ProfileMethod(Action action)
    {
        var st = new Stopwatch();
        st.Start();

        action();

        st.Stop();
        return st.Elapsed.TotalSeconds;
    }
}
После этого исходный пример немного изменится.
static void Main(string[] args)
{
    var resultArchiveFactory = StopwatchHelper.ProfileMethod(() =>
        {
            TestExtractArchiveFactory();
        });
    var resultSevenZipArchive = StopwatchHelper.ProfileMethod(() =>
        {
            TestExtractZipArchive();
        });

    Console.WriteLine("Использование ArchiveFactory {0} сек", resultArchiveFactory);
    Console.WriteLine("Использование SevenZipArchive {0} сек", resultSevenZipArchive);
    Console.ReadLine();
}
Как видите, код стал намного проще. Такой подход с использованием объекта-обертки над функциями или кодом является популярным подходом при разработке ПО. 
Рассмотрим еще один подход для использования класса Stopwatch, который основывается на Aspect Oriented Programming (AOP) с использованием PostSharp. Данный подход базируется на внедрении дополнительного кода в существующий IL код. Посмотрим, как можно использовать возможности PostSharp вместе с Stopwatch.
[Serializable]
[ProfilerAspect(AttributeExclude = true)]
public class ProfilerAspect : OnMethodBoundaryAspect
{
    public override void OnEntry(MethodExecutionArgs args)
    {
        args.MethodExecutionTag = Stopwatch.StartNew();
    }

    public override void OnExit(MethodExecutionArgs args)
    {
        var sw = (Stopwatch)args.MethodExecutionTag;
        sw.Stop();

        string output = string.Format("{0} Executed in {1} seconds",
                            args.Method.Name, sw.Elapsed.TotalSeconds);

        Console.WriteLine(output);
    }
}
Теперь достаточно пометить метод, который Вы хотите замерить. с помощью атрибута [ProfilerAspect].
[ProfilerAspect]
public static void TestExtractZipArchive()
{
    try
    {
        var path = Path.Combine(Directory.GetCurrentDirectory(), "IocContainersDemo.7z");
        var extractDirectory = CreteTempDirectory();
        using (Stream stream = File.OpenRead(path))
        using (var archive = SevenZipArchive.Open(stream))
        {
            var reader = archive.ExtractAllEntries();
            while (reader.MoveToNextEntry())
            {
                if (!reader.Entry.IsDirectory)
                {
                    reader.WriteEntryToDirectory(extractDirectory, ExtractOptions.ExtractFullPath | ExtractOptions.Overwrite);
                }
            }
        }
    }
    finally
    {
        DeleteTempDirectory();
    }
}
Для примера был сделан вывод в консольное окно. Вы можете переписать метод ProfilerAspect так, чтобы вывод происходил в Output Window, использовав вместо метода Console.WriteLine метод Debug.WriteLine. 

Итоги

В статье  рассмотрен подход для замера скорости выполнения кода с помощью класса StopwatchВ программировании часто приходится сталкиваться с задачами, предполагающими выбор варианта кода, работающего быстрее. Для этого лучше быть знакомым со способами, которые для этого существуют, а также знать, как их можно применить на практике.   

No comments:

Post a Comment