Наверняка многим из вас доводилось видеть подборку Best Practices на языке C#, где тот или иной разработчик-эксперт советует применить новую технику.
Часто в таких случаях используется принцип проектирования KISS (Keep It Simple, Stupid - "Делай проще, тупица"). Данный принцип был принят в ВМС США ещё в 1960 году. Принцип KISS утверждает, что большинство систем работают лучше всего, если они остаются простыми, а не усложняются. Поэтому в области проектирования простота должна быть одной из ключевых целей и следует избегать ненужной сложности. Фраза ассоциировалась с авиаконструктором Кларенсом Джонсоном (1910—1990)[3]. В 1970-х годах широко использовался термин «KISS-принцип» (англ. KISS principle)[4]. Вариации на фразу включают (обычно как эвфемизм для более грубого «stupid»): «keep it super simple», «keep it simple, silly», «keep it short and simple», «keep it short and sweet», «keep it simple and straightforward»[5], «keep it small and simple»[6], «keep it simple, soldier», «keep it simple, sailor», «keep it simple, sweetie»[7], «keep it stupidly simple» и «keep it sweet and simple».
Но мы немного отвлеклись от темы, нужно разобрать конкретный кейс:
Может показаться, что во 2-ом случае код стал меньше, значит понятнее. Однако, это не так, ведь код стал чуть менее читабельным. Т.к. теперь приходится "в голове" коппилировать результат исполнения метода Reverse, потом ToArray() и заключительной аллокации памяти из конструктора new string.
С аллокациями разобрались, что во 2-ом случае их стало больше. А что насчёт быстродействия?
Пока сложно ответить на этот вопрос, поэтому подготовим тестовый проект с подключенной библиотекой BenchmarkDotNet и воссоздадим условия для тестирования:
Далее откроем созданный проект в Visual Studio (или Rider) и установим пакет BenchmarkDotNet через Package Manager Console:
PM> Nuget\Install-Package BenchmarkDotNet
Теперь поставим шаблон для работы с новой библиотекой:
Также в проекте создадим класс Benchmarks, который будет содержать методы для замера производительности и выделения памяти:
В качестве тестовых данных выступают фамилия и имя автора, метод Reverse_BadExample - это "плохой" пример написания, а Reverse_CleanCode - это пример "чистого кода".
Также не забудем в главном классе программы, в методе Main прописать следующий код для запуска бенчмарков:
Получаем следующие результаты:
Высказанные выше догадки подтвердились. 2-ая версия метода, основанная на принципе KISS провалилась как по "сложности восприятия", по времени исполнения, так и по аллокациям. Значит, совет оказался действительно не таким уж и действенным. Чистый код - не всегда быстрый код. Особенно когда дело касается строк.
Что ж, попробуем улучшить 2-ую версию метода. Для этого вызовем у исходной строки тот же метод Reverse, получим IEnumerable<char>, но не будем преобразовывать его в массив. Сразу передадим в метод, который принимает такой тип данных. И это string.Concat, у которого есть аж 13 перегрузок. Получится следующий метод, назовем его ReverseString_Concat:
Как можно заметить - выделения памяти в 3-ем методе действительно стало меньше, чем у 2-ой версии. Также ускорилось и время исполнения. Однако это всё ещё хуже 1-го варианта.
В таком случае обратимся к официальной документации по классу string и увидим, что есть метод создания строки Create, который принимает на вход длину создаваемой строки, состояние и делегат-колбэк, который будет оперировать буфером-структурой и состоянием. Документация по методу: string.Create<TState>(Int32, TState, SpanAction<Char,TState>)
Так как нам необходимо произвести перестановку символов, то длина целевой строки известна и равна исходной. В качестве начального состояния передадим значение поля InputText. А делегат будет исходное состояние строки копировать в буфер chars, после чего этот буфер можно отсортировать в обратном порядке. Должна получиться следующая реализация метода:
В результате реализация метода получилась длиннее, однако если взглянуть глобально, то вызывается всего лишь 1 метод string.Create.
Сразу же запускаем проект и смотрим результаты:
Результат превзошёл все ожидания! По выделению памяти добились отличных показателей, снизив их до минимума за счёт использования структуры Span, которая не выделяет дополнительной памяти при работе с данными. Также дополнительной памяти (если посмотреть реализацию) не выделяет и метод CopyTo, он лишь копирует ссылки на указатели без какого-либо копирования самих элементов.
Вывод: далеко не все советы по чистому коду полезны и их стоит брать на вооружение в свои проекты. Всё проверяйте и меньше ошибайтесь. Да прибудет с Вами сила!
Комментариев нет:
Отправить комментарий