Часто приходится слышать, что важность отличий между наборами инструкций на современных компьютерах преувеличена и, в самом деле, сложно не согласиться с таким наблюдением. Поскольку стандартная программа на 90 % состоит из простейших инструкций для АЛУ, загрузки и сохранения, а также инструкций ветвления, а также с учётом, что на таком базовом уровне разбежки между различными наборами команд очень невелики, такой вывод просто напрашивается. Но RPCS3, эмулятор приставки Sony PlayStation-3 - не просто обычная программа. Даже если вам не доводилось работать с самим эмулятором, вам наверняка приводили RPCS3 в пример как образец потребительского ПО, в котором используются сильные стороны AVX-512. В этой статье я расскажу, почему именно новые инструкции и возможности, введённые в семействе AVX-512, так полезны для эмуляции PS3. В некоторых ситуациях использование 512-разрядных инструкций может положительно сказываться на RPCS3, но в этом посте будет рассказано, почему новые инструкции полезны и для 128-разрядных, и для 256-разрядных конфигураций. К набору инструкций AVX-512 прилагается файл инструкций, который вчетверо больше, чем в AVX2. Ширина регистров увеличилась вдвое, с 256 до 512 бит, и аналогично удвоилось количество регистров, которые есть в распоряжении у программиста: их было 16, а стало 32. The PS3s В SPU (синергичных процессорных блоках) у PS3 есть файл регистров, в который входят 128 отдельных регистров, по 128 разрядов в ширину каждый. В него не входят какие-либо универсальные регистры, в частности, ЦП, и даже адреса памяти будут жить именно в этих регистрах, несмотря на то, что адрес будет использовать всего 32 из 128 доступных разрядов. Поскольку в SPU не реализовано переименование регистров или выполнение в свободном порядке, критически важно обеспечить размотку циклов в SPU – только так можно воспользоваться преимуществами работы с двумя конвейерами выполнения, которые предоставляются на данном аппаратном обеспечении. Поскольку в размотанном виде цикл на SPU может использовать до 60 вышестоящих регистров, для эмуляторов здесь возникает проблема. Решение – сливать часть регистров в память, если их нужно слишком много, и все они не вмещаются в регистрах ЦП на хосте. Правда, такая тактика негативно сказывается на производительности. Дополнительные регистры, введённые в AVX-512, помогают сгладить эту проблему, но в целом также её не решают. AVX-512 добавляет не только новые инструкции, но и новые варианты кодировки для старых инструкций. Многие инструкции, поддерживавшие операнд памяти, теперь поддерживают возможность «встроенного широковещания», благодаря которой 32-разрядное или 64-разрядное значение из памяти может перед использованием широковещательно сообщаться всем элементам вектора. Ещё одна полезная возможность новой кодировки – это промежуточный режим адресации “disp8”. В обычных инструкциях для x86-64 встроенное смещение можно запрограммировать всего в одном байте, если значение смещения составляет от -128 до 127. Если фактическое смещение больше, то в кодировке требуется произвести полное смещение на 4 байта, даже если на практике оно сравнительно невелико, скажем, 256. При сжатии disp8 такое смещение можно закодировать всего в одном байте при условии, что оно кратно размеру вектора, и это кратное находится в диапазоне между -128 и 127. Например, смещение в 256 для 128-битного/16-байтного вектора кодировалось бы как 16 * 16. К сожалению, в новой кодировке EVEX требуется ещё один байт сверх старой кодировки VEX, которая использовалась инструкциями AVX. Поэтому при применении сжатия disp8 экономится всего 2 байта на каждой инструкции, закодированной в VEX. Кроме того, только инструкции, закодированные в EVEX, могут адресовать регистры 16-31, и не все старые инструкции имеют соответствие в новой кодировке. Эти инструкции по-прежнему могут использоваться в коде AVX-512. Но сложность несколько возрастает, поскольку эти инструкции не могут адресовать все регистры. AVX-512 также добавляет новые регистры масок, которые опционально могут использоваться с инструкциями в кодировке EVEX. Существуют новые инструкции сравнения, генерирующие маску в регистре масок в результате сравнения векторов. Когда регистр масок используется в качестве операнда, все элементы, не накрытые маской, либо будут обнуляться, либо существующее значение будет оставаться в целевом регистре нетронутым. Есть 8 регистров масок, k0 - k7, но только k1 - k7 могут применяться для выборочного накрывания, поскольку k0 неявно действует так, будто выделены все элементы. Соответственно, подобную последовательность в AVX можно привести примерно к такому сокращенному виду в AVX512: На современных ЦП Intel те инструкции, у которых в качестве аргумента применяется регистр масок, отличаются немного повышенной задержкой, хотя при этом и не возникает дополнительных микроопераций (uops). Этот фактор важнее, чем дополнительная задержка на ЦП, обусловленная свободным порядком выполнения. Кроме того, возможность сохранять маски именно в регистре масок, а не в векторных регистрах помогает высвободить больше таких векторных регистров. Фактически, файл регистров AVX-512 оказывается более чем вдвое вместительнее, чем такой файл на AVX2. Но довольно о старых инструкциях и новых способах работы с ними. Насколько полезны новые инструкции в AVX-512 для RPCS3? Эта инструкция подсчитывает ведущие нули в каждом 32-разрядном элементе входного вектора. Оказывается, функционально эта работа полностью аналогична тому, что делает инструкция clz синергичного процессорного блока.    Эта инструкция подсчитывает единицы в каждом 32-разрядном элементе входного вектора. Оказывается, функционально эта работа полностью аналогична тому, что делает инструкция cntb синергичного процессорного блока.    Эта инструкция посложнее, она предназначена для ускорения вычисления сумм модулей разностей. Эта инструкция производит сразу множество таких вычислений, распараллеливая их, а только потом горизонтально суммирует все блоки по 4 байта, получая 16-битные результаты. Применив для второго входного вектора ввод из одних нулей, можно немного злоупотребить этой инструкцией, эмулировав с её помощью инструкцию горизонтального сложения. Поскольку абсолютная разница между любым числом и нулём составит именно это число, данная инструкция оказывается полезна для горизонтального сложения блоков по 4 байта. Таким образом удобно эмулировать инструкцию sumb синергичных процессорных блоков. Эти инструкции поворачивают 32-битные элементы вектора на указанное значение. Таким образом, они аналогичны инструкциям rot и roti синергичных процессорных блоков.    vpternlog – это инструкция, позволяющая серьёзно упростить побитовые операции. Эта инструкция принимает байт-константу, который контролирует её работу. Например, выражение (a & c) | (b & ~c) может быть реализовано всего в одной инструкции, для этого можно воспользоваться vpternlog. Данное конкретное выражение выберет биты из каждого вектора, исходя из содержимого c. Это полезно для эмуляции инструкции selb синергичных процессорных блоков. Мало того, что эта инструкция бесценна для эмуляции любой побитовой инструкции, для которой ранее не существовало аналога в x86, она также бесценна для составления последовательностей более простых побитовых инструкций в меньшие последовательности. Эта инструкция очень гибкая, и, вероятно, полюбится всем SIMD-программистам, которые пишут софт для AVX-512. Единственный недостаток этой инструкции в том, что она зависит от выходного регистра. Было бы удобно иметь две входные версии данной инструкции, а также иметь их на те случаи, когда требуется всего две вводные. Эта инструкция получена слиянием двух уже имеющихся инструкций x86, vpmaddwd и vpadd. vpmaddwd – инструкция очень специфическая, по вертикали перемножающая пары 16-разрядных чисел до того, как просуммировать результаты по горизорнтали. Инструкция vpdpwssd действует точно так же, с той оговоркой, что приплюсовывает результат и в регистр назначения. Поскольку SPU поддерживают только 16-разрядное умножение, vpmaddwd полезна для эмуляции только некоторого количества инструкций семейства mpy, относящихся к SPU. Несколько инструкций семейства mpy, например, mpya, также добавляются в регистр-аккумулятор. Таким образом, vpdpwssd подходит для эмуляции этих инструкций. Изначально эта инструкция предоставлялась в рамках AVX512-VNNI в процессорах на основе Sunny cove. Но для неё уже существует и версия в кодировке AVX, включённая в процессоры на основе Golden cove и Gracemont. Просматривая публичную документацию по наборам инструкций для SPU, можно заметить любопытный маленький абзац, рассказывающий о таком явлении, которое именуется в документации «расширенный формат для чисел с плавающей точкой». Поддержка   чисел с плавающей точкой одиночной точности (расширенный диапазон),   ориентированная на работу с графическим процессором Большая часть базы кода в игровых приложениях рассчитана на формат чисел с плавающей точкой одиночной точности, отличающийся от формата IEEE 754, обычно внедряемого в процессорах общего назначения. Подробнее о формате одиночной точности см. в разделе 9 на с. 195   Что это вообще такое – число с плавающей точкой расширенного диапазона? И что понимается под «Большая часть базы кода в игровых приложениях рассчитана на формат чисел с плавающей точкой одиночной точности, отличающийся от формата IEEE 754, обычно внедряемого в процессорах общего назначения»? Оказывается, что SPU наследуют этот причудливый формат с плавающей точкой от PS2, где он ранее использовался. Такой формат с плавающей точкой не поддерживает значения NaN (не число) или INF (бесконечность), а интерпретирует их как очень большие числа с плавающей точкой. Как только сгенерировано значение NaN или INF, избавиться от него довольно сложно, так как большинство последующих операций будут возвращать NaN или INF. Например, INF * 0 = INF. INF * -0.2 = -INF. NaN * 3 = NaN. Единственный способ точно эмулировать это поведение – заново реализовать его в нашем софте, взяв тот вариант чисел с плавающей точкой, который у нас поддерживается. К сожалению, программные реализации чисел с плавающей точкой получаются невероятно медленными, поэтому в RPCS3 (пока) не эмулирован формат чисел с плавающей точкой, рассчитанный на расширенный диапазон (в RPCS3 такой формат называется “XFloat”). Одно из возможных решений также применяется в PCSX2, эмуляторе PS2. Прикрепив INF и NaN к максимальному и минимальному значениям, представимым на ЦП с архитектурой x86, можно без особых издержек решить большинство проблем, возникающих при работе с числами с плавающей точкой. В AVX нам нужны две инструкции, чтобы прикрепить наши числа с плавающей точкой к какому-нибудь удобному конструкту. Можно использовать vpminsd со значением 0x7f7fffff, а также vpminud со значением 0xff7fffff. Придётся работать с целочисленными версиями этих инструкций, поскольку инструкции min/max для чисел с плавающей точкой будут возвращать NaN, если хотя бы один из операндов окажется NaN. При использовании vrangeps вырисовывается, как решить эту задачу всего одной инструкцией. Такая инструкция предоставляет надмножество функционала, предлагаемого имеющимися инструкциями min/max для чисел с плавающей точкой. При использовании с этой инструкцией байта-константы 0x2 она будет принимать абсолютное значение нашего ввода, затем минимальное, а затем копировать в результат знаковый бит от первого вводного значения. Так мы сможем одновременно прикрепить значения и с положительного, и с отрицательного края. Критически важно, что эта инструкция будет возвращать NaN лишь в случае, когда оба входных значения являются NaN. При помощи такой стратегии прикрепления RPCS3 обрабатывает числа с плавающей точкой, когда вы выбираете опции “Relaxed XFloat” или “Approximate XFloat”. При использовании точной опции XFloat RPCS3 попытается эмулировать числа с плавающей точкой (расширенный формат) при помощи чисел с плавающей точкой двойной точности. Так можно обработать некоторые случаи, к которым неприменимо простое прикрепление, но такой подход гораздо медленнее прикрепления, поэтому некоторые игры ломаются с “Accurate XFloat”, но функционируют с “Approximate XFloat”. Наконец, стоит отметить, что инструкции для работы с числами с плавающей точкой двойной точности, предназначенные для выполнения на SPU, соответствуют требованиям IEEE. В отличие от PS2, где все реализации аппаратного обеспечения для чисел с плавающей точкой не соответствуют стандарту, у каждого аппаратного элемента PS3 – собственное поведение. Математический сопроцессор главного ядра PowerPC на PS3 соответствует требованиям IEEE. Однако реализация векторного математического сопроцессора на том же ядре дотягивает только до соответствия стандарту Java для чисел с плавающей точкой. Добавьте сюда графический процессор PS3, в котором предусмотрено собственное оборудование для чисел с плавающей точкой – и получится, что мы рассматриваем пять различных образцов аппаратного обеспечения для обработки чисел с плавающей точкой, и все они проявляют разное поведение. Вот это беспорядок. vgf2p8affineqb – это инструкция для ускорения криптографических вычислений. Однако многие SIMD-программисты осознают, что она может быть полезна и для решения задач, не связанных с криптографией. Так, для RPCS3 полезна предоставляемая в ней возможность перестановки битов. Данная возможность перестановки битов помогает частично эмулировать инструкцию shufb из SPU. По поведению shufb подобна инструкции pshufb из x86. Обе эти инструкции переупорядочивают байты, основываясь на индексах, предоставляемых в другом векторе. Также обе инструкции предусматривают специальные условные вводы, которые будут вставлять константу в целевое значение, а не принимать значение от одного из вводных векторов. У инструкции pshufb из x86 есть всего один вариант условного ввода: 1xxxxxxx = 0x00 У инструкции shufb, относящейся к SPU, 3 таких варианта: 10xxxxxx = 0x00 110xxxxx = 0xFF 111xxxxx = 0x80 Кроме того, инструкция shufb, относящаяся к SPU, индексирует вектор в обратном порядке, если сравнивать с pshufb, а shufb также принимает 3 входных значения, 2 вектора с данными и 1 вектор с индексами. Инструкция pshufb принимает только 2 входных вектора, 1 вектор с данными и 1 вектор с индексами. Подробное объяснение того, как используется vgf2p8affineqb, выходит за рамки этой статьи. Реализация shufb для AVX2 обладает обратной пропускной способностью, равной 4, тогда как у версии vgf2p8affineqb обратная пропускная способность равна 2,3. Не слишком хороший результат, учитывая, что обратная пропускная способность для данной инструкции у PS3 равна 1, а рабочая частота машины составляет 3,2 ГГц. Однако лишь для очень малой части инструкций shufb потребуется полная эмуляция, благодаря многочисленным оптимизациям, применяемым в обычной практике работы с этой инструкцией. В синергичных процессорных блоках PS3 предусмотрена всего одна перестановочная инструкция, shufb. В x86 имеется множество разных перестановочных и преобразующих инструкций для работы с фрагментами данных разного размера. Однако для перестановки данных любого размера (свыше одного байта) нужна только инструкция shuffle bytes. Когда индексы для инструкций shufb константны, можно по мере необходимости воспользоваться одной из таких инструкций, предназначенных для данных более крупного размера. Например, новые инструкции vpermt2d/vpermi2d полезны при перестановке 32-битных данных. К сожалению, не все эти новые инструкции одинаково быстры. Инструкции vpermt2w/vpermi2w и vpermt2b/vpermi2b пригодились бы для перестановки 16-битных и 8-битных данных, но их производительность на современных ЦП неидеальна, так как каждая из этих инструкций требует 3 микрооперации. Поскольку RPCS3 использует LLVM для генерации кода, она должна быть в состоянии выбирать эти новые инструкции, когда уместно. Так как у LLVM есть модель, позволяющая судить, какие инструкции на каждом ЦП быстрые, а какие медленные, она должна быть в состоянии выбрать наилучшую инструкцию для каждой ситуации. Когда/если на новых ЦП эти инструкции станут работать быстрее, LLVM должна быть готова к использованию и тех инструкций, которые ранее считались медленными. Слева направо: SSE2, SSE4,1, AVX2/FMA и Icelake уровня AVX-512. Когда целевой платформой является SSE2, производительность абсолютно ужасает; вероятно, дело в отсутствии инструкции pshufb из SSSE3. pshufb бесценна для эмуляции инструкции shufb, а также критически важна для векторов перестановки байт – также необходимых, поскольку в системе PS3 действует порядок от старшего к младшему, а в x86 – от младшего к старшему. Когда целевой платформой является SSE4,1 достигается в среднем 160 кадров в секунду, а для AVX2/FMA средний показатель составляет 190 кадров в секунду. Соответственно, имеем 18-процентное улучшение по сравнению с целевой платформой SSE4,1. В AVX2 не так много новых инструкций по сравнению с SSE4,1, но всё-таки здесь включена новая 3-операндная форма инструкций, позволяющая исключить многие инструкции mov, отвечающие за перемещение из регистра в регистр. Принципиально, что все ЦП с поддержкой AVX2 также поддерживают инструкции FMA. Инструкции FMA не просто быстрее, чем цепочки операций умножения и сложения, но и могут выдавать иные результаты, так как не округляют с одинарной точностью при переходе от умножения к сложению. Если нужно точно эмулировать такое поведение без инструкций FMA, то возникают некоторые издержки, поэтому здесь немало помогают нативные FMA-операции. Когда целевой платформой является Icelake с AVX-512, достигается гомерическое среднее значение 235 кадров в секунду, это на 23% быстрее, чем для целевой платформы AVX2/FMA. В абсолютном выражении в AVX-512 оказалось добавлено настолько много инструкций, что значительная их доля пригодилась и в RPCS3. В отличие от AVX2, которая представляла собой простое расширение имеющихся инструкций SSE до 256 бит, AVX-512 включает очень много новых возможностей, весьма полезных для SIMD-программирования даже при уменьшенной битовой ширине. Правда, поскольку Intel решила продвигать на рынке AVX-512 именно в варианте -512, те, кто не знаком с набором инструкций, обычно останавливаются на 512-разрядном векторе для набора инструкций. Уместен вопрос: а почему стоит нацеливаться на такие новые наборы инструкций, если их поддерживают только новейшие и самые быстрые процессоры? В конце концов, даже при нацеливании на SSE4,1 процессор 12900K @5,2 ГГц может выдавать вполне удобные для работы 160 кадров в секунду. Однако не все ЦП, поддерживающие AVX-512, – восьмиядерные монстры с тактовой частотой 5,2 ГГц. Сегодня большинство ЦП с поддержкой AVX-512 стоят либо на серверах, либо на ноутбуках. Поскольку у Intel наблюдаются проблемы с наращиванием производства линии по 10 нм, компания решила сначала выпустить новый вариант AVX-512, поддерживающий новую линию процессоров для ноутбуков, так как при этом варианте можно обойтись меньшим размером кристалла. При использовании AVX-512 с этими сравнительно слабыми чипами для ноутбуков производительность не удастся вывести выше 200, как на гиганте 12900K, но так будет легче вывести кадровую частоту на более приемлемый уровень.  По поводу недавно анонсированного Zen 4 было объявлено, что он также будет поддерживать инструкции AVX-512. Поскольку вероятно, что устройства следующего поколения после steam deck будут использовать ЦП на базе Zen 4, вполне возможно, что существенно возрастёт количество желающих поиграть в игры на бюджетных устройствах, поддерживающих AVX-512. Даже притом, что целевая кадровая частота достижима уже без AVX-512, подключение оптимизаций AVX-512 могло бы продлить срок службы батареи или обеспечить для GPU более выгодные требования по теплоотводу (TDP), чтобы можно было играть в более высоком разрешении. Мне уже доводилось наблюдать этот феномен на ноутбуке с процессором Tigerlake. При нацеливании на AVX-512 ядра ЦП потребляют на 1W меньше, а GPU использует на 1W больше, что обеспечивает более высокие кадровые частоты в RPCS3. Вне RPCS3 немногие эмуляторы используют AVX-512, хотя рекомпилятор dynarmic из Arm может результативно задействовать многие инструкции AVX-512. Dynarmic используется эмулятором  Citra от 3DS, а также эмулятором Yuzu от Nintendo Switch и эмулятором Vita3K от PS Vita. Не знаю, предпринимались ли попытки расставить бенчмарки и сравнить целевые платформы AVX2 и AVX-512 для каких-либо из этих эмуляторов, но предположу, что разрыв между ними меньше, чем в случае с RPCS3, поскольку ядра Arm поддерживают как векторные, так и скалярные инструкции. Поскольку в типичной игре больше времени будет тратиться на скалярные инструкции, чем на векторные, потенциальный выигрыш от оптимизации векторных инструкций будет не так велик. В случае с RPCS3 серьёзная причина, по которой такие векторные оптимизации оказываются столь эффективны, заключается в следующем: синергичные процессорные блоки поддерживают операции лишь над векторными регистрами, поэтому всё время, затрачиваемое на эмуляцию SPU, расходуется на выполнение векторных инструкций. PCSX2 – как раз такой эмулятор, который, вероятно, серьёзно выиграет от оптимизаций AVX-512. Поскольку элементы векторной обработки из PS2 во многом повлияли на поведение и устройство синергичных процессорных блоков, многие оптимизации, применимые к RPCS3, также должны быть актуальны для PCSX2. В частности, vrangeps должна быть полезна для улучшения прикрепляющего кода. Надеюсь, этот пост помог вам немного разобраться в том, почему инструкции AVX-512 полезны для RPCS3. Здесь я не останавливался на каждой из инструкций AVX-512, которая могла бы пригодиться при работе с RPCS3, а заострил внимание на тех, что кажутся мне наиболее интересными.