Какова относительная скорость сложения с плавающей запятой и умножения с плавающей запятой?
Десять или два года назад стоило написать числовой код, чтобы не использовать умножения и деления, а вместо этого использовать сложение и вычитание. Хорошим примером является использование прямых разностей для оценки полиномиальной кривой вместо непосредственного вычисления полинома.
Это все еще так, или современные компьютерные архитектуры продвинулись до такой степени, что *,/ уже не во много раз медленнее, чем +,-?
Если быть точным, меня интересует скомпилированный код C/C++, работающий на современных типичных чипах x86 с обширным встроенным аппаратным обеспечением с плавающей запятой, а не небольшой микропроцессор, пытающийся выполнять FP в программном обеспечении. Я понимаю, что конвейерная обработка и другие архитектурные усовершенствования исключают конкретное число циклов, но я все же хотел бы получить полезную интуицию.
6 ответов
Это также зависит от набора команд. Ваш процессор будет иметь несколько вычислительных блоков в любое время, и вы получите максимальную пропускную способность, если все они заполнены все время. Таким образом, выполнение цикла mul's так же быстро, как выполнение цикла или добавления - но то же самое не выполняется, если выражение становится более сложным.
Например, возьмем этот цикл:
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
В теории информация здесь:
Для каждого процессора, который они перечисляют, задержка на FMUL очень близка к задержке FADD или FDIV. На некоторых старых процессорах FDIV в 2-3 раза медленнее, чем на новых, а на более новых - то же, что и FMUL.
Предостережения:
Документ, на который я ссылаюсь, на самом деле говорит, что вы не можете полагаться на эти цифры в реальной жизни, так как процессор будет делать то, что он хочет, чтобы все было быстрее, если это правильно.
Вероятность того, что ваш компилятор решит использовать один из множества новых наборов инструкций, которые имеют умножение / деление с плавающей точкой, велика.
Это сложный документ, предназначенный только для чтения авторами компилятора, и я мог ошибиться. Как я не понимаю, почему число задержек FDIV полностью отсутствует для некоторых процессоров.
Лучший способ ответить на этот вопрос - на самом деле написать тест / профиль обработки, которую вам нужно сделать. Эмпирическое должно использоваться по сравнению с теоретическим, когда это возможно. Особенно, когда это легко достичь.
Если вы уже знакомы с различными реализациями Math, которые вам нужны, вы можете написать несколько различных кодов передачи математики и посмотреть, где пики вашей производительности. Это позволит процессору / компилятору генерировать различные потоки выполнения, чтобы заполнить конвейеры процессора и дать вам конкретный ответ на ваш ответ.
Если вас интересует, в частности, выполнение инструкций типа DIV/MUL/ADD/SUB, вы можете даже добавить некоторые встроенные сборки, чтобы конкретно контролировать, какие варианты этих инструкций выполняются. Однако вам необходимо убедиться, что вы заняты несколькими исполнительными блоками, чтобы получить представление о производительности, на которую способна система.
Выполнение чего-либо подобного позволит вам сравнить производительность на нескольких вариациях процессора, просто запустив на них одну и ту же программу, а также позволит учесть различия между материнскими платами.
Редактировать:
Базовая архитектура +- идентична. Таким образом, они логически занимают одно и то же время для вычисления. * с другой стороны, для выполнения одной операции требуется несколько слоев, обычно построенных из "полных сумматоров". Это гарантирует, что хотя a * может выдаваться конвейеру каждый цикл, он будет иметь большую задержку, чем схема сложения / вычитания. Операция fp / операция обычно реализуется с использованием метода аппроксимации, который итеративно сходится к правильному ответу с течением времени. Эти типы аппроксимаций обычно реализуются с помощью умножения. Таким образом, для числа с плавающей запятой обычно можно предположить, что деление займет больше времени, поскольку нецелесообразно "развертывать" умножения (которые уже сами по себе являются большими цепями) в конвейер множества схем умножителей. Тем не менее, производительность данной системы лучше всего измерять с помощью тестирования.
Я не могу найти точную ссылку, но обширные эксперименты говорят мне, что умножение с плавающей точкой в настоящее время примерно равно скорости сложения и вычитания, в то время как деление - нет (но не "во много раз медленнее"). Вы можете получить желаемую интуицию, только запустив свои собственные эксперименты - не забудьте заранее сгенерировать случайные числа (их миллионы), прочитать их до того, как вы начнете отсчитывать время, и использовать счетчики производительности ЦП (без запуска других процессов, так как насколько вы можете их остановить) для точного измерения!
Разница в скорости * / vs + - зависит от архитектуры вашего процессора. Вообще и с x86 в частности разница в скорости стала меньше у современных процессоров. * должен быть близок к +, если сомневаетесь: просто экспериментируйте. Если у вас действительно тяжелая проблема с большим количеством операций FP, также подумайте об использовании вашего графического процессора (GeForce, ...), который работает как векторный процессор.
Вероятно, существует очень небольшая разница во времени между умножением и сложением. деление, с другой стороны, все еще значительно медленнее, чем умножение из-за его рекурсивного характера. В современной архитектуре x86 инструкции sse следует учитывать при выполнении операций с плавающей запятой, а не при использовании fpu. Хотя хороший компилятор C/C++ должен дать вам возможность использовать sse вместо fpu.