Почему код должен быть выровнен по границам четных адресов на x86? - PullRequest
20 голосов
/ 05 февраля 2011

Я работаю через 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, в моем случае) сделает это для меня, как кажется из текста?

Спасибо

Scott

Ответы [ 3 ]

18 голосов
/ 06 февраля 2011

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

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

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

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

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

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

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

4 голосов
/ 06 февраля 2011

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

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

Надеюсь, мне все ясно.:)

1 голос
/ 06 февраля 2011

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...