Как уменьшить размер плагина

(на примере Microsoft VC++ 6.0)
Олег Данилов aka DO [dosoft@mail.ru]dosoft@mail.ru

«Mala herba cito crestis»
(Сорная трава быстро растет - лат.)

Введение.

Прошло уже несколько лет с тех пор, когда красивой программой считалась программа маленькая. Канули в лету XT, 286 и 386, процессор 486 можно найти лишь дома у любителей старины, а одних только Pentium'ов сменилось несколько штук. Не модно стало писать маленький код, напротив, чем больше компакт-дисков занимает ваше творение, тем лучше. Тем не менее, в этой статье я хотел бы слегка уделить внимание такой теме, как оптимизация. Очевидно, что оптимизировать мы будем плагин для Far'а. Менее очевидно, но все же это факт — для компиляции будет использоваться Microsoft Visual C++ 6.0. Выбор сделан был не случайно, у данного компилятора есть одно, неоспоримое достоинство — он установлен как на моем домашнем компьютере, так и на рабочем. К тому же VC достаточно подробно описан в MSDN, а я без этой библиотеки и шагу ступить не могу, поскольку ни языка C++ ни Win API абсолютно не знаю ;) Кстати, в связи с последним, сразу уточнение — изложенный ниже материал не претендует на полноту. И даже в некотором роде не претендует на правильность. Сформулируем так — у меня это заработало и позволило уменьшить размер получившегося плагина в несколько раз.

Этапы большого пути.

Шаг 0. Все функции, используемые, в плагине были заменены на аналогичные из Win API: strcpy на lstrcpy и т.д. К сожалению, история не сохранила исходный размер плагина, поэтому оценить экономию на этом шаге затруднительно.

 

Шаг 1. Документ VCReadme.txt из архива PlugDoc.rar, входящего в поставку Far'а (ред: начиная с FAR 1.70 beta 5 примеры устанавливаются в отдельный каталог PlugDoc), содержал первоначальную информацию, которая помогла мне успешно скомпилировать свой плагин «ScanDir». Оттуда я вынес следующее:

 

cl /Zp2 /LD ScanDir.cpp /link /def: ScanDir.def kernel32.lib advapi32.lib

 

Что получилось в результате — dll размером 30 килобайт. По нынешним меркам — не размер для программы, но…

 

Шаг 2. Включаем оптимизацию по размеру:

 

cl /O1 /Zp2 /LD ScanDir.cpp /link /def: ScanDir.def kernel32.lib advapi32.lib

 

Сути дела это не изменило. Будем считать ключ /O1 ритуальным обрядом, вызывающим бога оптимизации. Безуспешно…

 

Шаг 3. Пришлось взять в руки скальпель, коим выступила программа DUMPBIN и препарировать полученный файл ScanDir.dll. Вот, что меня там заинтересовало:

 

OPTIONAL HEADER VALUES
…
1000 section alignment
1000 file alignment
…

Summary

1000 .data
1000 .rdata
1000 .reloc
4000 .text

 

О чем это говорит? Только о том, что в данной программе есть четыре секции (section), каждая из которых выровнена на границу 0x1000. То есть, теряем мы тут в среднем 4*0x800 = 8Кб. Реально же после компиляции

 

cl /O1 /Zp2 /LD ScanDir.cpp /link /opt:nowin98 /def: ScanDir.def kernel32.lib advapi32.lib

 

размер ScanDir.dll уменьшился до 19Кбайт. Параметр /opt:nowin98 привел к тому, что выравнивание стало равно 0x200, а плагин существенно «похудел»

 

Шаг 4. Теперь настала очередь MAP-файла. Из него можно извлечь некоторую полезную информацию:

 

…
0001:00000a1f __CRT_INIT@12 10001a1f f LIBCMT:dllcrt0.obj
0001:00000af8 __DllMainCRTStartup@12 10001af8 f LIBCMT:dllcrt0.obj
0001:00000b95 __amsg_exit 10001b95 f LIBCMT:dllcrt0.obj
0001:00000bc8 __cinit 10001bc8 f LIBCMT:crt0dat.obj
0001:00000bf5 __exit 10001bf5 f LIBCMT:crt0dat.obj
…
0001:0000342d __seh_longjmp_unwind@4 1000442d f LIBCMT:exsup3.obj
…

 

Это не что иное, как startup code — код, проводящий инициализацию, необходимую при использовании функций из стандартных библиотек. Но я к этому моменту эти функции не использовал (см. Шаг 0). Потому вполне логичным было следующее:

 

cl /O1 /Zp2 /LD ScanDir.cpp /link /opt:nowin98 /noentry /def: ScanDir.def kernel32.lib advapi32.lib

 

Что я получил? ScanDir.dll размером 5120 байт!

 

Шаг 5. Все вышеперечисленное достаточно универсально и подойдет для любых плагинов. Теперь же начинаем экономить на мелочах, с учетом особенностей конкретной программы. Смотрим в MAP-файле, что из стандартных функций до сих пор используется:

 

…
0001:00000990 _memset 10001990 f LIBCMT:memset.obj
0001:000009f0 __chkstk 100019f0 f LIBCMT:chkstk.obj
0001:000009f0 __alloca_probe 100019f0 f LIBCMT:chkstk.obj
…

 

memset на самом деле вызывается всего пару раз, при этом накладные расходы по вызову функции достаточно велики по отношении к ее размеру. Логично было бы воспользоваться ключиком /Oi, который указывает, что вызовы ряда функций (memset, strcpy, abs и др.) надо заменять на ассемблерный код, который очень и очень лаконичен для функций такого рода.

 

cl /O1 /Zp2 /LD /Oi ScanDir.cpp /link /opt:nowin98 /noentry /def: ScanDir.def kernel32.lib advapi32.lib

 

Что такое chkstk? Как следует из названия — это проверка стека. Локальные переменные, как известно размещаются на стеке и если в одной из функций, общий размер переменных превышает 4K, то компилятор генерирует вызов chkstk, для того, чтобы гарантировать, что размер стека достаточен для них. Разумеется, это правильно и повышает надежность работы программы. Но моя статья рассказывает об оптимизации, поэтому chkstk — долой! Для этого укажем, чтобы вызов chkstk производился только при достижении 10K, чего заведомо в моей программе случиться не должно.

 

Ну а чтобы быть уверенным, что при сборке не используются никакие библиотеки, кроме указанных явно, зададим ключ /NODEFAULTLIB:

 

cl /O1 /Zp2 /LD /Oi /Gs10000 ScanDir.cpp /link /opt:nowin98 /noentry
   /nodefaultlib /def: ScanDir.def kernel32.lib advapi32.lib

 

Общая экономия на этом шаге — 512 байт.

 

Шаг 6. (Не надоело? Терпите, осталось чуть-чуть). Разумеется, memset и chkstk не занимали все 512 байт, это — результат выравнивания секций, о котором говорилось выше. А таких секций целых четыре. Попробуем уменьшить их число, ведь чем меньше секций, тем меньше выравниваний и тем меньше будут потери. Ясно, что .reloc лучше не трогать, она используется Windows в своих целях, а остальные можно попытаться объединить в одну:

 

cl /O1 /Zp2 /LD /Oi /Gs10000 ScanDir.cpp /link /opt:nowin98 /noentry
   /merge:.rdata=.text /merge:.data=.text
   /nodefaultlib /def:ScanDir.def kernel32.lib advapi32.lib

Заключение.

Тут, пожалуй, пока и остановимся. Итоговый размер плагина — 4K. Это не так уж много для программы, которая в процессе работы создает на экране диалог, вызывает внешние программы, работает с реестром, обменивается информацией с Far'ом. Можно ли еще на чем-нибудь сэкономить, и где предел? Попробуем посчитать: 1K ушел на заголовок dll, с ним сделать уже ничего нельзя, 0.5K — на секцию .reloc, и что с ней делать — не очень ясно. Итого остается для дальнейшей оптимизации 2.5 килобайта. А это не так уж мало — целых 2560 байт… Быть может, когда-нибудь…

 

дополнения от Eugene Yavorsky

 
29.01.2001
наверх