Первая публикация 22.08.2015
What’s the use of SPL register?
Введение
Я уже выступал [1] с критикой системы команд AMD64, сейчас более известной как x86-64. Причем, задача специально анализировать появившиеся и исчезнувшие возможности не стояла. Просто при переносе своих средств программирования [2] с Win32 на Win64 возникал ряд проблем, вызывавших один и тот же вопрос: «почему раньше все работало, а теперь нет?». Это касается некоторых выброшенных разработчиками архитектуры AMD64 команд, которые пришлось эмулировать, и, особенно, аппаратной поддержки контроля целочисленного переполнения с помощью инструкции INTO, которая вдруг стала недоступной. Кстати, удовлетворительного решения для INTO я так и не нашел, просто оставил ее в своих программах. Теперь обработчик исключительных ситуаций анализирует случай запрещенной команды INTO и если это она, сам вместо INTO проверяет флаг переполнения. Если флаг переполнения не установлен, обработчик передает управление на следующую инструкцию. Конечно, эффективность такого решения оставляет желать лучшего. При этом Windows приготовила мне здесь еще одну ловушку. Почему-то при срабатывании исключения на запрещенную теперь INTO, в Win64 устанавливается еще и признак ошибки в слове состояния FPU, хотя в запомненном контексте состояние FPU нормальное. Приходится еще и восстанавливать состояние FPU (точнее, командой FNCLEX сбрасывать признак ошибки) при каждом срабатывании INTO.
Но все-таки эти проблемы как-то разрешились, и пришло время не только бороться с недостатками системы команд AMD64, но и воспользоваться ее достоинствами. А основных достоинств, напомню, два: восьмибайтная адресация, снимающая предел в 4 Гбайт, и увеличенное число регистров общего назначения.
Увеличение числа регистров
В случае регистров размером в 2, 4 или 8 байт действительно все логично и естественно. Можно даже сказать, что число регистров увеличилось более чем в два раза, поскольку указатель стека и не используется в вычислениях как остальные. Поэтому в IA-32 у программиста реально было 7 регистров общего назначения, а в AMD64 их стало 15, т.е. RAX, RBX, RCX, RDX, RBP, RSI, RDI и R8-R15.
Но вот в случае регистров размером в байт не все так логично. Немного отвлекаясь, замечу, что объектам размером в байт вообще «не везет» в системах программирования. По сравнению с объектами больших размеров они выглядят какими-то ущербными. Помнится, в «Турбо»-Паскале был всего один тип переменной размером в байт – это CHAR, т.е. символ. Но это еще ничего. А вот в стандарте Си CHAR – это вообще почему-то было целое число. Мне это потому кажется странным, что в языке PL/1, который я использую, объекты размером в байт – это самые обычные объекты. Только маленькие. Есть целое знаковое число с атрибутами BINARY FIXED(7), есть строка бит BIT(8), есть строка символов CHAR(1). Да и логические данные с атрибутами BIT(1) при вычислениях все равно обычно занимают весь байтный регистр, поскольку так намного удобнее. Я это все веду к тому, что регистры с размером в байт очень часто нужны и используются в самых разных задачах. Поэтому увеличение числа таких регистров можно только приветствовать. Тем более что разработчики системы команд AMD64 предложили неожиданный бонус: регистров стало не 16, а целых 20. К восьми имевшимся AL, AH, BL, BH, CL, CH, DL, DH добавлены R8L-R15L, а также SPL, BPL, SIL и DIL.
Сложности использования дополнительных байтовых регистров
Но когда я приступил к доработкам транслятора в части распределения байтовых регистров, возникло чувство, которое иногда бывает при настойчивой рекламе «торговых скидок». Вроде и платишь меньше, а в конечном итоге как-то так получается, что столько же (а то и больше).
Первая ложка дегтя – появление регистра SPL, т.е. младшего байта указателя стека. Вопрос для чего он нужен, я даже вынес в заголовок. Единственная, хоть какая-то полезная операция, которую я смог для него придумать – это проверка указателя стека на кратность 8. В самом деле, если проверять весь указатель стека RSP на кратность 8 потребуется команда в 7 байт:
48F7C407000000….test….rsp,7
А если использовать регистр SPL, то только 4:
40F6C407………………test….spl,7
Но это такая мелочь. И, кстати, если использовать не RSP, а двухбайтный SP, команда станет лишь на байт длиннее:
66F7C40700…………..test….sp,7
В целом же, этот регистр бесполезен и просто занимает номер (№4) в системе кодов команд. Так что, реально можно использовать уже не 20 регистров, а 19.
Но хуже другое. Дополнительные «бонусные» регистры SPL, BPL, SIL и DIL используют те же коды, что AH, BH, CH, DH (т.е. 4, 5, 6, 7). Это приводит к тому, что «старые» регистры нельзя использовать вместе с «новыми» в одной команде.
Например, нельзя написать команду:
408AE6…………………mov…..ah,sil
Потому, что процессор просто воспримет ее как:
408AE6…………………mov…..spl,sil
Поскольку коды регистров AH и SPL одинаковые – 4.
А для задачи распределения регистров при компиляции [3] очень важно, чтобы все регистры были «совместимы», т.е. чтобы их легко можно было присваивать друг другу. Потому что часто байтовый регистр нужно сохранить, а затем восстановить. Эффективнее всего запомнить его в другом байтовом регистре. Но, например, если мне нужно сохранить регистр AH, а свободны в этот момент R8L-R15L, нельзя это сделать командой:
mov r8l,ah
а потом восстановить командой:
mov ah,r8l
Вот и получается, что мне надо или вообще не использовать AH, BH, CH, DH и тогда байтовых регистров остается 15 (а если бы не делали «бонусные» регистры их реально было бы 16), или как-то эмулировать отсутствующие команды.
Например, команду:
mov ah,sil
Можно эмулировать тремя командами:
86E0……….xchg….ah,al
408AC6…..mov…..al,sil
86E0……….xchg….ah,al
Но все это начинает убивать преимущества дополнительных байтовых регистров.
Альтернатива существующим байтовым регистрам
На мой взгляд, разработчики системы команд AMD64 в данном случае не использовали все возможности. Обратите внимание, что новые по сравнению с IA-32 команды реализованы с помощью REX-префиксов. По сути это просто часть кода команды, вынесенная в отдельный байт. И в этом REX-байте только 4 бита несут информацию. Три – это старшие биты номеров регистров-операндов команды (что и дает формальное удвоение их числа), а четвертый – это признак «W», определяющий работу данной команды с 8 или 4 байтами. Очевидно, что в случае однобайтных операндов и однобайтных регистров этот бит вообще не имеет смысла. Кстати, это легко проверить. Например, эти команды с разным кодом выполняются совершенно одинаково:
408AF4………mov…sil,spl ; бит «W» сброшен
488AF4………mov…sil,spl ; бит «W» установлен, но игнорируется
Этот неиспользуемый бит «W» и надо было бы тоже задействовать для указания типов байтовых регистров. Например, если бит сброшен, то считать, что по-прежнему используются «старые» восемь регистров AL, AH, BL, BH, CL, CH, DL, DH. Тогда бы и получилось 16 (а не 15+SPL) нормальных полностью независимых и «совместимых» регистров, которые любым образом можно было бы использовать в одной команде.
Если же бит «W» установлен, то считать, что это дополнительные регистры. Хотя я бы ввел не BPL, SIL, DIL и уж тем более не бессмысленный SPL, а вторые байты регистров R8-R15, т.е. ввел бы дополнительные регистры R8H-R15H, которые и имели бы в этом случае коды 0-7.
Тогда бы получилось не 20, а даже 24 байтовых регистра, и можно было бы хранить по две независимые переменные (размером в байт) в регистрах RAX, RBX, RCX, RDX, R8-R15.
Правда, все равно «старые» и дополнительные регистры в одной команде было бы использовать нельзя. Одного признака «W», увы, для возможности таких команд мало.
Т.е. можно было бы писать команды типа:
and r12l,r13h
Но нельзя писать:
and ah,r13h
Поскольку нет возможности указать, какой из двух операндов «старый», а какой – дополнительный.
Заключение
Разумеется, разработка системы команд это очень трудоемкое и ответственное дело. Наверняка при разработке системы команд AMD64 был проведен анализ реальных программ и обработана статистика использования регистров, что и привело к предложению ввести дополнительные регистры размером в байт. Но, возможно, при этом рассматривались как раз системы программирования, где байтные объекты являются «пасынками» и поэтому просто не попалось фрагментов программ «нормально» работающих с регистрами AH, BH, CH, DH, хотя это совершенно обычные регистры при манипулировании объектами размером в байт. Эти регистры также позволяют удобно хранить по две независимые переменные в одном физическом регистре и не тратить команды на распаковку. В результате стройная система, идущая еще от 8-разрядных микропроцессоров, когда все байтные регистры равноправны и могут встречаться в одной команде, была (на мой взгляд, не совсем обоснованно) нарушена. К тому же появился просто бесполезный регистр SPL. Целесообразней вместо SPL было бы оставить AH.
Конечно, я обойдусь в своем трансляторе и без 24 однобайтных регистров, тем более что возможности по сравнению с IA-32 все равно расширились. Однако, как показано выше, в случае однобайтовых регистров можно было бы сравнительно легко повысить эффективность исполнения и увеличить ресурсы (число таких регистров) без существенных изменений системы команд и внутреннего устройства процессора.
Литература
- Д.Ю. Караваев «Об исключенных командах или за что «списали» инструкцию INTO?» RSDN Magazine #3-4 2013
- Д.Ю.Караваев «К вопросу о совершенствовании языка программирования» RSDN Magazine #4 2011
- Д.Ю.Караваев «О реализации метода распределения регистров при компиляции» RSDN Magazine #1 2012