Нарушение производительности: денормализованные числа против ошибочных предсказаний ветвлений - PullRequest
1 голос
/ 01 апреля 2020

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

float calc(float y, float z)
{ return sqrt(y * y + z * z) / 100; }

Где y и z могут быть ненормальными числами, давайте предположим две возможные ситуации, когда просто y, просто z или, может быть, оба, совершенно случайным образом, могут быть денормальными числами

  • 50% времени
  • <1% времени </li>

А теперь предположим, что я хочу избежать снижения производительности при работе с ненормальными числами, и я просто хочу рассматривать их как 0, и Я изменяю этот фрагмент кода следующим образом:

float calc(float y, float z)
{
   bool yzero = y < 1e-37;
   bool zzero = z < 1e-37;
   bool all_zero = yzero and zzero;
   bool some_zero = yzero != zzero;

   if (all_zero)
      return 0f;

   float ret;

   if (!some_zero) ret = sqrt(y * y + z * z);
   else if (yzero) ret = z;
   else if (zzero) ret = y;

   return ret / 100;
}

Что будет хуже, снижение производительности за неправильное предсказание ветвления (для случаев 50% или <1%) или снижение производительности за работу с ненормальными числами? </p>

Чтобы правильно интерпретировать, какие операции могут быть нормальными или ненормальными в предыдущем фрагменте кода, я также хотел бы получить некоторые однорядные, но совершенно необязательные ответы ab из следующих тесно связанных вопросов:

float x = 0f; // Will x be just 0 or maybe some number like 1e-40;
float y = 0.; // I assume the conversion is just thin-air here and the compiler will see just a 0.
0; // Is "exact zero" a normal or a denormal number?
float z = x / 1; // Will this "no-op" (x == 0) cause z be something like 1e-40 and thus denormal?
float zz = x / c; // What about a "no-op" operating against any compiler-time constant?
bool yzero = y < 1e-37; // Have comparisions any performance penalty when y is denormal or they don't?

1 Ответ

4 голосов
/ 01 апреля 2020

HW поддерживает это бесплатно во многих ISA, включая x86, см. Ниже: FTZ / DAZ. Большинство компиляторов устанавливают эти флаги во время запуска, когда вы компилируете с -ffast-math или эквивалентным.

Также обратите внимание, что ваш код не может избежать штрафа (на HW, где он есть) в некоторых случаях: y * y или z * z может быть ненормальным для небольших, но нормализованных y или z. ( Хороший улов, @ chtz ). Показатель y*y в два раза больше показателя y, более отрицательный или более положительный. С 23 явными битами мантиссы в float, это примерно 12 значений экспоненты, которые являются квадратными корнями субнормальных значений, и не опустятся полностью до 0.

Квадрат субнормального всегда приводит к потере 0; субнормальный вход может иметь меньше шансов, чем субнормальный выход для умножения, я не знаю. Наличие субнормального штрафа или нет может варьироваться в зависимости от операции в пределах одной микроархитектуры, например, сложение / суб против умножения или деления.

Кроме того, любой отрицательный y или z обрабатывается как 0, что, вероятно, является ошибкой, если ваши входные данные не известны как отрицательные.

если результаты могут варьироваться так широко, микроархитектура x86 будет моим основным вариантом использования

Да, штрафы (или их отсутствие) сильно различаются.

Исторически (семейство P6) Intel всегда использовала очень медленную помощь при микрокодировании для субнормальных результатов и субнормальных входных данных, в том числе для сравнений. Современные процессоры Intel (семейство Sandybridge) выполняют некоторые, но не все операции FP с субнормальными операндами, не нуждаясь в помощи микрокода. (событие перфекта fp_assists.any)

Микрокод ассистент похож на исключение и очищает конвейер вне очереди и занимает более 160 циклов в семействе SnB, против ~ 10-20 для промах ветки. И ветки имеют "быстрое восстановление" на современных процессорах. Истинный штраф за промах ветки зависит от окружающего кода например, если условие ветвления действительно слишком поздно, чтобы быть готовым, это может привести к отбрасыванию большого количества последующей независимой работы. Но помощь с микрокодами все еще, вероятно, хуже, если вы ожидаете, что это будет происходить часто.

Обратите внимание, что вы можете проверить на ненормальное значение, используя целочисленные операции: просто проверьте поле экспоненты для всех нулей (и мантиссу для ненулевых : кодирование со всеми нулями для 0.0 технически является частным случаем субнормального). Таким образом, вы можете вручную обнулить sh до нуля с помощью целочисленных операций SIMD, таких как andps / pcmpeqd / andps

Микроарх PDF Agner Fog имеет некоторые Информация; он упоминает об этом в целом без подробного разбивки по каждому уарху. Я не думаю, что https://uops.info/ тесты для нормальных и субнормальных, к сожалению, к сожалению.

У Knight's Landing (KNL) есть только субнормальные штрафы за деление, но не добавление / муль. Как и графические процессоры, они использовали подход, который благоприятствовал пропускной способности по сравнению с задержкой и имел достаточное количество этапов конвейера в своем FPU для обработки субнормалей в аппаратном эквиваленте без ветвлений. Несмотря на то, что это может означать более высокую задержку для каждой операции FP.

AMD Bulldozer / Piledriver имеет штраф ~ 175 циклов за результаты, которые являются «ненормальными или недостаточными», если не установлена ​​FTZ. Агнер не упоминает субнормальные входы. Steamroller / Excavator не имеет штрафов.

AMD Ryzen (из PDF-файла Agner Fog)

Операции с плавающей запятой, которые дают ненормальный результат, требуют несколько дополнительных тактов. То же самое имеет место, когда умножение или деление уменьшается до нуля. Это намного меньше, чем высокий штраф за бульдозер и пиледривер. Нет никакого штрафа, когда режимы flu sh to-zero и режим denormals-are-zero оба включены.

В отличие от этого семейство Intel Sandybridge (по крайней мере Skylake) не штрафы за результаты, которые недостижимы вплоть до 0,0.

Intel Silvermont (Atom) от микроарча Агнера Фога pdf

Операции, которые имеют ненормальные числа в качестве входных или выходных данных или генерируют недостаточный поток, занимают приблизительно 160 тактов, если только не используются режимы flu sh to-zero и denormals-are-zero.

Это будет включать сравнение.


Я не знаю деталей для любых микроархитектур, отличных от x86, таких как ARM cortex-a76 или RIS C -V, чтобы выбрать пару случайных примеров, которые могут также быть актуальным. Штрафы за неправильное предсказание также сильно различаются, в зависимости от простых конвейеров упорядочения по сравнению с глубокими OoO exe c процессорами, такими как современный x86. Истинный штраф за неправильный прогноз также зависит от окружающего кода.


А теперь предположим, что я хочу избежать снижения производительности при работе с ненормальными числами, и я просто хочу рассматривать их как 0

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

Некоторые / большинство (?) Современных FPU (включая x86 SSE) но не унаследованный x87) позволяет обрабатывать субнормалы (иначе говоря, денормали) как ноль бесплатно, поэтому эта проблема возникает, только если вы хотите, чтобы это поведение выполнялось для некоторых функций, но не всех, в одном потоке. И со слишком мелкозернистым переключением, чтобы стоить менять регистр управления FP на FTZ и обратно.

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

Некоторые процессоры x86 даже переименовывают в MXCSR , поэтому при изменении режима округления или FTZ / DAZ может не потребоваться утечка неупорядоченных обратных конец. Это все еще не дешево, и вы не захотите делать это каждые несколько инструкций FP.

ARM также поддерживает аналогичную функцию: субнормальные числа с плавающей запятой IEEE 754 поддерживаются на iOS устройствах ARM (iPhone 4) - но, по-видимому, настройка по умолчанию для ARM VFP / NEON состоит в том, чтобы обрабатывать субнормалы как ноль, отдавая предпочтение производительности по сравнению со строгим соответствием IEEE.

См. Также flu sh -to-zero поведение в арифметике с плавающей точкой c о кроссплатформенной доступности этого.


На x86 механизмом c является то, что вы устанавливаете биты DAZ и FTZ в регистр MXCSR (математический регистр управления SSE FP; также имеются биты для режима округления FP, масок исключений FP и битов состояния липких маскированных исключений FP). https://software.intel.com/en-us/articles/x87-and-sse-floating-point-assists-in-ia-32-flush-to-zero-ftz-and-denormals-are-zero-daz показывает схему, а также обсуждает некоторые эффекты производительности на старых процессорах Intel. Много хорошего фона / введение.

Компиляция с -ffast-math свяжет некоторый дополнительный код запуска, который устанавливает FTZ / DAZ перед вызовом main. IIR C, Потоки наследуют настройки MXCSR из основного потока в большинстве операционных систем.

  • DAZ = Денормали - ноль, обрабатывает входные субнормалы как ноль. Это влияет на сравнение (независимо от того, испытали бы они замедление) и делает невозможным даже определить разницу между 0 и субнормальным, кроме использования целочисленных данных в битовой структуре.
  • FTZ = Flu sh В ноль, субнормальные выходы из расчетов просто занижены до нуля. т.е. отключить постепенное снижение нагрузки. (Обратите внимание, что умножение двух небольших нормальных чисел может привести к потере значения. Я думаю, что добавление / к югу от нормальных чисел, мантиссы которых исключаются, за исключением младших битов, может также привести к ненормальному.)

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


Specifi c random вопросы:

float x = 0f; // Will x be just 0 or maybe some number like 1e-40;

Это синтаксическая ошибка. Предположительно, вы имеете в виду, что 0.f или 0.0f

0.0f является точно представимым (с битовым шаблоном 0x00000000) как поплавок IEEE binary32, так что это определенно то, что вы получите на любой платформе, использующей IEEE FP. Вы не будете случайным образом получать субнормалы, которые не написали.

float z = x / 1; // Will this "no-op" (x == 0) cause z be something like 1e-40 and thus denormal?

Нет, IEEE754 не позволяет 0.0 / 1.0 давать что-либо кроме 0.0.

Опять же, субнормалы не появляются из воздуха. Округление «ошибка» происходит только тогда, когда точный результат не может быть представлен как число с плавающей запятой или двойное число. Максимально допустимая ошибка для операций IEEE «basi c» (* / + - и sqrt ) составляет 0,5 ulp, то есть точный результат должен быть правильно округлен до ближайшего представимого значения FP, вплоть до последнего di git мантиссы.

 bool yzero = y < 1e-37; // Have comparisons any performance penalty when y is denormal or they don't?

Может быть, возможно, нет. Нет штрафов для последних AMD или Intel, но медленный на Core 2. Например,

Обратите внимание, что 1e-37 имеет тип double и приведет к повышению от y до double. Вы можете надеяться, что это позволит избежать субнормальных штрафов по сравнению с использованием 1e-37f. Subnormal float-> int не имеет штрафов на Core 2, но, к сожалению, cvtss2sd все еще имеет большой штраф на Core 2. ( GCC / clang не оптимизирует преобразование даже при -ffast-math, хотя я думаю, что они могли бы это сделать, потому что 1e-37 точно представлен как квартира, и каждое субнормальное число с плавающей точкой может быть точно представлено как нормализованный дубль. Таким образом, повышение до double всегда точное и не может изменить результат).

В Intel Skylake сравнение двух субнормалей с vcmplt_oqpd не приводит к замедлению, а также с ucomisd в целочисленные флаги. Но в Core 2 оба медленные.

Сравнение, если оно выполняется как вычитание, должно смещать входы, чтобы выровнять их двоичные значения-места, и подразумеваемое ведущее di git мантиссы является 0 вместо 1, поэтому субнормалы - это особый случай. Таким образом, аппаратное обеспечение может решить не обрабатывать это по быстрому пути, а вместо этого воспользоваться помощью микрокода. Старые аппаратные средства x86 могут обрабатывать это медленнее.

Это может быть сделано по-другому, если вы построите специальный ALU сравнения, отдельный от обычного модуля add / sub. Битовые структуры с плавающей запятой можно сравнивать как целые числа знака / величины (с особым случаем для NaN), поскольку для этой работы выбрано смещение показателя IEEE. (т.е. nextafter - это просто целое число ++ или - в битовой комбинации). Но это, очевидно, не то, что делает аппаратное обеспечение.


Преобразование FP в целое число выполняется быстро даже в Core 2, однако. cvt[t]ps2dq или эквивалент в pd, конвертированный с плавающей запятой / double в int32 с усечением или текущим режимом округления. Так, например, эта недавняя предложенная оптимизация LLVM безопасна на Skylake и Core 2 , согласно моим тестам.

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

Но умножение нормальных чисел для получения ненормального результата имеет штраф даже на Скайлэйке (~ в 150 раз медленнее).

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