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

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

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

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

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

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


person Fascia    schedule 26.07.2012    source источник
comment
К вашему сведению: я обнаружил, что использование вычисляемого перехода в gcc значительно быстрее, чем большой коммутатор.   -  person Vaughn Cato    schedule 26.07.2012
comment
Ваш вопрос не совсем ясно дает мне понять, действительно ли вы вообще проводили тест для измерения производительности. Пост, на который вы ссылаетесь, действительно прекрасен, но такая информация заставляет людей «чрезмерно реагировать» и решать проблемы с производительностью, которые вызвали только 1% потери производительности (или сделали ее еще хуже, чем была). Преждевременная оптимизация — корень всех зол.   -  person Bart    schedule 26.07.2012


Ответы (4)


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

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

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

Редактировать

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

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

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

Таким образом, ваше утверждение «предсказатель ветвления никак не может предсказать скачок» должно звучать так: «предсказатель ветвления target никак не может предсказать скачок».

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

person Shahbaz    schedule 26.07.2012
comment
Наоборот, если вы прочитаете еще раз, вы увидите, что моя проблема связана с тем, что предсказатель ветвления никак не может предсказать скачок, и поэтому конвейер пуст для (я полагаю, для последних процессоров Intel) 14 циклы. При выполнении миллионов эмулируемых инструкций в секунду это складывается, на самом деле, я считаю, что это может быть одним из самых больших узких мест для эмулируемого ЦП (поскольку выполнение инструкций довольно тривиально). Мой вопрос в том, какие есть варианты, если таковые имеются, чтобы обойти это время простоя? - person Fascia; 26.07.2012
comment
Спасибо за ваше редактирование, я не понимал, что существует различие между механизмом, определяющим, прыгает ли он, и где он прыгает, это полезно знать. У меня есть ощущение, что вы, вероятно, правы в том, что здесь нет вариантов, и это такой позор, потому что время простоя составляет значительный процент от общего времени ЦП, необходимого для выполнения одной эмулируемой инструкции. - person Fascia; 26.07.2012
comment
@fascia, к сожалению, инструкции по расшифровке являются трудоемкой операцией. Я не могу найти способ поиска изображения, но даже в процессоре декодер опкода обычно занимает много места. То есть большая часть объема вашего процессора фактически декодирует, и только небольшая его часть выполняет какие-либо вычисления. - person Shahbaz; 26.07.2012
comment
Что произойдет, если у вас есть 3 случая: 0, 1000, 500000. Как процессор может справиться с этим? - person Mathew Kurian; 17.10.2014
comment
@bluejamesbond, эти случаи обрабатывает не процессор, а компилятор. Вы можете увидеть обсуждения в этом вопросе или здесь. Если компилятор не может преобразовать регистр переключения в таблицу переходов, он может пропустить его или сделать это частично. В вашем случае особенно умный компилятор может использовать value % 3 в качестве индекса для таблицы перехода, но убедиться, что никакое другое значение не принимается, все еще проблема. Вы можете попытаться поискать, например, как gcc это делает, но я сомневаюсь, что это будет легко выяснить. - person Shahbaz; 17.10.2014
comment
@Шахбаз Спасибо! Я сделал несколько примеров и скомпилировал их с помощью gcc. Кажется, он использует обычное сравнение и переходы, когда я делал некоторые из этих случаев со случайными/большими различиями между случаями. - person Mathew Kurian; 17.10.2014
comment
@bluejamesbond, это имеет смысл. Всегда есть компромисс, и gcc решает, какой из них будет более выгодным (я полагаю, вы тестировали с -O2 или -O3?). Было бы интересно сначала протестировать с 1, 2, 3, 4, 5, чтобы убедиться, что преобразование в таблицу переходов действительно выполнено, а затем протестировать с 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1000, чтобы посмотреть, что произойдет. Возможно gcc будет иметь if для 1000, а в части else использовать таблицу переходов, что было бы интересно узнать. - person Shahbaz; 17.10.2014
comment
Я не вижу никого (даже самого себя), чтобы современные довольно высокопроизводительные процессоры имели косвенные предсказатели ветвлений, часто с историей пути. Такой предиктор ветвления очень похож. BTB, но может предсказать несколько разных целей для одной и той же ветки в зависимости от пути по коду. Если ваши фрагменты кода достаточно малы, они могут включать несколько различных проходов через цикл декодирования инструкций и, таким образом, могут быть в состоянии выбрать несколько последовательностей инструкций (например, за инструкциями CMP обычно следует ветвь, а не сразу инструкция, которая перезаписывает флаги, установленные CMP) - person Krazy Glew; 06.12.2016
comment
Простые процессоры могут не иметь косвенных предсказателей переходов, ориентированных на путь. - person Krazy Glew; 06.12.2016

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

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

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

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

person jleahy    schedule 26.07.2012
comment
К сожалению, при эмуляции вы можете (пытаться) выполнять десятки или даже сотни миллионов инструкций в секунду. И если для каждого из них приходится 15 циклов простоя конвейера, это действительно значительно влияет на производительность. - person Fascia; 26.07.2012
comment
Здесь нет бесплатного обеда. Если вы хотите сделать одну из нескольких вещей, и это совершенно непредсказуемо, вы должны либо выполнить код для каждой (вероятной) возможности, либо очистить конвейер. Единственной альтернативой является JIT-компиляция того, что вы пытаетесь эмулировать в собственный код (именно так работали VMWare и другие эмуляторы x86 до виртуализации). Вы не можете ожидать, что процессор будет спекулировать выполнением вашего кода операции до того, как он прочитает код операции из памяти. - person jleahy; 26.07.2012

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

На старых машинах, таких как, скажем, 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, при этом каждая запись в дереве указывает на к другим узлам или к функции для оценки.

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

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

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

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

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

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

person Krazy Glew    schedule 03.04.2013
comment
Для всех, кто заинтересован, этот метод широко известен как Label as Values ​​и поддерживается в gcc и clang. - person Chuu; 02.12.2019

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

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

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

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

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

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

person user2011773    schedule 25.01.2013