воскресенье, 7 ноября 2010 г.

Борьба за килобайты. Компактность VC++-приложения

Собрав первый снапшот плагина NamedFolders я обнаружил, что размер итогового бинарного файла вырос с 380 до 700 кб. В два раза(!) больше. При том, что я всего лишь сменил компилятор с VC2005 на VC2008 и внес небольшие изменения в код. Более того. Размер новой, 64-битной версии плагина зашкаливает за 1 Mb. Для плагина FAR это уже через-чур. Как уменьшить размер итоговой dll? Решил разобраться.

Что говорит Google
Вот какие полезные статьи по теме минимизации размеров бинарных файлов мне удалось найти.
  • Как уменьшить размер плагина (на примере Microsoft VC++ 6.0) - давняя статья про оптимизацию размеров плагина для FAR Manager. Методы оптимизации: выравнивание, отключение инициализации CRT, замена strlen на lstrlen (см. дополнение).
  • Анатомия C Run-Time, или Как сделать программу немного меньшего размера - хорошая статья на rsdn. Методы оптимизации: отключение инициализации CRT, грамотное использование директивы #import и вычислений с плавающей точкой, использование Automation API для преобразования типов. Рассмотрен вопрос зависимости STL от CRT (std::string - зависит). Статья 2002 года.
  • Reduce EXE and DLL Size with LIBCTINY.LIB - экзотический подход - замена LIBC.LIB и LIBCMT.LIB библиотекой LIBCTINY.LIB. Подходит лишь для очень простых приложений, минимально использующих CRT. Статья 2001 года.
  • Techniques for reducing Executable size - здесь все методики разложены по полочкам. Рассмотрены ключи компилятора и линкера, приведен список стандартных функций, имеющих Win32-эквиваленты. Статья 2008 года.
  • Creating the smallest possible PE executable - здесь описывается, как получить рабочий exe-файл размером всего 133 байта.
  • Создание компактных приложений на VC++ - Статья на хабре. Рассматривается вариант использования стандартной библиотеки "msvcrt.dll" вместо актуальных "msvcr71.dll" и "msvcp71.dll". Из Windows Driver Kit (WDK) нужно взять файлы, содержащие разницу между актуальной и стандартной версией runtime-библиотек и прилинковать эти файлы к приложению. Из минусов - придется ограничить минимальную версию операционной системы, например Windows XP). Статья 2010 года.
  • Dynamically linking with MSVCRT.DLL using Visual C++ 2005 - а это оригинальная статья, в которой предложен трюк с WDK-файлами. 2007 год.

Что можно сделать
Большинство методик, изложенных выше, подходят для очень небольших приложений. Таких, которые используют минимум или вообще не используют возможности стандартной библиотеки C++. В моем приложении функции CRT напрямую не используются. Зато плагин очень интенсивно использует библиотеки STL, Boost и stlsoft. Поэтому отказаться от CRT или заменить ее альтернативным вариантом вряд ли получится. Хотя попробовать конечно стоит. Это - первое направление.

Второе направление работ - замена функций аналогами. Windows и FAR предоставляют ряд функций-аналогов, которые могут запросто заменить многие функции библиотеки С++. Если вызов библиотечных функций увеличивают размеры итоговой dll, то вызовы аналогов практически ничего не будут стоить.

Третье направление - борьба с разбуханием шаблонов. Как известно, опрометчивое использование шаблонов быстро приводит к разбуханию бинарного файла. Если у меня в коде используется два контейнера - std::vector<int> и std::vector<byte>, то это два разных класса, так что код вектора будет сгенерирован дважды. В моем приложении используется приличное количество контейнеров и, вполне возможно, от некоторых можно отказаться. Естественно, такие замены должны быть прозрачными, легковесными и не требовать внесения больших изменений в код.

Четвертое направление - попробовать поварьировать настройки компилятора и используемые инструменты. Проверить, что будет, если заменить VC2008 на VC2010, стандартную STL на STLPort и т.п.

Результаты оптимизации
Оптимизация была проведена в 7 шагов. На каждом шаге плагин собирался на Visual Studio 2008 и Visual Studio 2010 для платформ x86 и x64. Результаты собраны в таблицу.
Размер в kbx86x64
VC2008VC2010VC2008VC2010
0 (static, /MT)6826401067,5869,5
0 (dll, /MD)469440,5742575,5
Шаг 1.-std::locate526473816616,5
Шаг 2. 2х map -> list.519,5466,5806,5610
Шаг 3. map -> list.517,5464,5803606,5
Шаг 4. -vector;517463,5801605
Шаг 5. /Os413,5361,5718543
Шаг 6. -vector.411361713,5542
Шаг 7. -regex389348679,5522,5
Шаг 0 - это исходная позиция. Для удобства приведены размеры DLL с двумя вариантами линковки библиотек С++ - статическим (/MT) и динамическим (/MD).
Шаг 1 Обнаружил в коде функцию, преобразующую строку к нижнему регистру. Функция была реализована таким образом: boost::algorithm::to_lower(dest, std::locale("")). Far Manager предоставляет функцию-аналог для приведения символа к нижнему регистру - FarStandardFunctions.LLower(ch). Заменил аналогом. Выигрыш оказался существенным ~150-250 kb. Подозреваю, что прежде всего за счет исключения std::locale.
Шаг 2 В коде широко используются контейнеры vector и list. А вот map используется всего три раза. Причем функциональность map там как таковая не нужна - в контейнеры помещается по 5-7 элементов. Заменил std::map<std::wstring, int> std::map<std::wstring, std::wstring> на vector<std::pair<std::wstring, std::wstring> > (такой вектор в программе уже использовался). Выиграл ~10 kb.
Шаг 3 Заменил и последний мап std::map<pointer, pointer> на новый list, которого в программе еще не было. Выиграл ~3 kb.
Шаг 4 Исключил std::vector<int>, заменил уже имеющимся std::vector<byte>. Сэкономил ~ 1 kb.
Шаг 5 Включил опцию компилятора /Os (Favor small code). Ранее была включена опция /Ot. Размер уменьшился на ~100 kb.
Шаг 6 Исключил вектор std::vector< std::pair<tstring> >, заменил имеющимся аналогичным списком. Экономия ~3 kb.
Шаг 7 Приложение интенсивно использует boost::regex. Обнаружил, что у меня используется два варианта функций - для std::wstring и для wchar_t const*. Оставил только стринги. DLL похудела еще на ~25 kb.

Результат неплохой - размер DLL уменьшился примерно в 2 раза и стал даже меньше варианта "0 (dll)". Наибольший эффект, конечно, дали исключение из кода вызова std::locale и включение опции компилятора /Os, но и чистка шаблонов тоже не прошла незамеченной. Очень порадовала Visual Studio 2010 - размер компилируемых файлов существенно ниже.

boost::regex vs boost::xpressive
Как известно, с недавнего времени в boost имеется две библиотеки для работы с регулярными выражениями: boost::regex и boost::xpressive. Последняя состоит целиком из заголовочных файлов (не требует статической линковки с какими-либо lib-файлами) и позволяет задавать регулярные выражения не только динамически (в строковом виде), но и статически, на уровне классов. По заверениям разработчиков, статические выражения дают прирост производительности на уровне 15%. При этом boost::xpressive поддерживает тот же интерфейс, что и boost::regex, так что заменить одну библиотеку на другую - не проблема. Заменил:
Размер в kbx86x64
VC2008VC2010VC2008VC2010
Шаг 7389348679,5
Шаг 8. Замена regex на xpressive473,5457,5779638,5
К сожалению, стало хуже. С точки зрения размеров генерируемого кода boost::regex в данном случае оказался несколько более оптимальным. Оставил boost::regex.

Visual C++ STL vs STLPort
Попробовал заменить родную STL на STLport-5.2.1. По идее, чтобы использовать stlport, необходимо пересобирать boost. Все из-за boost::regex, т.к. она использует lib-файлы, которые требуется прилинковывать к приложению. Именно их необходимо пересобрать с использованием stlport. Чтобы избежать этой мороки (мне ведь нужно всего лишь попробовать), я заменил boost::regex на boost::xpressive и собрал приложение с stlport без пересборки boost. Собрал только на VC2008 под x86.
Размер в kbx86x64
VC2008VC2010VC2008VC2010
Шаг 8473,5457,5779638,5
Шаг 9. Замена STL на STLPort545---
Результаты огорчили. Размер приложения вырос довольно ощутимо. Будем ждать выхода новой версии stlport.

Замена CRT
Можно ли заменить CRT на меньшую? Вариантов замены как минимум три: Tiny C Runtime Library (доработанная LIBCTINY.LIB), Win32API CRT и ntdll.dll.

Честно говоря, я не стал особо заморачиваться с такой заменой. Это не тот случай, когда такая замена может быть оправдана. Гораздо больше меня заинтересовала методика линковки с msvcrt.dll (см. последние две статьи в списке статей выше). Почему бы не попробовать?

Попробовал. И воткнулся в ряд проблем. Во-первых, если ваше приложение прилинковывает какие-либо сторонние библиотеки, то их нужно ПЕРЕсобрать с использованием msvcrt.dll. В моем случае, такой пересборки потребовала библиотека boost::regex.

Во-вторых, невозможно использовать стандартную STL, т.к. она полагается на стандартную CRT. Поподробнее об этом написано вот здесь. Так что нужно использовать STLPort.

Но STLPort тоже требует сборки, если используются iostream. В моем приложении, к сожалению, стримы где-то (неявно) зацеплены. В результате, STLPort так же требуется собирать с msvcrt.dll.

В итоге, чтобы прилинковаться к msvcrt.dll потребуется по-хитрому пересобрать stlport и boost. Овчинка выделки не стоит. Особенно если учесть, что stlport в настоящее время дает существенно большие размеры dll, чем стандартная STL.

P.s. Но в принципе такая работа похоже выполнима. По крайней мере, вот в этом проекте подобным образом пересобрали, например, OpenSSL.

Новшества C++ 1X
Теоретически, для уменьшения размера приложения можно попробовать применить новые возможности компилятора Visual C++ 2010. Прежде всего, rvalue reference и класс unique_ptr в качестве замены boost::shared_ptr. Однако все это требует существенного изменения кода приложения, так что оставлю это на будущее.

Выводы
Размеры итоговой dll сократились примерно в два раза. Неплохо - освободилось место для новых функций :) По итогам работы сделал для себя вывод: надо переходить на 2010-ую студию. Visual C++ 2010 генерирует ощутимо более компактный код, чем Visual C++ 2008.

3 комментария:

  1. Очень хорошая статья. Спасибо.

    ОтветитьУдалить
  2. Creating the smallest possible PE executable - здесь описывается, как получить рабочий exe-файл размером всего 133 байта.
    http://www.phreedom.org/research/tinype/
    cсылка изменилась

    ОтветитьУдалить