В программировании есть много концепций, которые не имеют практического применения. Например, квайны — код, который выводит сам себя. Писать такой код довольно сложно, но очень интересно. Есть обфускация — практика создания кода, который трудно понять. Есть программы-самоликвидаторы, программы-головоломки, однострочники, рекурсивные программы и многое другое. А ещё есть сайзкодинг — это когда размер программ максимально минимизируют, просто потому, что могут. Рассказываем, как сайзкодеры пишут такие маленькие, но интересные программы.
Что такое сайзкодинг
Сайзкодинг — это искусство создавать программы размером от 256 байт и меньше для разных типов процессоров. Для сравнения: это всего на 37 байт больше, чем занимает этот абзац, если его сохранить в однобайтной кодировке.
Сайзкодинг зародился в ранние годы вычислительной техники, когда ресурсы были ограничены, а программистам приходилось выжимать максимум из имеющихся возможностей. Тогда сайзкодинг был необходимостью. Но по мере развития технологий и увеличения доступных ресурсов стал искусством.
Сегодня сайзкодинг — это субкультура. Сайзкодеры пишут минимальное количество кода, чтобы достичь максимального эффекта. По всему миру проводятся соревнования и фестивали, посвящённые сайзкодингу и демосцене — это когда создают аудиовизуальные демопроизведения.
Какие трюки используют при сайзкодинге
Основной инструмент сайзкодеров — язык ассемблера. Он позволяет писать код, который напрямую работает с железом. В ассемблере используются мнемоники — короткие символические имена, представляющие машинные команды (например, MOV
, ADD
, SUB
).
Чтобы писать на ассемблере, нужно очень хорошо понимать архитектуру процессора. Для оптимизации кода используют множество трюков: применяют короткие инструкции, работают с регистрами вместо памяти, минимизируют переходы и ветвления, используют побитовые операции и мультибайтовые инструкции.
Также программисты используют недокументированные возможности языка. Например, в сайзкодинге есть трюк с использованием кода как данных и данных как кода. Это означает, что в зависимости от контекста одни и те же байты в памяти могут быть интерпретированы и как инструкции для процессора, и как значения данных. Это позволяет эффективно использовать ограниченное пространство, экономя байты.
Допустим, есть ассемблерная инструкция mov ah
, 37h
, которая перемещает значение в регистр. В машинном коде она представлена как два байта: B4
37
:
B4
— это командаmov ah
, (перемещение значения в регистрah
).37
— это значение, которое перемещается вah
.
Если процессор интерпретирует байт 37
сам по себе, то распознаёт его как команду AAS (корректировка ASCII после вычитания). То есть один и тот же байт 37
можно использовать в разных контекстах: не нужно добавлять новую инструкцию, а можно взять уже имеющийся байт.
Платформы для сайзкодинга
Маленькие программы пишутся практически под любые платформы. Наиболее популярные: x86/DOS, ARM, RISC-V, виртуальные консоли типа TIC-80 или PICO-8, а также классические 8-битные и 16-битные системы Commodore 64, ZX Spectrum и Atari ST.
Языки программирования тоже не ограничиваются одним ассемблером. Например, вот такая программа, имитирующая шахматную доску, написана на JavaScript и занимает 128 байт:
Разметка страницы состоит из элемента:
<pre id=p></pre>
А вот цикл для создания строки:
n = setInterval("for(n+=7,i=k,P='p.\\n';i-=1/k;P+=P[i%2?(i%2*j-j+n/k^j)&1:2])j=k/i;p.innerHTML=P", k = 64)
Для консоли TIC-80 пишут игры на JavaScript или Lua, и их можно портировать на другие платформы. Игра на 128 байт для TIC-80 может занимать всего 64 байта на x86. Мы уже рассказывали, как раньше писали игру для приставок: здесь всё почти так же.
На сайте консоли есть онлайн-редактор для написания кода на Lua, там же есть встроенный редактор карт и спрайтов. Можно не только написать свою игру, но и поиграть в другие.
Платформа для «классического» сайзкодинга — это x86/DOS. Она предлагает прямой доступ к системным ресурсам и возможность написания программ, использующих системные вызовы DOS или BIOS. В результате получаются программы в формате .COM.
Есть программы на ассемблере x86, которые изначально были написаны на C, а затем уменьшены вручную. Есть программы на ассемблере ARM2, созданные для RISC OS, которые работают как на современном оборудовании, например Raspberry Pi, так и на оборудовании Acorn 1987 года.
Демосцена
С сайзкодингом тесно связана демосцена. Эта субкультура зародилась в 1980-х годах с появлением первых домашних компьютеров Commodore 64, ZX Spectrum и Amiga. Изначально демосцены были простыми программами, которые демонстрировали возможности графики и звуковых эффектов. Со временем демосцена превратилась в самостоятельное направление искусства и программирования, включающее создание сложных графических и звуковых композиций.
В демосцене сайзкодинг используется для создания заставок («интро») — небольших демонстрационных программ, ограниченных по размеру, но при этом со сложными визуальными и звуковыми эффектами.
Вот пример такого интро для MS-DOS:
Размер программы 256 байтов, код написан на ассемблере. Код устанавливает видеорежим, настраивает палитру, инициализирует константы и вычисляет координаты для каждого пикселя, используя проекцию «рыбий глаз». В процессе рендеринга каждый пиксель обрабатывается с учетом его положения и направления, что позволяет создавать реалистичные тени и отражения. В коде используется работа с регистрами и инструкциями блока обработки чисел с плавающей точкой.
В программе есть интересный фрагмент кода, который выполняет трассировку лучей и определяет цвет пикселя в зависимости от его пересечения с объектами сцены. И всё это на чистом ассемблере.
Примеры программ
Написать программу, которая будет генерировать вот такой лабиринт, можно задействовав всего 10 байтов или 6 строчек ассемблерного кода:
Изначально программа занимала 42 байта, но сайзкодеры смогли уменьшить её до 10, используя множество оптимизаций:
init:
scasb
salc
and al,''-'/'
add al,'/'
int 29h
jmp init
Код представляет собой бесконечный цикл, который изменяет значение в регистре и выводит соответствующий символ на экран. Сначала выполняется несколько операций над значением, затем выводится символ, соответствующий этому значению. Затем цикл повторяется. В результате программа непрерывно выводит на экран символы, которые изменяются в зависимости от текущего состояния регистра.
Казалось бы, звучит несложно, но, чтобы дойти до такой оптимизации, нужно очень хорошо понимать, как работает процессор и как взаимодействуют процессорные команды.
А такая программа для Linux занимает 64 байта:
Здесь код на языке ассемблера x86 использует логическую операцию XOR для генерации графических паттернов. Используя целые числа и простые арифметические операции, программа создаёт графические эффекты. Основной цикл бесконечно повторяет операции, изменяя цвет и положение каждого пикселя в буфере, что создаёт динамический эффект.
Зачем это вообще нужно?
Минимизация программного кода имеет несколько важных и интересных целей. Создание максимально компактного кода требует от программиста глубокого понимания архитектуры процессора, алгоритмов и методов оптимизации. Это отличное упражнение для улучшения своих навыков и знаний в программировании. Задачи на уменьшение кода часто входят в обучение эффективному программированию.
Сайзкодинг побуждает искать новые и более эффективные способы решения задач. В условиях ограничений по размеру разработчики демонстрируют свою креативность и находят нестандартные решения. Это создаёт элемент соревнования и стимулирует разработчиков развивать свои способности.
Сайзкодинг отчасти сохраняет культуру прошлого, когда вычислительных мощностей и памяти было очень мало. А ещё создавать минималистичные программы — это просто интересное занятие :-)