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

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

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

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

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

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*/

(в основном мулы, но одно дополнение: 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

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

Intel®64 и IA-32 архитектура оптимизация справочное руководство, приложение c задержка и пропускная способность

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

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

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

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

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


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

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

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

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

Edit:

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


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


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


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