Как иметь дело с предсказанием ветвей при использовании случая переключения в эмуляции процессора

Недавно я прочитал вопрос Почему быстрее обрабатывать отсортированный массив, чем несортированный массив? и нашел ответ абсолютно увлекательным, и он полностью изменился мой прогноз по программированию при работе с ветвями, основанными на данных.

В настоящее время у меня есть довольно простой, но полностью функционирующий интерпретируемый эмулятор Intel 8080, написанный на C, в основе которого лежит 256-битная таблица коммутаторов для обработки каждого кода операции. Моя первоначальная мысль заключалась в том, что это был бы самый быстрый способ работы, поскольку кодировка кода операции не является последовательной во всем наборе команд 8080, и декодирование добавит много сложности, непоследовательности и одноразовых случаев. Стол коммутационного шкафа с препроцессорными макросами очень удобен и прост в обслуживании.

К сожалению, после прочтения вышеупомянутого сообщения мне пришло в голову, что абсолютно нет способа, которым предсказатель ветвления на моем компьютере может предсказать прыжок для случая коммутатора. Таким образом, каждый раз, когда коммутационный футляр перемещается, трубопровод должен быть полностью протер, что приведет к нескольким задержкам цикла в том, что в противном случае было бы невероятно быстрой программой (там даже не так много, как умножение в моем коде).

Я уверен, что большинство из вас думает: "О, решение здесь простое, переходите к динамической перекомпиляции". Да, похоже, что он вырезает большую часть корпуса коммутатора и значительно увеличит скорость. К сожалению, мой основной интерес - это эмуляция старых 8-битных и 16-разрядных консолей эпохи (Intel 8080 здесь является всего лишь примером, поскольку это мой самый простой фрагмент эмулируемого кода), где цикл и синхронизация с точностью до точной инструкции важны как видео и звук должны обрабатываться на основе этих точных таймингов.

При работе с этим уровнем точности производительность становится проблемой даже для старых консолей (например, посмотрите на bSnes). Есть ли какой-либо регресс или это просто вопрос, связанный с процессорами с длинными конвейерами?

Ответы

Ответ 1

Наоборот, операторы switch, вероятно, будут преобразованы в таблицы перехода, что означает, что они выполняют, возможно, несколько if (для проверки диапазона), и один прыжок. if не должен вызывать проблемы с предсказанием ветвления, потому что маловероятно, что у вас будет плохой op-код. Скачок не очень дружелюбен к конвейеру, но, в конце концов, он только один для всего оператора switch.

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

Если есть сомнения, реализуйте другие методы и измерьте производительность.

Изменить

Прежде всего, убедитесь, что вы не путаете предсказание ветвления и предсказание целевой ветки.

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

С другой стороны, предсказание ответвления на конечной основе пытается угадать, куда будет входить прыжок.

Итак, ваше утверждение "нет способа предсказателя ветвления предсказать скачок" должно быть "нет способа, которым предиктор отрасли цели может предсказать скачок".

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

Ответ 2

Поскольку ветки в инструкции с 256-контактным коммутатором плотно упакованы, компилятор будет реализовывать это как таблицу переходов, поэтому вы правы в том, что каждый раз, когда вы проходите через этот код, вы будете запускать неверное предсказание с одной веткой (как косвенный прыжок не покажет никакого предсказуемого поведения). Штраф, связанный с этим, будет составлять около 15 тактов на современном процессоре (Sandy Bridge) или, возможно, до 25 на более старых микроархитектурах, которым не хватает кэша микроопераций. Хорошей ссылкой для такого рода вещей является "Ресурсы оптимизации программного обеспечения" на agner.org. В "Оптимизация программного обеспечения на С++" полезно начать.

http://www.agner.org/optimize/?e=0,34

Единственный способ избежать этого наказания - обеспечить выполнение одних и тех же команд независимо от значения кода операции. Это часто можно сделать с помощью условных ходов (которые добавляют зависимость данных, поэтому медленнее, чем предсказуемая ветвь) или иначе ищут симметрию в ваших кодах кода. Учитывая то, что вы пытаетесь сделать это, вероятно, не будет возможным, и если бы это было тогда, это почти наверняка добавило бы накладные расходы, превышающие 15-25 тактов для неверного прогноза.

Таким образом, в современной архитектуре вы не можете сделать это, что будет более эффективным, чем коммутатор/случай, а стоимость неправильного прогнозирования ветки не так сильно, как вы могли бы ожидать.

Ответ 3

Непрямой прыжок, вероятно, лучше всего подходит для декодирования команд.

На старых машинах, например, Intel P6 с 1997 года, косвенный скачок, вероятно, получит неверное предсказание отрасли.

На современных машинах, таких как Intel Core i7, существует косвенный предсказатель скачка, который делает довольно хорошую работу, чтобы избежать ошибочного предсказания ветки.

Но даже на старых машинах, у которых нет непрямого предсказателя ветвей, вы можете сыграть в трюк. Этот трюк (был), кстати, задокументирован в Руководстве по оптимизации кода Intel со времен Intel P6:

Вместо того, чтобы генерировать что-то похожее на

    loop:
       load reg := next_instruction_bits // or byte or word
       load reg2 := instruction_table[reg]
       jmp [reg]
    label_instruction_00h_ADD: ...
       jmp loop
    label_instruction_01h_SUB: ...
       jmp loop
    ...

сгенерируйте код как

    loop:
       load reg := next_instruction_bits // or byte or word
       load reg2 := instruction_table[reg]
       jmp [reg]
    label_instruction_00h_ADD: ...
       load reg := next_instruction_bits // or byte or word
       load reg2 := instruction_table[reg]
       jmp [reg]
    label_instruction_01h_SUB: ...
       load reg := next_instruction_bits // or byte or word
       load reg2 := instruction_table[reg]
       jmp [reg]
    ...

то есть. замените переход на вершину цикла выборки/декодирования команды/выполнения по коду в верхней части цикла в каждом месте.

Оказывается, что это имеет гораздо лучшее предсказание ветвления, даже в отсутствие косвенного предсказателя. Точнее, условная, единственная цель, ПК с индексированным BTB будет намного лучше в этом последнем, потоковом, коде, чем на оригинале, только с единственной копией косвенного перехода.

Большинство наборов команд имеют специальные шаблоны - например. на Intel x86, инструкция сравнения почти всегда сопровождается веткой.

Удачи и получайте удовольствие!

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

О, и, возможно, я должен упомянуть: эти таблицы, эти операторы switch или структуры данных генерируются специальными инструментами.

Дерево N-way-переходов, потому что есть проблемы, когда количество случаев в таблице переходов становится очень большим - в инструменте mkIrecog (введите инструкцию распознавателя), который я написал в 1980-х годах, я обычно делал таблицы перехода до 64 тыс. записей в размерах, то есть прыгать на 16 бит. Компиляторы того времени сломались, когда таблицы переходов превысили 16M (24 бит).

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

Я был очень агрессивным в mkIrecog, так как я говорю, позволяя использовать до 32 бит в коммутаторе, хотя практические ограничения почти всегда останавливали меня на 16-24 бит. Я помню, что я часто видел первый декодирование как 16 или 18-битный коммутатор (записи 64K-256K), а все остальные декоды были намного меньше, не более 10 бит.

Хмм: Я отправил mkIrecog в Usenet около 1990 года. ftp://ftp.lf.net/pub/unix/programming/misc/mkIrecog.tar.gz Если вам интересно, вы можете увидеть используемые таблицы. (Будьте добры: тогда я был молод. Я не помню, был ли это Pascal или C. Я с тех пор переписывал его много раз, хотя я еще не переписал его для использования битовых векторов С++.)

Большинство других ребят, которых я знаю, кто делает такие вещи, делают вещи байтом за раз, то есть поиск по 8 бит, 256 методам, ветки или таблицы.)

Ответ 4

Я думал, что добавлю что-то, потому что никто не упомянул об этом.

Конечно, косвенный прыжок, вероятно, будет лучшим вариантом.

Однако, если вы идете с N-образным способом, есть две вещи, которые приходят мне на ум:

Во-первых, вместо того, чтобы выполнять равенство N, вы можете сравнить неравенство log (N), проверив свои инструкции на основе их числового кода операции путем дихотомии (или проверите число по биту, если пространство значений близко к полному). Это немного похоже на хэш-таблицу, вы реализуете статическое дерево, чтобы найти конечный элемент.

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

Но я не вижу, чтобы это было быстрее, чем средняя 15-тикратная пенальти, если у вас нет 99% MOV, и вы ставите равенство для кода операции MOV перед другими тестами.