Продолжим. 1. Если «тяжеловесная» функция многократно вызывается с одними и теми же аргументами, лучшим способом оптимизации будет запоминание однажды вычисленного результата и возвращение его в готовом виде в случае совпадения аргументов. В особенности это касается операций поиска в файле/базе данных. Идентичные запросы здесь — обычное дело, и базы данных давно научились кэшировать их. Так почему бы не применить этот подход и к поиску в файле?
С математическими функциями все обстоит еще интереснее. Как известно, существуют два основных подхода — вычисления на лету и предвычисленные таблицы (часто комбинируемые с различными алгоритмами интерполяции). Проблема в том, что мы наперед не знаем, какой из двух подходов окажется эффективнее. Вычисления требуют времени, предвычисленные таблицы — памяти, а обращение к памяти (особенно вытесненной на диск) — операция не из «дешевых». Однако кэширование вычислений позволяет решить эту проблему, автоматически адаптируясь под конкретную ситуацию и особенно хорошо себя проявляя в распределенных системах, где мы можем одновременно начать поиск с поиском уже вычисленного значения. А там уже кто кого опередит. Если ответ вернется раньше, чем вычисления будут закончены, прерываем их!
Даже на однопроцессорных машинах с поддержкой Hyper Threading уже наблюдается ощутимый прирост производительности! И чем больше процессоров, тем значительнее выигрыш. Единственное, о чем следует позаботиться, — чтобы 2 и более процессора не выполняли одни и те же вычисления одновременно. То есть, получив запрос на наличие уже вычисленных значений, функция не должна выполнять их сама, даже если они отсутствуют в ее кэше, поскольку их уже выполняет кто-то другой. Соответственно, если функция уже приступила к вычислениям, то одна должна вернуть сообщение: «Данных еще нет, но скоро будут», чтобы другая функция не начинала вычисления с нуля.
2. Типизация, призванная оградить программиста от совершения ошибок, хорошо работает лишь на бумаге, а в реальной жизни порождает множество проблем (особенно при низкоуровневом разборе байтов), решаемых с помощью явного преобразования типов или, другим словами, «кастинга» (от английского «casting»), например, так:
Code
int *p; char x;
…
x = *(((char*)p)+3); // получить байт, лежащий по смещению 3 от ячейки *p
Типизация была серьезно ужесточена в приплюснутом Си, вследствие чего количество операций явного преобразования резко возросло, захламляя листинг и культивируя порочный стиль программирования.
Рассмотрим следующую ситуацию:
Жесткая типизация приплюснутого Си трактует попытку передачи void* вместо char* как ошибку
Code
f00(char *x); // функция, ожидающая указателя на char
void* bar(); // функция, возвращающая обобщенный указатель void
f00(bar()); // ошибка! Указатель на char не равнозначен указателю void*
Здесь функция f00 принимает указатель на char, а функция bar возвращает обобщенный указатель void*, который мы должны передать функции f00, но… мы не можем этого сделать!
Компилятор, сообщив об ошибке приведения типов, остановит трансляцию. Что здесь плохого? А то, что программиста вырабатывается устойчивый рефлекс преобразовывать типы всякий раз, когда их не может проглотить компилятор, совершенно не обращая внимания на их «совместимость», в результате чего константы сплошь и рядом преобразуются в указатели, а указатели — в константы со всеми вытекающими отсюда последствиями. Но по-другому программировать просто не получается! Различные функции различных библиотек по-разному объявляют физически идентичные типы переменных, так что от преобразования никуда не уйти, а ограничиться одной конкретной библиотекой все равно не получится. Платформа .NET выглядит обнадеживающей, но… похожая идея (объять необъятное) уже предпринималась не раз и не два и всякий раз заканчивалась если не провалом, то разводом и девичьей фамилией. Взять хотя бы MFC… и попытаться прикрутить ее к чему-нибудь еще, например, к API-функциям операционной системы. Преобразований там будет…
Но частые преобразования очень напрягают, особенно если их приходится выполнять над одним и тем же набором переменных. В этом случае можно (и нужно) использовать объединения, объявляемые ключевым словом «union» и позволяющие «легализовать» операции между разнотипными переменными.
Code
union pint2char /* декларация объединения */
{
int *pi; // указатель на int
char *pb; // указатель на char
} ppp;
int *p; char x; // объявление остальных переменных
…
ppp.pi = p; x = *(ppp.pb+3); // элегантный уход от кастинга
На первый взгляд, вариант с объединениями даже более громоздкий, чем без них, но объединение достаточно объявить единожды, а потом использовать сколько угодно раз, и с каждым разом приносимый им выигрыш будет увеличиваться, не говоря уже о том, что избавление от явных преобразований улучшают читабельность листинга.
Приплюснутый Си идет еще дальше и поддерживает анонимные объединения, которые можно вызвать без объявления переменной-костыля, которой в данной случае является ppp. Переписанный листинг выглядит так:
Использование анонимных объединений в приплюснутом Си избавляет нас от кастинга, но делает логику работы кода менее очевидной
Code
union /* декларация анонимного объединения */
{
void *VOID; // обобщенный указатель void*
char *CHAR; // указатель на char
};
VOID = bar(); f00(CHAR); // уход от кастинга
Анонимные объединения элегантно избавляют нас от кастинга, но, в то же самое время, затрудняют чтение листинга, поскольку из конструкции «VOID = bar(); f00(CHAR);» совершенно не очевидно, что функции f00 передается значение, возращенное bar. Не видя объединения, можно подумать, что VOID и CHAR - это две разные переменные, когда, на самом деле, это одна физическая ячейка памяти.
Музыка: Poets Of The Fall - Carnival Of Rust