mov
. (В общем-то известно, что одних инструкций mov
достаточно, чтобы написать любую программу.)Я решил развить исследование de Vos, взяв в качестве эталонного кода компилятор LLVM/Clang. У него сразу несколько преимуществ перед содержимым /usr/bin неназванной версии неназванной ОС:
- С ним удобно работать: это один огромный бинарник, по размеру сопоставимый со всем содержимым /usr/bin среднестатистического линукса;
- Он позволяет сравнить разные ISA: на releases.llvm.org/download.html доступны официальные бинарники для x86, ARM, SPARC, MIPS и PowerPC;
- Он позволяет отследить исторические тренды: официальные бинарники доступны для всех релизов начиная с 2003;
- Наконец, в исследовании компиляторов логично использовать компилятор и в качестве подопытного объекта :-)
Начну со статистики по мартовскому релизу LLVM 10.0:
ISA | Размер бинарника | Общее число инструкций | Число разных инструкций |
---|---|---|---|
AArch64 | 97 МБ | 13,814,975 | 195 |
ARMv7A | 101 МБ | 15,621,010 | 308 |
i386 | 106 МБ | 20,138,657 | 122 |
PowerPC64LE | 108 МБ | 17,208,502 | 288 |
SPARCv9 | 129 МБ | 19,993,362 | 122 |
x86_64 | 107 МБ | 15,281,299 | 203 |
А вот распределение по числу инструкций:
Неожиданно, что код для SPARC на 11% состоит из
nop
-ов, заполняющих branch delay
slots. Для i386 среди самых частых инструкций видим и
int3
, заполняющую промежутки между функциями, и
nop
, используемую для выравнивания циклов на строки
кэша. Наблюдение de Vos о том, что код на треть состоит из
mov
, подтверждается на обоих вариантах x86; но даже и
на load-store-архитектурах mov
оказывается если не
самой частой инструкцией, то второй после load.А как набор используемых инструкций менялся со временем?
Единственная ISA, для которой в каждом релизе есть официальный бинарник это i386:
Серая линия, отложенная на правой оси это число разных инструкций, использованных в компиляторе. Как мы видим, некоторое время назад компилятор компилировался гораздо разнообразнее.
int3
стала использоваться для заполнения промежутков только с 2018; до
этого использовались такие же nop
, как и для
выравнивания внутри функций. Здесь же видно, что выравнивание
внутри функций стало использоваться с 2013; до этого
nop
-ов было гораздо меньше. Ещё интересно, что до 2016
mov
-ы составляли почти половину компилятора.Самые первые версии LLVM, до появления clang, выпускались ещё и с бинарниками для SPARC. Потом поддержка SPARC утратила актуальность, и вновь она появилась лишь через 14 лет с на порядок увеличившимся числом
nop
-ов:Исторически следующая ISA, для которой выпускались бинарники LLVM это PowerPC: сначала для Mac OS X и затем, после десятилетнего перерыва, для RHEL. Как видно из графика, переход после этого перерыва к 64-битному варианту ISA сопровождался заменой большинства
lwz
на ld
, и вдобавок
удвоением разнообразия инструкций:В бинарниках для x86_64 и ARM частота использования разных инструкций почти не изменялась:
При подсчёте инструкций ARM я отсекал суффиксы условий вместе с ними получалось около тысячи разных инструкций, но даже и без них ARM сильно опережает другие ISA по разнообразию генерируемых инструкций. Таким образом, слой
b
на этом графике включает и все условные переходы
тоже. Для остальных ISA, где условными могут быть только переходы и
немногие другие инструкции, суффиксы условий при подсчёте не
отсекались.Наконец, самая недавняя ISA, для которой публикуются официальные бинарники это AArch64. Здесь интересно то, что
orr
с
прошлого года почти перестала использоваться:PowerPC и AArch64 оказались единственными ISA, для которых число разных используемых инструкций всё растёт и растёт.