Почему код должен быть выровнен с границами четного адреса на x86?

Я работаю через язык сборки Kip Irvine "для x86-процессоров, шестое издание" и действительно наслаждаюсь им.

Я только что прочитал о мнемонике NOP в следующем абзаце:

"It [NOP] is sometimes used by compilers and assemblers to align code to 
 even-address boundaries."

Приведенный пример:

00000000   66 8B C3   mov ax, bx
00000003   90         nop
00000004   8B D1      mov edx, ecx

В книге затем говорится:

"x86 processors are designed to load code and data more quickly from even 
 doubleword addresses."

Мой вопрос: причина в том, что причина в том, что для процессоров x86 в книге упоминается (32 бит), размер слова процессора составляет 32 бита, и поэтому он может вытягивать инструкции с помощью NOP и обрабатывать их в один конец? Если это так, я предполагаю, что 64-битный процессор с размером слова квадлового слова будет делать это с гипотетическим 5 байтами кода плюс nop?

Наконец, после того, как я напишу свой код, должен ли я пройти и правильно выполнить выравнивание с NOP, чтобы его оптимизировать, или компилятор (MASM, в моем случае), сделает это для меня, поскольку текст, по-видимому, подразумевает?

Спасибо,

Скотт

Ответ 1

Код, выполняемый на слове (для 8086) или DWORD (80386 и более поздних), выполняется быстрее, потому что процессор извлекает целые (D) слова. Поэтому, если ваши инструкции не выровнены, тогда при загрузке есть стойка.

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

На практике выравнивание кода на границах dword (или любого другого) помогает только тогда, когда команда является целью ветвящейся команды, а компиляторы обычно выравнивают первую инструкцию функции, но не будут выравнивать цели ветвления, которые также могут быть достигнутым путем падения. Например:

MyFunction:
    cmp ax, bx
    jnz NotEqual
    ; ... some code here
NotEqual:
    ; ... more stuff here

Компилятор, который генерирует этот код, обычно выравнивает MyFunction, потому что это цель ветвления (достигнута с помощью call), но не будет выровнять значение NotEqual, потому что при этом будут вставляться инструкции NOP, которые должны выполняться при прохождении. Это увеличивает размер кода и замедляет процесс падения.

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

Ассемблер обычно не будет делать это автоматически.

Ответ 2

Поскольку процессор (16 бит) может извлекать значения из памяти только по четным адресам из-за своего конкретного макета: он делится на два "банка" по 1 байт каждый, поэтому половина шины данных подключена к первому а другая половина - в другой банк. Теперь предположим, что эти банки выровнены (как на моем снимке), процессор может извлекать значения, которые находятся в одной и той же строке.

  bank 1   bank 2
+--------+--------+
|  8 bit | 8 bit  |
+--------+--------+
|        |        |
+--------+--------+
| 4      | 5      | <-- the CPU can fetch only values on the same "row"
+--------+--------+
| 2      | 3      |
+--------+--------+
| 0      | 1      |
+--------+--------+
 \      / \      /
  |    |   |    |
  |    |   |    |

 data bus  (to uP)

Теперь, поскольку это ограничение выборки, если процессор вынужден извлекать значения, которые расположены на нечетном адресе (предположим 3), он должен извлекать значения с 2 и 3, затем значения в 4 и 5, выбрасывать значения 2 и 5, затем присоединитесь к 4 и 3 (вы говорите о x86, который в качестве макета маленькой конечной памяти).
Вот почему лучше иметь код (и данные!) По четным адресам.

PS: На 32-битных процессорах код и данные должны быть выровнены по адресам, которые делятся на 4 (так как есть 4 банка).

Надеюсь, я поняла.:)

Ответ 3

Проблема не ограничивается только извлечением команд. И, к сожалению, программистам не известно об этом рано и наказывают за это часто. Архитектура x86 сделала людей ленивыми. Это затрудняет переход к другим архитектурам.

Он имеет все, что связано с природой шины данных. Когда у вас есть, например, 32-битная шина данных, считывание из памяти выравнивается по этой границе. В этом случае нижние два бита адреса обычно игнорируются, поскольку они не имеют никакого значения. Поэтому, если вы должны выполнить 32-битное чтение с адреса 0x02, будь то часть выборки команд или чтение из памяти. Затем требуются два цикла памяти: чтение с адреса 0x00 для получения двух байтов и чтение из 0x04 для получения двух других байтов. Принимая в два раза больше, задерживая трубопровод, если это выборка команды. Снижение производительности является драматичным и ни в коем случае не напрасно оптимизируется для чтения данных. Программы, которые выравнивают свои данные по естественным границам и настраивают структуры и другие элементы в целых кратных размерах, могут видеть вдвое большую производительность без каких-либо других усилий. Точно так же использование переменной int вместо char для переменной, даже если она будет только считать до 10, может быть быстрее. Это правда, что добавление nops в программы для выравнивания направлений назначения обычно не стоит усилий. К сожалению, x86 - это переменная длина слова, байт, и вы постоянно страдаете этой неэффективностью. Если вы окрашены в угол и вам нужно выжать еще несколько часов из цикла, вы должны не только выровнять по границе, которая соответствует размеру шины (в эти дни 32 или 64 бит), но также и на границе строки кэша и попытайтесь сохранить этот цикл в пределах одной или двух строк кэша. В этой заметке один случайный nop в программе может привести к изменениям, при которых удаляются строки кэша, и может быть обнаружено изменение производительности, если программа достаточно велика и имеет достаточно функций или циклов. Та же история, скажем, например, у вас есть цель ветвления по адресу 0xFFFC, если не в кеше, должна быть выбрана кешлина, ничего неожиданного, но требуется одна или две инструкции позже (четыре байта) другой строки кэша. Если цель была 0x10000, в зависимости от размера вашей функции, естественно, вы могли бы отключить ее в одной строке кеша. Если это часто называемая функция, а другая часто называемая функция находится на достаточно близком адресе, который эти два выселяют друг друга, вы будете работать в два раза медленнее. Это место, где x86 помогает, хотя с переменной длиной инструкции вы можете упаковать больше кода в строку кэша, чем на другие хорошо используемые архитектуры.

С x86 и инструкциями вы не сможете выиграть. На данный момент часто бесполезно пытаться настроить настройки x86-программ (с точки зрения инструкций). Количество различных ядер и их нюансов в один прекрасный день вы можете заработать на одном процессоре на одном компьютере, но тот же код заставит другие процессоры x86 на других компьютерах работать медленнее, иногда меньше половины скорости. Лучше быть в целом эффективным, но иметь немного неряшливости, чтобы он работал нормально на всех компьютерах каждый день. Выравнивание данных покажет улучшение для всех процессоров на разных компьютерах, но выравнивание команд не будет.