Проектируйте с учётом возможности оптимизации
Причина
Потому что нам часто нужно оптимизировать начальный дизайн. Потому что дизайн, игнорирующий возможность последующего улучшения, сложно изменить.
Пример
Из стандарта C (и C++):
void qsort (void* base, size_t num, size_t size, int (*compar)(const void*, const void*));
Когда вам когда-либо требовалось сортировать сырую память? На самом деле мы сортируем последовательности элементов, как правило хранящихся в контейнерах. Вызов qsort выбрасывает много полезной информации (например, тип элемента), заставляет пользователя повторять уже известные данные (например, размер элемента) и писать дополнительный код (например, функцию сравнения double). Это влечёт лишнюю работу для программиста, чревато ошибками и лишает компилятор информации, необходимой для оптимизации.
double data[100];
// ... заполнение a ...
// 100 блоков памяти размером sizeof(double), начиная с
// адреса data, в порядке, определённом compare_doubles
qsort(data, 100, sizeof(double), compare_doubles);
С точки зрения проектирования интерфейса qsort выбрасывает полезную информацию.
Можно сделать лучше (в C++98):
template<typename Iter>
void sort(Iter b, Iter e); // сортировка [b:e)
sort(data, data + 100);
Здесь мы используем знание компилятора о размере массива, типе элементов и способе сравнения double.
В C++20 можно сделать ещё лучше:
// sortable указывает, что c должен быть
// последовательностью произвольного доступа, элементы которой сравнимы через <
void sort(sortable auto& c);
sort(c);
Ключевой момент — передавать достаточно информации для выбора хорошей реализации. При этом показанные интерфейсы sort всё ещё имеют слабое место: они неявно полагаются на то, что для типа элемента определён оператор меньше (<). Для полноты интерфейса нужна вторая версия, принимающая критерий сравнения:
// сравнение элементов c с помощью r
template<random_access_range R, class C> requires sortable<R, C>
void sort(R&& r, C c);
Спецификация sort в стандартной библиотеке предлагает обе версии и ещё больше.
Примечание
Говорят, что преждевременная оптимизация — корень всех зол, но это не повод пренебрегать производительностью. Никогда не рано подумать о том, как сделать дизайн открытым для улучшений; повышение производительности — желанное улучшение. Стремитесь выработать привычки, которые по умолчанию дают эффективный, сопровождаемый и оптимизируемый код. В частности, при написании функции, не являющейся разовой деталью реализации, учитывайте:
Предпочитайте чистые интерфейсы, несущие достаточно информации для последующего улучшения реализации. Помните, что информация поступает в реализацию и выходит из неё через предоставляемые нами интерфейсы.
Если вам кажется, что нужна связная структура, попробуйте спроектировать интерфейс так, чтобы пользователи её не видели.
Различайте изменяемые и неизменяемые данные. Не перекладывайте на пользователей бремя управления ресурсами. Не навязывайте пользователям лишние косвенные обращения в рантайме. Используйте общепринятые способы передачи информации через интерфейс; нестандартные и/или «оптимизированные» способы передачи данных могут серьёзно усложнить последующую переработку.
Не переусердствуйте с обобщением; дизайн, пытающийся учесть каждое возможное использование (и злоупотребление) и откладывающий каждое решение на потом (через компиляторные или рантайм-косвенности), как правило представляет собой запутанный, раздутый и трудный для понимания беспорядок. Обобщайте на основе конкретных примеров, сохраняя производительность при обобщении. Не обобщайте на основе лишь предположений о будущих потребностях. Идеал — обобщение с нулевыми накладными расходами.
Используйте библиотеки с хорошими интерфейсами. Если подходящей библиотеки нет — создайте её самостоятельно, подражая стилю интерфейса хорошей библиотеки. Стандартная библиотека — хорошее место для поиска вдохновения.
Изолируйте свой код от запутанного и/или устаревшего кода, предоставив для него интерфейс по своему выбору. Это иногда называют «обёрткой» для полезного, но беспорядочного кода. Не позволяйте плохому дизайну «просачиваться» в ваш код.
- Передачу информации:
- Компактность данных: По умолчанию используйте компактные структуры данных, такие как
std::vector, и обращайтесь к ним систематически. - Передача аргументов и возврат из функций:
- Абстракция:
- Библиотеки:
- Изоляция:
Пример
Рассмотрим:
template<class ForwardIterator, class T>
bool binary_search(ForwardIterator first, ForwardIterator last, const T& val);
binary_search(begin(c), end(c), 7) сообщит вам, содержится ли 7 в c. Однако он не скажет вам, где находится этот 7 и существует ли более одного 7.
Иногда достаточно вернуть минимум информации (здесь true или false), но хороший интерфейс возвращает вызывающей стороне нужную информацию. Поэтому стандартная библиотека также предлагает:
template<class ForwardIterator, class T>
ForwardIterator lower_bound(ForwardIterator first, ForwardIterator last, const T& val);
lower_bound возвращает итератор на первое совпадение (если оно есть), иначе — на первый элемент, больший val, или last, если такого элемента нет.
Однако lower_bound по-прежнему не возвращает достаточно информации для всех случаев использования, поэтому стандартная библиотека также предлагает:
template<class ForwardIterator, class T>
pair<ForwardIterator, ForwardIterator>
equal_range(ForwardIterator first, ForwardIterator last, const T& val);
equal_range возвращает pair итераторов, указывающих на первое совпадение и на позицию за последним совпадением.
auto r = equal_range(begin(c), end(c), 7);
for (auto p = r.first; p != r.second; ++p)
cout << *p << '\n';
Очевидно, эти три интерфейса реализованы на основе одного базового кода. Это просто три способа представить базовый алгоритм бинарного поиска пользователям: от простейшего («делайте простые вещи простыми!») до возвращающего полную, хотя и не всегда нужную информацию («не скрывайте полезную информацию»). Разумеется, создание такого набора интерфейсов требует опыта и знания предметной области.
Примечание
Не проектируйте интерфейс, исходя лишь из первой реализации и первого варианта использования, который пришёл в голову. После завершения первоначальной реализации пересмотрите её; после внедрения исправить ошибки будет трудно.
Примечание
Необходимость в эффективности не означает необходимость в низкоуровневом коде. Высокоуровневый код не обязательно медленный или раздутый.
Примечание
Всё имеет свою цену. Не будьте параноидальны насчёт затрат (современные компьютеры действительно очень быстрые), но имейте приблизительное представление о порядке величины стоимости того, что вы используете. Например, имейте грубое представление о стоимости: обращения к памяти, вызова функции, сравнения строк, системного вызова, обращения к диску и отправки сообщения по сети.
Примечание
Если вы можете придумать только одну реализацию, вероятно, у вас нет чего-то, для чего можно создать стабильный интерфейс. Возможно, это просто деталь реализации — не каждый фрагмент кода нуждается в стабильном интерфейсе — но стоит задуматься. Полезный вопрос: «Какой интерфейс потребовался бы, если бы эту операцию нужно было реализовать с использованием нескольких потоков? с векторизацией?»
Примечание
Это правило не противоречит правилу Не оптимизируйте преждевременно. Оно дополняет его, побуждая разработчиков создавать условия для последующей — уместной и непреждевременной — оптимизации там, где это нужно.
Контроль
Сложно. Возможно, поиск аргументов типа void* в функциях поможет найти примеры интерфейсов, препятствующих последующей оптимизации.