Как уменьшить размер файла visual studio

Обновлено: 08.07.2024

Я делаю приложение Xamarin Forms с портативной библиотекой классов (PCL).

Я создал приложение с двумя экранами. Когда .apk сгенерирован, я заметил, что он слишком тяжелый. Я видел это из-за Linker/Процесс связывания.

Шагами, которые я выполнил для создания .apk, являются:

    Конфигурация сборки: Release - любой процессор
    Bluid → Clean Solution
    Build → Build Solution
    В проводнике решений я выбираю проект Android (Droid)
    Сборка → Экспорт пакета Android (.apk)
    Пакет apk находится в файле project_forder/bin/Release/project-name.Droid-Signed.apk

Результат - это пакет около 17 МБ.

Действия, которые я предпринял для уменьшения размера, следующие:

    Щелкните правой кнопкой мыши по проекту Android (Droid)
    Нажмите "Свойства"
    Вкладка "Настройки Android"
    Затем вкладка "Linker"
    В компиляции Linking я выбираю только Sdk Assemblies
    И я повторяю шаги для создания apk.

При объединении комбо я выбираю:

    Sdk Только сборки: размер apk одинаковый (17 МБ)
    Sdk и User Assembles: уменьшение размера (13 МБ), но я думаю, что это слишком много. Кроме того, при установке пакета в мобильном и запущенном приложении он немедленно останавливается. Я думаю, что это связано с тем, что некоторая зависимость не включена.
    Я еще не установил ресурсы изображения.
    Я включил зависимость ZXing от NuGet.

EDIT:

Я раскодировал apk, и его структура:

    /assemblies/(10.8 MB, с большим количеством .dll)
    /lib/armeabi/(2.84 MB, с libmonodroid.so и libmonosgen-2.0.so)
    /lib/armeabi-v7a/(2.80 MB, с libmonodroid.so и libmonosgen-2.0.so)
    /lib/x86/(3,41 МБ, с libmonodroid.so и libmonosgen-2.0.so)
    /META-INF/(100 КБ)
    /res/(1.02 MB)
    AndroidManifest.xml(4 КБ)
    classes.dex(2.5 MB)
    среда (1 КБ)
    УВЕДОМЛЕНИЕ (1 КБ)
    resources.arsc(241 КБ)
    typemap.jm(332 KB)
    typemap.mj(381 КБ)

Одним из решений является создание .apk для каждой архитектуры. В этом случае каждый .apk занимает 13-14 МБ, где в папку lib только включает libmonodroid.so и libmonosgen-2.0.so для конкретной архитектуры. Я все еще думаю, что слишком много 14 МБ.

Как узнать, какие файлы в папке сборок могут быть опущены?

Я слышал (от друга), что при публикации одного файла .apk в Google Play, если пакет apk имеет библиотеки для всех архитектур (arm, x86 и т.д.), а пакет занимает 17 МБ, когда пользователь загружает приложение, Google Play создает .apk с определенными характеристиками мобильного пользователя (14 МБ). Это правда? Если это так, публикация 17 МБ .apk не будет проблемой.

Этот ответ - это то, о чем говорит мой вопрос, но он не уточняет, какие сборки я могу опустить или что делать, чтобы их исключить. Я не думаю, что открытие/распаковка файла .apk и удаление библиотек и сборок - вежливое решение.

Как человеку, выросшему во времена дискет и 56 Кбит модемов, мне всегда нравились небольшие программы. Я мог поместить много небольших программ на дискету, которую носил с собой. Если программа не помещалась на моем гибком диске, я начинал думать, почему: много графики? Музыка? Программа сложная или просто раздулась?

В наши дни дисковое пространство стало настолько дешевым (а огромные флешки настолько вездесущими), что люди отказались от оптимизации размера.

Единственная область, где размер е щ е имеет значение — это передача: при передаче программы по проводу мегабайты приравниваются к секундам. Быстрое соединение на 100 Мбит может пропускать только 12 мегабайт в секунду в лучшем случае. Когда на другом конце провода находится человек, ожидающий завершения загрузки, разница между пятью и одной секундами может оказать существенное влияние на восприятие. Человек может зависеть от времени передачи либо напрямую: он загружает программу по сети, либо косвенно — бессерверная служба развертывается для ответа на веб-запрос.

Люди обычно воспринимают что-то быстрее 0,1 секунды как мгновенное. 3 секунды — примерно предел непрерывности потока пользователя, и вам было бы трудно удержать пользователя после 10 секунд.

Хотя меньший размер больше не является существенным, он всё равно лучше.

Мы создадим клон змейки:

Не интересна игровая механика? Не стесняйтесь переходить к интересным частям, где мы сжимаем игру с 65 мегабайт до 8 килобайт за 9 шагов, прокрутите до графика.

Игра будет работать в текстовом режиме, и мы используем поле рисования символов, чтобы нарисовать змею. Я уверен, что Vulcan или DirectX намного веселее, но мы справимся и с System.Console .

Начнем со структуры буфера кадров. Буфер кадров — это компонент, содержащий пиксели (или в данном случае — символы), отображаемые на экране:

Мы предоставляем методы установки отдельных пикселей, очистки и визуализации содержимого буфера кадров System.Console . Шаг рендеринга особых случаев несколько символов, так что мы получаем красочный вывод без необходимости отслеживать цвет каждого пикселя буфера кадров.

Мы не можем переборщить с размером фиксированного массива, потому что как часть структуры массив должен жить в стеке, а стеки, как правило, ограничены небольшим количеством байтов (обычно 1 Мб на поток) и 40*20*2 width*height* sizeof(char) — допустимое число.

Этот генератор не слишком хорош, но нам не нужно ничего сложного. Теперь пишем обёртку для логики игры:

Состояния, которые должна отслеживать змейка:

  • Координаты каждого пикселя тела.
  • Длина змейки.
  • Текущее направление движения.
  • Прошлое направление для случая, когда нужно нарисовать символ изгиба вместо прямой линии.

Структура предоставляет метод расширения змеи на один элемент (возвращает false, если змея уже находится на полной длине), метод HitTest для теста столкновений пикселя тела, отрисовки змейки во FrameBuffer и метод обновления положения змеи как ответ на игровой тик (возвращает false , если змея съела себя). Существует также свойство, чтобы установить направление змеи.

Мы используем тот же трюк с фиксированным массивом, что и в буфере кадров, чтобы не использовать new . Это означает, что максимальная длина змеи должна быть постоянной времени компиляции. Последнее, что нам нужно — игровой цикл:

Мы используем генератор случайных чисел для генерации случайного положения и направления змеи. Мы случайным образом размещаем еду на игровой поверхности так, чтобы она не перекрывала змею, и запускаем цикл игры. Внутри игрового цикла мы просим змею обновить свое положение и проверить, съела ли она сама себя. Затем рисуем змею, проверяем клавиатуру на ввод, тестируем змею с едой и отображаем всё на консоль. Давайте посмотрим, где мы находимся с точки зрения размера.

Я поместил игру в репозиторий GitHub, чтобы вы могли следить за ней. Файл проекта собирает игру в различных конфигурациях в зависимости от переданного при публикации свойства Mode . Чтобы создать конфигурацию по умолчанию с помощью CoreCLR, выполните:


С этой настройкой игра сжимается до 25 МБ. Хорошее сокращение, но оно далеко от нашей цели.

IL Linker имеет более агрессивные настройки, не выставляемые публично, и они могут работать дальше, но в конце концов, мы ограничимся размером самой среды выполнения CoreCLR coreclr.dll в 5,3 Мбайт. Возможно, мы зашли в тупик на пути к игре на 8 Кб?


В отличие от CoreCLR, Mono также зависит от распространяемой библиотеки среды выполнения Visual C++, недоступной в установке Windows по умолчанию: чтобы сохранить автономность приложения, нам нужно зашить эту библиотеку в приложение. Это увеличивает объем ещё на один мегабайт или около того.

Мы, вероятно, сможем сделать приложение меньше, добавив Il Linker, но тогда столкнемся с той же проблемой, что и с CoreCLR — размером среды выполнения mono-2.0-sgen.dll , он составляет 5,9 МБ. Плюс размер библиотек времени выполнения C++ поверх него. Это предел оптимизаций уровня IL.


Давайте посмотрим, где мы находимся теперь с конфигурацией CoreRT по умолчанию:

4,7 МБ. Файл пока самый маленький, но этого недостаточно.


Сейчас мы на уровне 4,3 МБ.


Я сгруппировал еще несколько вариантов компиляции в режим “сильной экономии”. Режим удаляет поддержку возможностей, важных для многих приложений, но не для нашей змейки. Мы удаляем:

Мы достигли 3,0 МБ, это 5% от начального размера, но у CoreRT есть еще один трюк.


Сейчас мы на уровне 1,2 МБ. Оверхед на рефлексию довольно значителен.


Как мы уже видели ранее, CoreRT — это набор библиотек времени выполнения в сочетании с опережающим компилятором. Что делать, если мы заменим библиотеки времени выполнения с минимальным переопределением? Мы решили не использовать сборщик мусора, и это делает работу намного более выполнимой. Начнём с простого:

Мы просто переопределили Thread.Sleep и Environment.TickCount64 (для Windows), избегая всех зависимостей от существующей библиотеки времени выполнения. Делаем то же самое для подмножества System.Console , используемого игрой:

Пересоберём игру с заменой фреймворка:

Неудивительно, что это не слишком эффективно. Заменяемые API уже относительно легки, переписывание только добавляет пару килобайт, о которых не стоит упоминать. Но это важная ступенька к последнему шагу нашего путешествия.

Оставшиеся 1,2 МБ кода и данных в игре — это поддержка вещей, которые мы не видим, но они есть, они готовы, если нам понадобятся. Есть сборщик мусора, поддержка обработки исключений, код для форматирования и печати трассировок стека на консоль, когда происходит необработанное исключение, и многие другие вещи под капотом.

Компилятор может обнаружить, что ничего этого не требуется, и избежать их генерации, но то, что мы пытаемся сделать, настолько странно, что неплохо добавить функции компилятора для его поддержки. Способ избежать этого — просто предоставить альтернативную библиотеку времени выполнения. Начнем с переопределения минимальной версии базовых типов:

/noconfig , /nostdlib , и /runtimemetadataversion — волшебные параметры, необходимыми для компиляции чего-то, определяющего System.Object . Я выбрал расширение .ilexe , потому что .exe мы используем для готового продукта.

Перестроим IL с добавленным кодом и повторно запустим ILC.

Теперь у нас есть zerosnake.obj — стандартный объектный файл, который ничем не отличается от объектных файлов, создаваемых другими нативными компиляторами, такими как C или C++. Последний шаг — связать его. Воспользуемся link.exe , он должен быть в “x64 Native Tools Command Prompt”. Возможно, вам потребуется установить средства разработки C/C++ в Visual Studio.

Имя символа __managed__Main является контрактом с компилятором — это имя управляемой точки входа программы, созданной ILC. Но команда не работает:

Некоторые из этих символов кажутся знакомыми: компоновщик не знает, где искать вызываемые API Windows. Добавим библиотеки для них:

Выглядит лучше, всего 4 неразрешенных символа:

Остальные отсутствующие символы — это помощники, которые компилятор ожидает найти в библиотеке времени выполнения. Их отсутствие обнаруживается только во время связывания, потому что эти помощники обычно реализуются в сборке, и компилятор ссылается на них только по их символическому имени в отличие от других типов и методов, необходимых компилятору и предоставленных нами выше.

После перестроения исходного кода с этими изменениями и повторного запуска ILC, связывание, наконец, будет успешным. Мы сейчас на 27 килобайтах. Игра работает!


Оставшиеся килобайты можно получить с помощью трюков, которые используют разработчики нативного кода, чтобы уменьшить свои приложения. Мы собираемся:

  • Отключить инкрементное связывание.
  • Обрезать информацию о релокации.
  • Объединить похожие разделы внутри исполняемого файла.
  • Установить внутреннее выравнивание в небольшое значение

8176 байт! Игра все еще работает и, что интересно, она полностью отлаживаема. Вы можете отключить оптимизацию в ILC, чтобы сделать исполняемый файл еще более отладочным: просто удалите аргумент --Os .


Исполняемый файл ещё содержит несущественные данные — компилятор ILC просто не предоставляет параметры командной строки, отключающие их генерацию.

Одна из этих избыточных структур данных — информация GC для отдельных методов. В CoreRT есть точный сборщик мусора, требующий, чтобы каждый метод описывал, где находятся ссылки на кучу GC в каждой инструкции тела метода.

Поскольку у нас нет сборщика мусора, эти данные не нужны. Другие среды выполнения — например Mono — используют консервативный сборщик, не требующий этих данных. Он просто предполагает, что любая часть стека и регистров процессора может быть ссылкой GC. Консервативный сборщик торгует производительностью GC ради экономии размера. Точный сборщик CoreRT также может работать в консервативном режиме, но он еще не подключен. Это потенциальное будущее дополнение, которое мы могли бы использовать, чтобы сделать программу ещё меньше. Может, однажды мы сможем сделать упрощенную версию нашей игры в 512 байт загрузочного сектора. А до тех пор — счастливого кода!


В интернетах есть несколько старых статей (тыц, тыц) про то, как уменьшить размер exe-файла, который генерирует Visual C++. Я взялся проверить актуальность рецептов, которые там приводятся, для Visual Studio 2010.

Задачу я взял классическую: найти 10 самых популярных слов в текстовом файле. Мой исходный файл был размером 73728 байт.

Начало

Вот мое изначальное решение, написанное за 8 минут и заработавшее со второй компиляции:

int main()
try string s = "input.txt" ;
ifstream inf(s.c_str(), ios::binary);
if (inf.good()) string word;
map<string, int > wc;
while (inf >> word) ++wc[word];
>
inf.close();
vector< pair< int , string> > wcvec;
for ( auto i = wc.begin(), iend = wc.end(); i != iend; ++i) wcvec.push_back(make_pair(i->second, i->first));
>
sort(wcvec.begin(), wcvec.end());
for ( size_t i = wcvec.size() - 1 , j = 0 ; i >= 0 && j < 10 ; --i, ++j) cout << boost::format( "%6d %s \n " ) % wcvec[i].first % wcvec[i].second;
>
> else throw runtime_error( "No such file" );
>
> catch (exception& e) cerr << e.what() << endl;
> catch (. ) cerr << "Unknown error" << endl;
>
>

VC2010 в конфигурации Release выдал exe-файл размером 73728 байт. Посмотрим внутрь, что там столько жрет:

Заголовок0x400
.text0xC400 = 50176 байтВ этой секции находится код
.rdata0x3A00строки, манглированные имена
.data0x0A00еще немного манглированных имен
.rsrc0x0200манифест
.reloc0x1200рассказ загрузчику как менять адреса в программе, если файл загружен не по тому адресу, по какому ожидал линкер

Наш файл зависит от библиотек рантайма Visual C: msvcp100.dll, msvcr100.dll (10.0 здесь это версия, XP SP2 в стандартной поставке имеет только до 7.0), и, как и каждый исполнимый файл в системе, от kernel32.dll (тот, в свою очередь всегда тянет за собой ntdll.dll).

В памяти программа занимает максимум 2.5 мегабайта на входном файле в полмегабайта. В адресное пространство мапятся нужные dll, кодовые страницы и .nls, по странице (4 KB) жрут переменные окружения, параметры процесса, PEB и TIB, 574 страницы съедает heap и, вопреки опасениям, стек выделяется не весь сразу, а по мере надобности, в пике 3 страницы на сам стек и 1 на его guard page.

Сборка

Для начала попробуем уменьшить файл ключами компилятора и линкера. Я провел небольшое исследование, воспользовавшись, в том числе, утилитой, которая собирала программу, перебирая найденные ключи. Каких-то 8000 exe-файлов за ночь сборки, и я уже могу поделиться результатами :)

Отмечу, что с теми же опциями + /NODEFAULTLIB минимальный hello world с одним MessageBoxA занимает 640 байт (против 7186 байт в Release по дефолту и 28160 в Debug).

Воспользовавшись Function list в одном известном дизассемблере и самописным скриптом, взвесим различные смысловые части кода в изначальном файле:

Init and deinit 889
main 1832
Exceptions 4537
RTTI 82
Runtime checks 527
PE functions 332
Float arithmetics 222
Memory allocation 413
Locks 12
8846
std memory fns 1524
std::allocator 368
std::exception types 152
std::iostream 2022
Locale and facets 700
std::fstream 528
std bufs 3142
std::map 400
std::_Tree 2992
std::vector 1984
std::basic_string 3607
std::char_traits 128
std::pair 400
std::sort 4192
std::heap 993
std comparisons 160
23292
boost::format 4128
boost::io 9216
boost::optional 48
boost::base_from_member 64
boost::exception 4184
boost::detail 234
17874
Overall: 50012

Легко видеть, что boost, который здесь почти ничего не делает (чего не смог бы сделать банальный printf ), занимает почти треть кода. Заменяем, 42544 → 19744 байт.

Сказал Э, скажи Ю. Заменим весь iostream на fopen, printf и K°. 10865 байт.

Уберем sort и vector, заменив на insertion sort в простом массиве размера 10. 8336 байт.

Заменим map на unordered_map. 8896 байт. Ой, я пошутил, откатимся обратно. Внезапно хэш-таблица, которую бы я писал, если бы фашисты отобрали у меня STL, оказалась толще красно-черного дерева.

Выпилим исключения, все равно они здесь почти ничего не делают, и заменим на обработку ошибок в старом мерзком си-стиле. 8112 байта.

Да, поддержку C++ исключений теперь можно и отключить. 7360 байт. Все, больше ничего не смог придумать :)

Теперь можно расслабиться и проверить энтропию. Популярнейший упаковщик исполнимых файлов UPX пожал исходный файл до 32256 байт, с исправленным не справился из-за /align; проставил /align:512, upx пожал до 6144 байт, но побил файл. 7z пожал исходный файл до 29591 байт, а исправленный до 3955 байт. Есть еще простор для сжатия :)

Напомню, исходный код был написан за 8 минут, а оптимизация ключей компилятора и линкера с последующим переписыванием в более компактный вид заняла 18 часов.

UPD: По совету xproger и wizzard0 попробовал Crinkler, хитрый линкер для интрописателей. Как линкеру, ему нужен .obj, который генерирует студия в одну из папок проекта. Для начала следует собрать проект с отключенной Whole program optimization (/GL, оно немного увеличит студийный exe), затем исполнить команду вида:

crinkler.exe /ENTRY:main /SUBSYSTEM:CONSOLE /COMPMODE:SLOW kernel32.lib user32.lib msvcrt.lib msvcprt.lib main.obj

Подумав минуту, Crinkler выплюнет exe размером 3907 байт, который вообще не сжимается 7z! Теперь, думаю, простор для сжатия закончился :)

А какие тебе возможности нужны? Если что, поставишь свежий PSDK и все.

Купил пару толковых книг - полный справочник WinAPI, кое-что из вспомагаловки (типа "С++ глазами хакера" и пр.). Хочу начать изучать чистый API, первооснову, а не нагромождение криво унаследованных друг от друга объектов. Для начала решил написать простенькое приложение для общалова в локальной сети. Сам я по должности сетевой админ. Так что изучение всего этого помогло бы в работе капитально. Delphi конечно очень хорошо, очень люблю эту среду, но хочется первоисточник хоть немного знать. Вот было запостил свои проблемы сюда: Модератор прибил. Совсем Хобот скучным стал. Народ оттуда валит именно из-за вот такого беспредела. Скоро будет, как во времена заката fido - куча эх, где кроме злого на весь мир модера никто не подписан.

В таком случае не дергайся, тебе хватит и 5-ки :-) Единственно, что более ранние версии (до 7.1) не совмем точно соответствуют стандарту С++

- ксати очень дельный совет особенно если C++ Builder или Delphi. Есть еще ASPack/AsProtect

Да насчет UPX я в курсе :) Там еще где - то ASPack лежит :) только вот например если в той-же Дельфи консольник срубить, то занимать он будет без всяких оптимизаций не более 40 кило (7-я версия, ессно, ранние делают еще меньше).

"Только чистый WinAPI" - Тогда хватит и С без плюсов. А вот конкретные советы :

Хе, так скоро вообще до бинарников дойдем :) Будет "от изходника" на все 100 %

(Allan Stark) "полный справочник WinAPI" - как книжка называется, если не секрет?

Вопрос про название купленной книжки в . Слово MSDN мне знакомо :)

Сократить размер можно если создать дистрибутив. В VC++ в свойствах проекта есть переключатель debug/release (или что-то в этом духе), и резко сокращается размер exe.

Ага, не включать отладочную информацию. Естественно отключал - без особой разницы.

19К на чистом консольном проекте должно быть. Поищи в Гугле/Яндексе - "уменьшение размера exe visual c++", находит хорошую статью.

Читайте также: