Не мешайте лодке плыть: об идеальном коде на C++
Есть такая забава — плавать на лодках по прудикам и речкам. Мореманы хмыкнут и скажут, что не «плавают», а «ходят». Мы ИТ-шники. Нам можно и не такое.
Бывает так, что на борту оказывается активный новичок, результативность которого несопоставима с мощностью реки. В таких случаях можно услышать, - «Не мешай лодке плыть самой!». Действительно, деятельность такого гребца наносит больше вреда, нежели пользы.
Теперь две новости
Плохая, пора признаться себе честно в том, что тягаться с современными компиляторами и их средствами оптимизации становится всё сложнее и сложнее. На самом деле я уверен, что в большинстве случаев это уже невозможно, но буду себя тешить мыслью, что просто сложнее.
Есть и хорошая новость, в отличие от бурной реки, на которой у нас просто нет времени, в отношениях с компилятором у нас есть возможность вдумчиво оттачивать нашу технику.
Есть избитая мысль о том, что человеку лучше оставить оптимизацию алгоритмическую, то есть найти все «O» и стремиться нагнуть их в горизонт. Низкоуровневую оптимизацию лучше оставить компилятору.
Но в процессе работы, особенно в настроении «пишу сразу идеальный код», хочется нет-нет да и помочь кодогенератору сделать идеальный код тоже.
И вот опять, в который раз, я свидетель такой инициативы. Инициатива «сделать всё лучше» прекрасна, особенно в сочетании с хоть какими-то доказательствами её эффективности.
Итак, код сильно упрощён и очищен от смысла, оставлена лишь ключевая конструкция.
#include <iostream> void foo(int num) { if (num == 0) std::cout << "zero" << std::endl; else if (num == 1) std::cout << "one" << std::endl; else if (num == 2) std::cout << "two" << std::endl; else if (num == 3) std::cout << "three" << std::endl; }
Теперь, когда вы посмотрели этот код, я ещё раз повторю, что оставлена лишь ключевая конструкция.
Итак, где хочется помочь: изменить на
#include <iostream> void foo(int num) { switch (num) { case 0: std::cout << "zero" << std::endl; break; case 1: std::cout << "one" << std::endl; break; case 2: std::cout << "two" << std::endl; break; case 3: std::cout << "three" << std::endl; break; } }
Казалось бы совсем не похоже: переменную читаем один раз и выражаем своё намерение более явно. Прежде чем делать такой вывод, крайне полезно посмотреть получившийся код. Для этого нам может понадобится либо набор специальных ключей для компилятора, либо, что гораздо проще и нагляднее, использовать online-сервис.
Что мы увидим в первом и втором случаях?
Что компилятор построил практический идентичный код, который даже скорее ближе в первому варианту с
Торопиться не будем, ведь если бы наш
Даже если бы мы не для всех значений написали
Это лишний раз свидетельствует о том, что мы вполне можем сосредоточиться на написании кода в таком виде, в котором он больше будет нравиться нам самим. Не забывая, конечно же, периодически проверять что на самом деле генерирует компилятор, чтобы не перейти ту черту, когда наш «идеальный и красивый» код начинает превращаться в какую-то кашу, поскольку даже компилятор не смог понять, что же именно мы планировали сделать.
И напоследок, чтобы не сильно углубляться в дебри ассемблера, хочется показать к какому кода на самом деле ближе то, что сделал компилятор и в первом и во втором случае.
#include <iostream> void foo(int num) { const char* p; if (num == 0) p = "zero"; else if (num == 1) p = "one"; else if (num == 2) p = "two"; else if (num == 3) p = "three"; std::cout << p << std::endl; }
Фактически наша строка была вынесена в отдельную переменную, а её печать выполнялась в конце.
Желаю всем интересных экспериментов и открытий!