Я реализую режим рандомизированного округления для оценки числовой стабильности вычислений с плавающей запятой (FP) в коде C99. Это идея этой работы по вычислительной химии, но обновленная для обработки инструкций SSE и AVX, общих для последних архитектур x86_64. Рандомизированное округление вводит ассемблерные инструкции, чтобы задать режим округления либо округления до +inf, либо округления до -inf способом, характерным для каждой инструкции FP, случайным образом. При нескольких прогонах вы получаете образцы распределения возможных результатов.
Чтобы сделать это правильно, мне нужно знать, какие инструкции включают округление FP. Кажется, существует 4-сторонняя классификация:
movq
, addq
, imulq
movaps
или выполнение некоторых вычислений значений FP, но без округления, например. орпыfldcw
), например. добавляет, sqrtss
Я могу каталогизировать все инструкции, которые я вижу в заданном фрагменте сгенерированной сборки, и читать документацию (например, приведенную здесь), чтобы узнать, к какой категории относится данная инструкция, но это медленно и подвержено ошибкам, и я не не знаю, будет ли другой код или другие настройки компилятора нацелены на инструкции, которые я еще не классифицировал вручную.
Как проще всего, учитывая инструкцию x86, классифицировать ее, как указано выше? Где-то уже есть список с эквивалентной информацией? Или есть советы по поиску на страницах https://www.felixcloutier.com/x86/, которые могут автоматически определить эту классификацию?
Было бы очень больно, если бы вы просто вставляли рандомизированные установщики режима округления перед каждой инструкцией, заканчивающейся на pd
, sd
, ps
, ss
?
Я отредактировал конец своего вопроса, чтобы уточнить, что я специально не запрашиваю сторонние ресурсы; Я просто хочу знать, как классифицировать инструкции x86. Основываясь на вашем комментарии @fuz, я просмотрел ref.x86asm.net/geek.html, который основан на XML-описании всех инструкций, но я не уверен, как его использовать. cvtps2ps и roundss, например, оба находятся в "grp2" "convert", но очень разные в том, как они выбирают округление (неявное или явное, соответственно).
@chtz хорошая простая идея! Я все обдумаю. Но я обнаружил, что инструкции, чувствительные к округлению, составляют незначительное меньшинство инструкций FP в моей сборке (большинство из них представляют собой различные виды перемещений и сравнений), так что это может быть большим ударом по скорости, но я не знаю. пока не попробую. Также это не касается таких вещей, как roundss, которые требуют явного округления.
@GordonKindlmann Если производительность имеет значение, вы, конечно, все еще можете отфильтровать некоторые очень распространенные инструкции без округления (movaps
, andps
, ...). Если у вас осталось всего несколько ложных срабатываний, это не должно сильно повредить.
TL:DR: Я думаю, что должен работать поиск инструкций, которые могут вызвать исключение «точности»: это означает, что они выполняют округление. Но вы можете не захотеть переопределять режим округления для инструкций, которые округляют или преобразовывают в целое число с текущим режимом округления, это может быть слишком большой ошибкой округления.
@chtz предлагает полезную эвристику изменения округления перед каждой инструкцией ss
/sd
/ps
/pd
, за исключением нескольких распространенных, которые вы заносите в черный список, таких как [v]movaps/d
, [v]movups/d
, [v]movss/d
, [v]shuf*
, [v]blend*
, [v]perm*
, побитовые логические значения и т. д. Также [v]ucomis/d
/ [v]comis/d
и упакованный-SIMD сравнивается как [v]cmpps
; Я думаю, что только денормалы равны нулю будут влиять на сравнение, а не на фактические режимы округления.
Возможно, вы захотите убедиться, что режим округления является ближайшим — даже для преобразования в целое число, например [v]cvtps2dq
, или вообще для всех преобразований в / из целого числа, которые все начинаются с мнемоники cvt
, хотя, возможно, все же рандомизированы для двойного числа с плавающей запятой ([v]cvtpd2ps
). int->fp должен округляться для больших нечетных целых чисел, например больше 2^23 для числа с плавающей запятой.
Использование кодировки EVEX AVX-512 для переопределения режима округления для каждой инструкции без изменения mxcsr
может значительно повысить производительность, если у вас есть процессор с поддержкой AVX-512. Смотри ниже.
Базы инструкций:
Если вы хотите использовать что-то подобное, я думаю, это должно сработать, чтобы проверить возможность вызвать исключение FP Precision, также известное как inexact. Это возможно только в том случае, если инструкция может иногда давать результат, который необходимо округлить. (Обычно все исключения FP маскируются, поэтому они только устанавливают бит в MXCSR для записи того, что они произошли. Мы по-прежнему говорим о «возбуждении» исключения FP независимо от того, маскированы ли они, с пониманием того, что обычно это просто установка фиксированного бит в среде FP, фактически не перехватывая и не запуская обработчик исключений и не заставляя ОС убивать ваш процесс с помощью SIGFPE.)
Несколько инструкций могут округлять, но не вызывают исключений по этому поводу, например roundsd, когда режим округления переопределяется его немедленным. Но округление до целочисленного значения double обычно является частью алгоритма, который, как вы ожидаете, сломается, если вы замените nearbyint
на floor
, ceil
или trunc
. Обычно roundsd
используется с определенным режимом округления, указанным в непосредственном, вообще не используя MXCSR, но можно использовать текущий режим по умолчанию. (В этом случае он может вызвать исключение точности, поэтому оно будет отображаться в соответствии с этой эвристикой.)
Возможно, также оставьте cvtsd2si
и cvtps2dq
в покое, убедившись, что они используют ближайший-даже, как программа, ожидаемая для (int)nearbyint
или lrint
. (Предположим, что ваши программы не используют функции fenv
для установки режимов округления.)
Я не думаю, что есть какие-либо инструкции, затронутые режимом округления, которые никогда не могут вызвать исключение точности.
Рассмотрение того, возможны ли какие-либо исключения FP, - это еще один способ определить, является ли это «математической FP» инструкцией по сравнению с просто побитовой или перетасовкой или чем-то еще. Такие инструкции, как ucomisd
скалярное сравнение с двойной точностью, не могут вызвать много исключений (U для неупорядоченного означает, что один или оба операнда, являющиеся NaN, не вызывают беспокойства). Но он не подавляет SNaN, а только QNaN, и может вызвать денормальное исключение.
Я думаю, что именно неточное исключение, известное как точность, может быть полезной лакмусовой бумажкой. например
списки movaps Исключения SIMD с плавающей запятой: нет, потому что он вообще не выполняет математику.
списки addps Исключения SIMD с плавающей запятой: Переполнение, недополнение, недействительное, точное, денормальное.
cvtpd2dq списки Исключения SIMD с плавающей запятой ¶ Недействительно, точность. Упакованное преобразование из FP в целое число с использованием текущего режима округления. Вы, вероятно, захотите занести его в белый список как незащищенный, так как +-1 целое число намного дальше, чем +-1ulp.
списки ucomisd Исключения SIMD с плавающей запятой ¶
Недопустимый (если операнды SNaN), денормализованный. (И не более того; сравнения не округляются и не переполняются/не занижаются. А «u» означает неупорядоченный, что означает, что он не рассматривает тихий NaN как заслуживающий исключения, а только сигнализирует о NaN. Другие математические инструкции, такие как sqrtsd
, производят QNaN для недопустимых значений. входы, вы получите SNaN, только если создадите его вручную.)
«SIMD с плавающей запятой» означает, что он заботится о MXCSR, а не о регистрах управления/статуса x87. addss
также использует их; это скалярная инструкция, использующая регистры SIMD.
Надеюсь, ваша программа вообще не будет использовать какие-либо устаревшие инструкции x87, если вы скомпилируете для x86-64 и не используете 80-битную long double
. Но если вы это сделаете, режим округления и контроль точности в управляющем слове x87 (fldcw
) будут полностью отделены от режима округления SSE/AVX в MXCSR. (http://www.ray.masmcode.com/tutorial/index.html).
AVX-512 может переопределить округление для каждой инструкции, используя пару битов внутри префикса EVEX. Это намного эффективнее, чем частое изменение MXCSR, поэтому я рекомендую его, если у вас есть доступ к машине, поддерживающей AVX-512.
Инструкции по первому сохранению, хоть магазин + ldmxcsr
, а ldmxcsr
недешево, вроде 4 мкп на Intel SKX с пропускной способностью 3 такта. (по сравнению с пропускной способностью 0,5c для математики FP). Или на Zen4, 6 мкп с пропускной способностью 21 такт. Кроме того, если ЦП не переименует весь MXCSR, включая управление округлением, загрузка MXCSR должна будет сериализовать выполнение математических инструкций FP относительно. к этому. Я не уверен, что современные процессоры Intel, такие как Skylake или Alder Lake, переименовывают MXCSR; Я нашел пару ответов SO, в которых упоминается концепция:
Наблюдение за зависимостями регистров x86 цитирует некоторый документ Intel, в котором упоминается событие производительности для некоторой микроархитектуры: Циклы остановки переименования MXCSR — остановки из-за переименования регистра MXCSR, происходящего слишком близко к предыдущему переименованию MXCSR. Так что это плохо для вашего варианта использования, когда вы очень часто меняете его в коде, интенсивно использующем FP.
Что может означать resource_stall.other — некоторые упоминания о прилавках ресурсов MXCSR на Whiskey Lake, но IDK, если это подразумевает переименование или просто ожидание сброса старых инструкций при изменении режима округления.
Из-за ограничений кодирования встроенное округление доступно только для скалярных и 512-битных векторов, а не для 128 или 256. И только когда операнды являются регистрами, а не источником памяти.
Переопределение режима округления также подразумевает SAE, подавление всех исключений.
# GAS AT&T syntax
vaddss {rn-sae}, %xmm0, %xmm1, %xmm1 # nearest-even
vaddss {rz-sae}, %xmm0, %xmm1, %xmm1 # toward zero
vaddss {rd-sae}, %xmm0, %xmm1, %xmm1 # down
vaddss {ru-sae}, %xmm0, %xmm1, %xmm1 # up
vaddps {ru-sae}, %zmm0, %zmm1, %zmm1 # also available for 512-bit vectors
#vaddps {ru-sae}, %ymm0, %ymm1, %ymm1 # but not 128 or 256
#vaddss {rz-sae}, (%rsi), %xmm31, %xmm31 # nope, memory source operand
# vaddps {rz-sae}, (%rsi), %zmm31, %zmm31 # nope
Обратите внимание на необязательное {er}
в форме AVX-512 EVEX addss
(https://www.felixcloutier.com/x86/addss ), но только для 512-битной формы vaddps , а не EVEV xmm /ymm формы. См. также Длина вектора AVX512 и контроль SAE
Учитывая эти ограничения на то, когда его можно использовать, вам могут понадобиться некоторые запасные регистры для его использования. например загрузить во временный регистр, например x/zmm31, а затем использовать скалярную 512-битную инструкцию.
Так что, возможно, скомпилируйте с AVX2+FMA, оставив вам zmm16..31 доступными в качестве временных. (Которое можно испачкать, не беспокоясь vzeroupper
)
vsubps (%rsi), %xmm1, %xmm0 # 128-bit SIMD original
# round-down replacement
vmovups (%rsi), %xmm31
vsubps {rd-sae}, %zmm31, %zmm1, %zmm31 # write a 512-bit temporary
vmovaps %xmm31, %xmm0 # write the original destination with original width
----
vsubps %xmm2, %xmm1, %xmm0 # 128-bit SIMD original
# round-down replacement
vsubps {rd-sae}, %zmm2, %zmm1, %zmm31 # write a 512-bit temporary
vmovaps %xmm31, %xmm0 # write the original destination with original width
Я не уверен на 100%, что важно избегать записи %zmm0
на любых процессорах, если вы уже используете старшие половины ymm16..31, но я думаю, что это может зависеть от того, как именно выполняются переходы SSE/AVX и сохраненные верхние значения. реализовано.
vzeroupper
.Если оригинал использовал 256-битные векторы YMM, он будет использовать vzeroupper
в какой-то момент перед выполнением инструкций устаревшего SSE. В таком случае:
vsubps (%rsi), %ymm1, %ymm0 # original already using YMM upper halves
vmovups (%rsi), %ymm31 # replacement
vsubps {rd-sae}, %zmm31, %zmm1, %zmm0
Для скаляра это проще и с меньшими затратами на производительность: нам не нужно использовать какие-либо 512-битные векторные операции ZMM, поэтому у нас нет таких эффектов, как SIMD-инструкции, снижающие частоту процессора или отключение порта 1 для SIMD uops на процессорах Intel. Или для Zen4 512-битные uops занимают исполнительный блок за 2 такта. (Но по-прежнему являются однооперативными для внешнего интерфейса, что отлично подходит для узких мест внешнего интерфейса и неупорядоченного выполнения, позволяющего видеть далеко.)
vsubsd (%rsi), %xmm1, %xmm0 # scalar original
vmovsd (%rsi), %xmm31 # replacement still only needs scalar
vsubsd {rd-sae}, %xmm31, %xmm1, %xmm0
Это значительно увеличивает размер кода, например. objdump Разборка синтаксиса Intel:
0: c5 f3 5c 06 vsubsd xmm0,xmm1,QWORD PTR [rsi] # orig: 4 bytes, or 5 with some registers or instructions
4: 62 61 ff 08 10 3e vmovsd xmm31,QWORD PTR [rsi] # replacement, 2x 6B
a: 62 91 f7 38 5c c7 vsubsd xmm0,xmm1,xmm31{rd-sae}
x87 сложнее, потому что даже fld
/ fstp
(или версия любой другой инструкции с источником памяти) выполняет преобразование на лету между 80-битным внутренним форматом и 32/64-битным числом с плавающей запятой/двойным числом в памяти. Это связано с округлением при сохранении, но не при загрузке, поэтому на самом деле вам нужно рандомизировать режим округления x87 перед fst
/ fstp
, если только пунктом назначения не является m80, сохраняя внутренний формат без изменений.
Я не уверен, что произойдет, если биты контроля точности будут установлены на 24-битную точность мантиссы (32-битное число с плавающей запятой) при загрузке 64-битного двойного числа (53-битная мантисса). В руководстве об этом не упоминается, поэтому, вероятно, биты точности не вводят новые места для округления, а только влияют на то, какая точность требуется в первую очередь для ускорения вычислений, контролируя округление в тех случаях, когда округление потребуется даже с ними. установить с полной точностью.
#IS
Произошло опустошение или переполнение стека.
(может произойти с любой инструкцией FPU, которая перемещает стек регистров.)#IA
(Исходный операнд — это SNaN. Не происходит, если исходный операнд находится в формате двойной расширенной точности с плавающей запятой (FLD m80fp или FLD ST(i). Таким образом, очевидно fld
не рассматривает QNaN как заслуживающую исключения.#D
Исходный операнд является денормальным значением. Не происходит, если исходный операнд имеет формат с плавающей запятой двойной расширенной точности.fld
, но #IA
перечисляет «Исходный операнд — это значение SNaN, ∞ или неподдерживаемый формат». Плюс:
#U
(недополнение) Результат слишком мал для целевого формата.#P
(точность) Значение не может быть точно представлено в целевом формате.Спасибо @PeterCordes за все это. Ключевым моментом является проверка исключений точности. Кстати, статья по вычислительной химии 2011 года (ссылка на мой вопрос), вдохновляющая мой подход, специфична для инструкций x87 в конце вашего ответа. Их подход генерирует большую таблицу контрольных слов (инициализируемых один раз в соответствии со случайным начальным числом, полученным во время выполнения) для динамического задания направления округления для каждой инструкции. может быть, глупый вопрос: с вашим подходом к префиксу EVEX, специфичным для AVX, разве это не требует перекомпиляции для изменения направления округления?
@GordonKindlmann: Да, со встроенным округлением AVX-512 режим округления жестко запрограммирован в машинном коде. Я предполагал, что это то, что вы делаете, постобрабатывая вывод gcc -S
(плюс любые другие варианты, такие как -O3 -march=native -mno-avx512f -fno-trapping-math -fno-math-errno
). Но теперь, когда вы упомянули об этом, динамическая рандомизация для выполнения инструкции, вероятно, была бы численно более интересной, чем статическая рандомизация для каждой инструкции.
@GordonKindlmann: Да, вы можете создать большую таблицу и зациклиться на ней или запустить дешевый PRNG, такой как xorshift+, и объединить 2 его бита в фиксированное значение MXCSR, где биты RC уже очищены, так что просто и/или / хранит / ldmxcsr. Будем надеяться, что exec не по порядку сможет запустить эти инструкции, пока выполнение работы FP остановлено. IDK, который будет работать лучше. Основное преимущество запуска PRNG заключается в том, чтобы избежать промахов в кеше и уменьшить объем кеша, если код FP не настолько замедлен, что ему никогда не приходится ждать памяти.
К сожалению, запрашивать сторонние ресурсы не по теме на этом сайте. Я думаю, что Intel публикует файлы XML, описывающие набор инструкций. Вы должны быть в состоянии использовать их.