Олег Данилов aka DO
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 байт Быть может, когда-нибудь
29.01.2001