Обзор книги Optimized C++: Proven Techniques for Heightened Performance

Optimized C++ Книга написана в 2016 году Куртом Гунтеротом (Kurt Guntheroth), и похоже, что это его первая книга. Наткнулся на неё я случайно, когда покупал что-то другое на Amazon. Но покупать не стал. Через какое-то время она снова всплыла в моём поле зрения, когда один из пользователей русской часть StackOverflow порекомендовали её мне. Учитывая то, что я на неё внимание обратил сам, да ещё и получил рекомендацию, в конце концов я решил её приобрести. Тем более, что стоящих книг по C++ не так много, а по оптимизации и того меньше — все куда-то подевались. Да и репутация O'Reilly тоже сделала своё дело; будь эта книга любого другого издательства (кроме Addison Wesley), я бы скорее всего пренебрёг.

Прежде чем мы перейдём к непосредственному обзору содержимого глав книги, хочу упомянуть некоторые вещи, которые характерны для книги в целом и не принадлежат ни к каким главам по отдельности. Одной из таких вещей являются т.н. «war stories». Они представляют из себя короткие врезки, которые находятся во многих главах книги, где автор кратко описывает какие-то жизненные ситуации, с которыми он столкнулся непосредственно сам — это одна из частей книги, которая мне действительно пришлась по душе.

Дальше, на протяжении всей книги автор использует только одну машину и компилятор: MSVS 2010 на Windows. И хотя этого может быть достаточно для того, чтобы сравнить относительную выгоду от той или иной оптимизации, этого явно недостаточно, чтобы рассмотреть ситуации с разных сторон. Используя только один компилятор, очень легко попасть в такую ситуацию, когда оптимизации на этом компиляторе даст пессимизацию на другом! И всё потому, что выполненная оптимизация сама по себе является неверной (причины могут быть разные).

Если вы разрабатываете продукт для одной конкретной платформы, то это не должно быть для вас проблемой, но если вы пишите книгу для широкой аудитории, то было бы неплохо задействовать несколько компиляторов от разных производителей. Да и выпускать в 2016 году книгу, в которой упоминается только C++11 (утверждать не буду, но не помню, чтобы там было что-то про С++14) и используется MSVS 2010, как-то странновато. Я из этого могу сделать только один вывод: книга была написана задолго до того, как она была опубликована, и её обновлением никто не озаботился.

И последнее: книга просто кишит ошибками и неточностями по отношению к языку C++ и его библиотеке. Первую ошибку я встретил на странице 115. Вот, что пишет автор:

<...>

Smart pointers remember whether the allocated storage is an array or a single instance, and call the correct delete-expression for the type of the smart pointer.

<...>

Это можно трактовать как «не совсем так», так и как «совсем не так». Во-первых, ничего они не помнят. Чтобы умный указатель удалил память, выделенную под массив, нужно создавать умный указатель с типом массива. Во-вторых, shared_ptr не умел удалять массивы до C++17 (без специально переданной функции для удаления, разумеется). Когда я только наткнулся на эту ошибку, я решил, что автор либо описался, либо просто не совсем в курсе. Но чем дальше я погружался в книгу, тем больше фактически ошибок я находил.

Я даже стал их выписывать, чтобы потом послать автору и помочь улучшить книгу. Исходя из всего, что я увидел в книге, рискну предположить, что знания автора в части C++ весьма посредственные, поэтому мне не ясно, зачем было включать в книгу его попытки объяснить ту или иную функциональность языка. Пишешь про оптимизацию — пиши. Знаешь, что твоя навыки и знания языка оставляют желать лучшего? Не пиши по языку вообще. Ну или дай кому-нибудь из тех, кто хорошо в языке разбирается свой манускрипт на вычитку, чтобы не было подобных казусов.

Теперь перейдём непосредственно к главам книги. Первые три главы являются вводными. В них автор знакомит читателя с понятием оптимизации, и тем в чём обычно эта самая оптимизация заключается. Автор упоминает некоторые общие вещи, справедливые как для железа в целом, так и для языка в частности: память медленная, условные переходы «кусаются», копирование и вызов функций — дорогие и т.д. и т.п. После этого он переходит к методологии: как вообще измерять производительность. Здесь обсуждаются различные препятствия и не совсем очевидные места, с которыми программист сталкивается в попытке измерить производительность того или иного участка, а также средства, которые помогают в преодолении этих препятствий.

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

Практика же начинается с 4-й главы, которая посвящена строкам, что совершенно неудивительно — строки один из наиболее часто используемых примитивов в практически любом ПО. А ещё строки весьма просты в анализе, и на их примере довольно просто состряпать код, который будет сначала медленным, а потом ускорится. Этим наш автор и занимается. В порыве оптимизации, он нам даже приводит такой раздел (совет?): «Use Character Arrays Instead of Strings». Как вам?

Дальше идёт неплохая глава, в которой обсуждается оптимизация путём настройки алгоритма. Тут будем немного про О-нотацию, а также про всякие очевидные вещи типа кэша (программного) и прочего. После чего следует глава, которая попытается нас убедить в том, что переменные, создаваемые во время исполнения, это враг производительности. Там же в кучу свалено ещё и копирование с перемещением.

Последующие главы советуют нам оптимизировать выражения, которые часто исполняются в рамках выполнения программ (hot statements), с умом выбирать структуры данных, алгоритмы сортировки и поиска, а также задуматься о том, чтобы использовать сторонние библиотеки, чья реализация может дать весомый прирост в производительности в вашем случае. Кстати, в главе, в которой идёт обсуждение структур данных, автор измеряет производительность std::unordered_map и сокрушается, мол, он ожидал большего. Это довольно курьёзно, делать вывод о структуре данных, протестировав одну её реализацию на одном компиляторе. Причём сделав это на каком-то явно синтетическом тесте.

Ещё одно интересное место находится в главе по поиску/сортировке. Там автор обнаруживает, что передача функтора даёт лучшую производительность, чем использование указателя на функцию. И он никак это не комментирует! Это просто поразительно, ведь это очень интересный момент, а он просто констатирует факт: быстрее. Это очень красноречивый и показательный момент: в этой книге вы не найдёте нормального анализа действительно интересных примеров. Кстати, подобное же утверждение (функтор быстрее указателя на функцию) находится и в первом издании «The C++ Standard Library», тоже без объяснения причины. Но Джосаттис не книгу по оптимизации писал, поэтому ему простительно, а тут нет!

Три последних главы относятся больше к железному миру, чем к особенностям языка и алгоритмов. Первая из них посвящена файловому вводу/выводу. Это довольно короткая глава сомнительного качества, которая оканчивается разделом посвящённым std::sync_with_stdio. Разумеется, утверждается, что с помощью этой функции можно увеличить производительность. Правда, в конце автор нам признается: «I did not test how significant the performance difference was.». Что ж, правда это хорошо, но к чему тогда приводить эту функцию? Это же не всеобъемлющая книга по C++, это книга по оптимизации. Надо признаться, что я неоднократно пробовал применять эту функцию, и ни разу не видел никаких положительных эффектов от её использования.

Следующая глава посвящена оптимизации конкурентного кода. Правда, львиная её доля посвящена описанию возможностей C++11 (причём с ошибками), поэтому то, что вначале кажется интересной главой на деле оказывается небольшим набором весьма поверхностных советов. Одним из которых является «Prefer std::async to std::thread». Весьма сомнительный совет, честно говоря.

Наконец, завершает книгу глава посвящённая оптимизации управления памятью, где есть полезные зёрна, а также приводится пример минимального C++11 аллокатора.

Итак, исходя из того, что я написал до этого, читатель может решить, что я совершенно точно не стану рекомендовать эту книгу. Но это не совсем так. У меня, честно говоря, довольно смешанные чувства, а всё потому, что я не понимаю для кого эта книга. Для опытных программистов тут нет ничего нового, автор просто переписывает труды прошлых поколений (того же Майерса) на новый лад, с минимальным приложением собственного труда и без каких-либо полезных изысканий.

С другой стороны, если говорить о новичках, то они здесь могут почерпнуть немало. Как фактической информации, так и подходов к проблеме (т.е. что можно и нужно рассматривать, когда тебе нужно увеличить производительность, а ты не знаешь с какого конца взяться). Но для новичков здесь очень много ошибок именно в части языка, которые могут дать неверное представление. Поэтому я скажу так: если вы новичок в программировании, то эта книга может дать вам небольшой толчок к пониманию того, с какой стороны подходить к оптимизации. Главное не принимайте ничего на веру, а встретив сомнительные утверждения — проверяйте (это хорошее правило для любого текста, но для этого — особенно). Опытные же программисты могут просто проходить мимо этой книги, не тратя на неё время.

Кстати, письмо автору я так и не написал, потому что количество ошибок и неточностей стало так велико, что я решил, что спасение этой книги мне не интересно.