Какова относительная скорость добавления с плавающей запятой по сравнению с умножением с плавающей запятой

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

Это все еще так, или современные компьютерные архитектуры продвинулись до такой степени, что *,/ больше не во много раз медленнее, чем +,- ?

Чтобы быть конкретным, меня интересует скомпилированный код C/C++, работающий на современных типичных чипах x86 с обширным встроенным оборудованием с плавающей запятой, а не маленькая микропрограмма, пытающаяся сделать FP в программном обеспечении. Я понимаю, что конвейерная обработка и другие архитектурные усовершенствования исключают подсчет конкретных циклов, но я все же хотел бы получить полезную интуицию.


person J. Peterson    schedule 18.07.2009    source источник
comment
Связанный: Деление с плавающей запятой и умножение с плавающей запятой сравнивает вещи на современных микроархитектурах x86. TL:DR divps может быть приемлемым для пропускной способности, если вы не делаете это очень часто, и всегда имеет гораздо большую задержку.   -  person Peter Cordes    schedule 30.10.2018


Ответы (6)


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

Например, возьмите этот цикл:

for(int j=0;j<NUMITER;j++) {
  for(int i=1;i<NUMEL;i++) {
    bla += 2.1 + arr1[i] + arr2[i] + arr3[i] + arr4[i] ;
  }
}

для NUMITER=10^7, NUMEL=10^2, оба массива инициализируются небольшими положительными числами (NaN намного медленнее), это занимает 6,0 секунд с использованием удвоений в 64-битной процедуре. Если я заменю цикл на

bla += 2.1 * arr1[i] + arr2[i] + arr3[i] * arr4[i] ;

Это занимает всего 1,7 секунды... поэтому, поскольку мы "переусердствовали" с добавлениями, мулы были по существу бесплатными; и уменьшение добавок помогло. Это становится более запутанным:

bla += 2.1 + arr1[i] * arr2[i] + arr3[i] * arr4[i] ;

-- то же распределение mul/add, но теперь константа добавляется, а не умножается -- занимает 3,7 секунды. Ваш процессор, вероятно, оптимизирован для более эффективного выполнения типичных числовых вычислений; так что скалярные произведения, такие как суммы muls и масштабированные суммы, примерно настолько хороши, насколько это возможно; добавление констант не так распространено, так что это медленнее...

bla += someval + arr1[i] * arr2[i] + arr3[i] * arr4[i] ; /*someval == 2.1*/

снова занимает 1,7 секунды.

bla += someval + arr1[i] + arr2[i] + arr3[i] + arr4[i] ; /*someval == 2.1*/

(то же, что и начальный цикл, но без дорогостоящего добавления констант: 2,1 секунды)

bla += someval * arr1[i] * arr2[i] * arr3[i] * arr4[i] ; /*someval == 2.1*/

(в основном muls, но одно дополнение: 1,9 секунды)

Итак, в основном; трудно сказать, что быстрее, но если вы хотите избежать узких мест, более важно иметь разумный микс, избегать NaN или INF, избегать добавления констант. Что бы вы ни делали, убедитесь, что вы тестируете и тестируете различные настройки компилятора, поскольку часто небольшие изменения могут иметь значение.

Еще несколько случаев:

bla *= someval; // someval very near 1.0; takes 2.1 seconds
bla *= arr1[i] ;// arr1[i] all very near 1.0; takes 66(!) seconds
bla += someval + arr1[i] * arr2[i] + arr3[i] * arr4[i] ; // 1.6 seconds
bla += someval + arr1[i] * arr2[i] + arr3[i] * arr4[i] ; //32-bit mode, 2.2 seconds
bla += someval + arr1[i] * arr2[i] + arr3[i] * arr4[i] ; //32-bit mode, floats 2.2 seconds
bla += someval * arr1[i]* arr2[i];// 0.9 in x64, 1.6 in x86
bla += someval * arr1[i];// 0.55 in x64, 0.8 in x86
bla += arr1[i] * arr2[i];// 0.8 in x64, 0.8 in x86, 0.95 in CLR+x64, 0.8 in CLR+x86
person Eamon Nerbonne    schedule 18.07.2009
comment
Сочетание инструкций — это хороший момент, у меня есть люди, с которыми я работаю, которые настаивают на том, что DSP с 200 операциями с плавающей запятой превосходит DSP с 600 операциями с фиксированной точкой. Они абсолютно не выполняют циклическую обработку и тратят больше времени на обработку ввода-вывода, чем на вычисления. Более быстрый процессор с фиксированной точкой выиграл бы, основываясь на общем наборе инструкций, но люди просто думают, что единицы FP — это магия, а не аппаратная реализация структуры данных. - person NoMoreZealots; 18.07.2009
comment
хорошее объяснение с интуитивными примерами! - person Sebastian Good; 11.09.2009

Теоретически информация здесь:

Справочное руководство по оптимизации архитектур Intel® 64 и IA-32, ПРИЛОЖЕНИЕ C. ЗАДЕРЖКА И ПРОПУСКНАЯ СПОСОБНОСТЬ ИНСТРУКЦИИ< /а>

Для каждого процессора, который они перечисляют, задержка на FMUL очень близка к задержке FADD или FDIV. На некоторых старых процессорах FDIV медленнее в 2-3 раза, а на новых процессорах так же, как FMUL.

Предостережения:

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

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

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

person Scott McIntyre    schedule 18.07.2009
comment
Очень крутой документ. Я думаю, что одна вещь, которая остается неизменной (и этот документ показывает это), заключается в том, что деление все еще намного медленнее, чем умножение, сложение и вычитание. Судя по этому документу, задержка деления с двойной точностью в 10 раз меньше, чем умножения. Так, например, я считаю, что вызов x = y * 0,5 должен быть быстрее, чем вызов x = y/2. - person Steve Wortham; 20.07.2009
comment
@SteveWortham Не могли бы вы указать страницу, на которой вы нашли информацию о том, что fdiv в 10 раз медленнее, чем fmul? - person 0fnt; 11.05.2012
comment
То же самое в основном по-прежнему верно для современной x86 с математикой SSE2 FP. addsd и mulsd очень похожи, а divsd имеет более высокую задержку и намного худшую пропускную способность. Деление с плавающей запятой и умножение с плавающей запятой. (Пропускная способность Haswell/Broadwell в два раза выше, чем умножения, но задержка добавления по крайней мере не хуже, чем умножения. Так что это странно до тех пор, пока Skylake, когда add/mul не работают одинаково на одних и тех же исполнительных модулях FMA, оба с задержкой 4c с 2 на каждый тактовая пропускная способность. agner.org/optimize) - person Peter Cordes; 05.09.2018

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

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

Если вас интересует конкретно производительность инструкций типа DIV/MUL/ADD/SUB, вы можете даже добавить какую-то встроенную сборку, чтобы конкретно контролировать, какие варианты этих инструкций выполняются. Однако вам нужно убедиться, что вы держите несколько исполнительных блоков занятыми, чтобы получить хорошее представление о производительности, на которую способна система.

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

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

Базовая архитектура +- идентична. Таким образом, они логически занимают одинаковое время для вычисления. * с другой стороны, для выполнения одной операции требуется несколько слоев, обычно состоящих из «полных сумматоров». Это гарантирует, что, хотя символ * может быть выдан конвейеру в каждом цикле, он будет иметь более высокую задержку, чем схема сложения/вычитания. Операция fp / обычно реализуется с использованием метода аппроксимации, который итеративно сходится к правильному ответу с течением времени. Эти типы приближений обычно реализуются посредством умножения. Таким образом, для плавающей запятой вы обычно можете предположить, что деление займет больше времени, потому что нецелесообразно «разворачивать» умножение (которое уже является большой схемой само по себе) в конвейер множества схем умножения. Тем не менее производительность данной системы лучше всего измеряется с помощью тестирования.

person NoMoreZealots    schedule 18.07.2009

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

person Alex Martelli    schedule 18.07.2009

Разница в скорости */vs+ - зависит от архитектуры вашего процессора. Вообще и с х86 в частности разница в скорости стала меньше с современными процессорами. * должен быть близок к +, если сомневаетесь: просто поэкспериментируйте. Если у вас есть действительно сложная проблема с большим количеством операций FP, рассмотрите возможность использования вашего графического процессора (GeForce, ...), который работает как векторный процессор.

person Community    schedule 18.07.2009

Вероятно, разница во времени между умножением и сложением очень мала. с другой стороны, деление все еще значительно медленнее, чем умножение, из-за его рекурсивного характера. в современной архитектуре x86 инструкции sse следует учитывать при выполнении операций с плавающей запятой, а не при использовании fpu. Хотя хороший компилятор C/C++ должен дать вам возможность использовать sse вместо fpu.

person hacim    schedule 18.07.2009