Уменьшаем размер исполняемого файла Nim со 160 КБ до 150 Б
Кажется, размер бинарных файлов в языке Nim недавно стал популярной темой для обсуждения. Слоган языка Nim: «Выразительно, эффективно, элегантно», и сейчас мы опробуем его эффективность, разобрав несколько вариантов уменьшения размера обычного файла Hello World на Linux.
Следующие несколько минут мы будем:
- писать обычную программу размером 6 KB;
- стараться не использовать стандартную библиотеку C;
- создавать бинарный файл весом 952 байта;
- использовать измененный компоновщик скриптов и ELF заголовок для создания файла весом всего лишь 150 байт.
Исходный код всех примеров из поста можно будет найти в репозитории. Все действия будут произведены на Linux x86-64 с использованием GCC 5.1 и Clang 3.6.0.
Использование стандартной библиотеки С
По умолчанию Nim использует GCC, так как он является базовым компилятором С для большинства платформ, и мы можем использовать команду echo
без подключения glibc
. Мы можем попробовать увеличить скорость и уменьшить размер, а также убрать ненужные после компиляции символы:
Вышло достаточно неплохо! Данный метод можно использовать с любыми программами на Nim, чтобы уменьшить размер файла.
Сейчас давайте попробуем избавиться от glibc
, по крайней мере, временно (мы вернемся к более громоздкому решению немного позднее). Вместо glibc
мы будем использовать библиотеку musl libc
, которая слегка уменьшит размер нашего файла:
Ну что же, в итоге мы получили файл размером 18 KB, который может быть открыт вне зависимости от того, какая у вас версия glibc
(или любой другой библиотеки)!
Вот пример использования Clang вместо GCC при помощи команды --cc:clang
:
Повышение скорости почти не заметно, но мы уменьшили размер. Насколько ваш результат будет похож на наш, конечно, зависит от версий Clang и GCC. Размер вашего файла может немного отличаться.
Конечно, лучшим компилятором на сегодня является GCC, поэтому дальше мы будем работать с ним.
Первым шагом мы отключим этот сборщик мусора, я не думаю, что он нам понадобится:
Дальше мы перемещаем всю динамическую память, сообщения об ошибках и другие плюшки, зависящие от операционной системы, при помощи --os:standalone
(этому соответствует --gc:none
). В нашем коде мы можем заметить panicoverride.nim, который мог бы быть полезным. Но он нам ни к чему. Если будет допущена ошибка, то вместо файла размером в 6 КВ получится это:
Откажемся от стандартной библиотеки C
Теперь мы должны начать думать масштабнее: если мы хотим написать программу, которая ничего не делает, даже не выводит «Hello!», мы можем просто использовать пустой файл. Сейчас мы не должны полностью полагаться на стандартную библиотеку C. Можно попробовать вообще не использовать -passL:-nostdlib
(passL в таком случае будет просто проходить как аргумент в GCC на этапе компиляции):
Ого, достаточно неплохо! Давайте же запустим нашу программу, которая ничего не делает, и насладимся этим:
Ой. Что-то пошло не так. Пересмотрите ваш результат и найдите ошибку. Код работать не может, файл начинает выполняться совершенно непредсказуемо и неправильно. Вместо всего этого мы можем взять в свои руки работу библиотеки C и прописать нашу собственную функцию _start
:
Также мы можем выйти из программы, для которой будем использовать системный вызов библиотеки, это обеспечит чистый вызов системы в ядре Linux на языке Nim. Давайте объединим все команды системного вызова, которые нам нужны, и используем их для написания правильного кода:
Сейчас мы можем успешно скомпилировать:
Последний трюк в этом разделе – это сообщить GCC о необходимости оптимизировать неиспользуемые функции. Эти функции уже установлены в Nim, по типу наших модулей hello
или system
из стандартной библиотеки, в конечном итоге они просто остаются пустыми. Может быть, компилятор Nim мог бы и пропустить их самостоятельно, но обычно вы не заморачиваетесь с несколькими байтами памяти и начинаете писать что-нибудь полезнее. Но сегодня мы говорим конкретно об этом, и для начала мы сообщим GCC отправить функцию и файлы data в отдельные разделы (-ffunction-sections
& -fdata-sections
) для компиляции. Для компоновки мы пропишем Nim, чтобы тот сообщил GCC передать --gc-sections
в ld
, наш компоновщик, который затем удаляет разделы, на которые нет ссылки:
Великолепно! Мы начали с файла размером 160 KB и закончили на 952 байтах. Сможем ли мы еще уменьшить наш файл? Да, конечно, но не с обычными набором «инструментов».
Изменение ссылок для достижения размера файла в 150 байт
Для этого используют точный метод из поста «151-байт статического Linux-файла в Rust» на нашем блоге, за исключением того, что языку Nim и компилятору GCC удается уменьшить размер файла на 1 байт. Между тем, Clang требует на 1 байт больше, чем версия Rust.
Мы продолжим с той программы, которую мы получили в итоге (размер — 952 байта). Но вместо того, чтобы использовать Nim для выполнения всей работы, мы просто создадим файл объектного кода в Nim (--app:staticlib
) и отсюда начнем делать вручную. Измененный компоновщик скриптов и заголовок ELF сделают всю основную работу. Но фактически данная операция выполняется при условиях нашего Nim кода:
158 байт! Сейчас я покажу еще один способ снизить размер файла на 8 байт. Мы переместим наш код за отведенное место в заголовке ELF и получим доступ к памяти вручную:
150 байт! Это конечный результат, который можно получить. Если вам этого до сих пор много и вы хотите сделать свой файл вручную, то вы можете попробовать схитрить еще больше, чтобы получить в итоге 45 байт, как это описано в прекрасной статье «Учебник по созданию крошечных ELF, выполняемых под Linux».
Вывод
Nim — хороший язык для написания маленьких бинарных файлов. Теперь вы знаете, как писать код на Nim без помощи библиотеки C. Писать что-нибудь на Nim с нуля — достаточно интересное и захватывающее занятие. Вы можете проверить ваши результаты в репозитории:
Перевод статьи «Nim binary size from 160 KB to 150 Bytes»