Знакома ситуация, когда место на флэше закончилось, и требуется впихнуть невпихуемое, пожертвовав чем то нужным? Попробуем вместо этого пожертвовать ненужным, оно прячется в довольно неожиданных местах.
Захотелось мне сделать telnet сервер для управления разной техникой на популярном и недорогом модуле WIZnet W5500. Все, что для этого нужно, входит в состав стандартной библиотеки Arduino, результат можно посмотреть тут. Но речь не о нем. Первое, что меня сильно удивило, — этот простой по функциональности код занял больше половины флэша ATmega328P. Конечно, кода в библиотеке Ethernet много, но ведь он не весь используется, компилятор должен выбрасывать неиспользуемый код из собранной прошивки. Проверим, так ли это.
Заходим в директорию, где происходит сборка, — путь к ней можно подсмотреть в сообщениях компиляции, и делаем objdump -t <elf файл прошивки>, чтобы получить таблицу символов. Видим в ней множество связанных с Ethernet функций, включая такие, надобность которых не очевидна, например функции для работы с UDP. То есть выглядит все так, как будто удаления ненужных функций не произошло. В чем же дело?
Ответ может показаться неожиданным, — все дело в наследовании классов, реализующих Ethernet, от базовых классов с множеством виртуальных функций. Компилятор считает, что функция (или метод класса) используется, когда на нее есть ссылки в других местах кода. Но для того, чтобы создать такую ссылку, функцию необязательно вызывать. Достаточно сохранить ее адрес. Даже если мы не делаем этого явно, C++ это делает за нас, помещая указатель на функцию в таблицу виртуальных функций. Даже если мы никогда не пользуемся этой виртуальной функцией, она будет присутствовать в прошивке. Если функция определена в базовом классе как чисто-виртуальная (без реализации), то у нас нет других вариантов, кроме как реализовать ее, даже если она нам вообще не нужна, тем самым увеличив размер кода прошивки.
Проверим, правильность нашей гипотезы. Возьмем библиотеку Ethernet из гитхаба, например здесь, чтобы не трогать стандартную, и модифицируем ее. Уберем наследование, а виртуальные функции сделаем просто методами. Как это сделать аккуратно, обратимым способом, можно посмотреть тут. Результат: размер кода уменьшился на 4460 байт — более чем на четверть от первоначального размера.
Конечно, наследование и виртуальные функции бывают полезны. Однако создавать базовый класс с чистыми виртуальными функциями только для того, чтобы определить интерфейс для последующих реализаций, не всегда оправдано. Сначала стоит убедиться, что вы действительно будете пользоваться этим интерфейсом с объектами разных типов, либо функциональность, реализованная в базовом классе (как например в классе Print), будет вам полезна.
Автор: oleg_v