Сетевой дневник одного программиста

Персональный блог Константина Огородова

BenchmarkDotNet

Возникла необходимость вспомнить старые добрые времена и решить «студенческую» задачку: выравнивание текста по ширине. Дабы привнести немного разнообразия решено было реализовать несколько алгоритмов. Первый попроще, через string.Split(' ', StringSplitOptions.RemoveEmptyEntries);. Второй максимально оптимальный. Третий — многопоточный. После того, как всё было сделано приступил к замерам производительности в консольке используя DateTime.Now.Ticks. В качестве тестовых данных был выбран первый том романа «Война и мир» Льва Николаевича Толстого. Всего 114 154 слов, 1,2МБ данных в кодировке UTF-8. Получил следующие результаты: попроще — 600K, оптимальный — 200K, многопоточный — 5500K. Но как выяснилось позже полученные цифры чуть менее чем полностью не соответствуют действительности, потому что DateTime.Now работает очень и очень медленно. Поэтому никогда, слышите, никогда-никогда не используйте DateTime.Now для замеров производительности. На платформе .net для этого есть специальный класс Stopwatch. Или ещё можно использовать Environment.TickCount — количество миллисекунд с момента загрузки системы. В этом случае получаем следующие результаты: попроще — 125, оптимальный — 15, многопоточный — 78. Теперь понятно почему нельзя использовать DataTime? Однако это ещё не всё: от теста к тесту результаты менялись порой значительно. Например иногда оптимальный = 30, а многопоточный = 32. Такой разброс меня не устраивал, и решено было познакомиться с BenchmarkDotNet, тем более, что желание было давно, а сейчас подвернулся удобный случай. На обзорной странице BenchmarkDotNet имеется простенький пример использования в консольном приложении. Тесты производительности должны быть запущены в режиме Release, так что мне показалось удобным использовать препроцессорные директивы для разделения непосредственно тестов и режима отладки:

#if DEBUG
            // Ручное тестирование в режиме отладки
#else
            var summary = BenchmarkRunner.Run(typeof(Program).Assembly);
#endif

После запуска приложения, согласно описанию на странице How it works, BenchmarkDotNet будет выполнять предварительный «прогрев», а затем прогонять тесты несколько раз чтобы получить наиболее достоверные результаты. В результате получим вот это:

MethodMeanErrorStdDevRatioRatioSD
Simple42.545 ms0.8496 ms1.5958 ms5.330.23
SingleThread7.901 ms0.1555 ms0.2279 ms1.000.00
MultyThread19.984 ms0.1988 ms0.4878 ms1.250.08

Благодаря оптимизациям режима Release производительность значительно выросла, а благодаря многократным запускам тестируемого метода отклонения в измерениях от раза к разу стали незначительны. Profit!
Но я на этом не остановился 🙂 Мне захотелось поиграться с многопоточным исполнением чтобы обойти однопоточный вариант. Было реализовано 4 варианта:

  • MultyThread1 — на основе потокобезопасной коллекции BlockingCollection. Анализ входной строки был в фоновом потоке, а сборка результата в текущем.
  • MultyThread2 — аналогично предыдущему, но анализ в текущем, а сборка в фоновом.
  • MultyThread3 — с использованием массива на 5 тысяч элементов в качестве промежуточного хранилища и ManualResetEventSlim для синхронизации потоков. Анализ в текущем потоке, сборка в фоновом.
  • MultyThread4 — аналогично предыдущему, но для синхронизации потоки «молотили» вхолостую используя SpinWait.
MethodMeanErrorStdDevRatioRatioSD
Simple41.251 ms0.8195 ms1.4985 ms5.240.21
SingleThread7.877 ms0.1530 ms0.2426 ms1.000.00
MultyThread19.618 ms0.1920 ms0.4413 ms1.210.05
MultyThread210.391 ms0.2061 ms0.5208 ms1.310.09
MultyThread38.112 ms0.1604 ms0.3555 ms1.030.05
MultyThread489.374 ms6.8980 ms20.3390 ms10.742.35

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

BenchmarkDotNet

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Пролистать наверх