Я выполняю некоторую критическую для производительности работу на C ++, и в настоящее время мы используем целочисленные вычисления для задач, которые по своей сути являются плавающими, потому что «они быстрее». Это вызывает множество неприятных проблем и добавляет много надоедливого кода.
Теперь я помню, как читал о том, как вычисления с плавающей запятой были настолько медленными примерно в течение 386 дней, когда, как я полагаю, (IIRC) был необязательный сопроцессор. Но, конечно, в наши дни с экспоненциально более сложными и мощными процессорами нет никакой разницы в «скорости» при вычислении с плавающей запятой или целочисленных вычислениях? Тем более, что фактическое время расчета крошечное по сравнению с чем-то вроде остановки конвейера или извлечения чего-либо из основной памяти?
Я знаю, что правильный ответ - провести тест на целевом оборудовании, что было бы хорошим способом проверить это? Я написал две крошечные программы на C ++ и сравнил время их выполнения со «временем» в Linux, но фактическое время выполнения слишком непостоянно (не помогает, когда я работаю на виртуальном сервере). Если не проводить весь день за сотнями тестов, построением графиков и т. Д., Могу ли я что-нибудь сделать, чтобы получить разумный тест относительной скорости? Есть идеи или мысли? Я совершенно не прав?
Программы, которые я использовал следующие, они ни в коем случае не идентичны:
#include <iostream>
#include <cmath>
#include <cstdlib>
#include <time.h>
int main( int argc, char** argv )
{
int accum = 0;
srand( time( NULL ) );
for( unsigned int i = 0; i < 100000000; ++i )
{
accum += rand( ) % 365;
}
std::cout << accum << std::endl;
return 0;
}
Программа 2:
#include <iostream>
#include <cmath>
#include <cstdlib>
#include <time.h>
int main( int argc, char** argv )
{
float accum = 0;
srand( time( NULL ) );
for( unsigned int i = 0; i < 100000000; ++i )
{
accum += (float)( rand( ) % 365 );
}
std::cout << accum << std::endl;
return 0;
}
Заранее спасибо!
Изменить: платформа, о которой я забочусь, - это обычная x86 или x86-64, работающая на настольных компьютерах Linux и Windows.
Изменить 2 (вставлено из комментария ниже): в настоящее время у нас есть обширная база кода. На самом деле я столкнулся с обобщением, что мы «не должны использовать float, поскольку целочисленные вычисления выполняются быстрее» - и я ищу способ (если это даже правда) опровергнуть это обобщенное предположение. Я понимаю, что для нас было бы невозможно предсказать точный результат, если не сделаем всю работу и не профилируем ее потом.
В любом случае, спасибо за все ваши отличные ответы и помощь. Не стесняйтесь добавлять что-нибудь еще :).
источник
addl
замененаfadd
на). Единственный способ получить действительно хорошие результаты - это получить основную часть вашей реальной программы и профилировать различные ее версии. К сожалению, это может быть довольно сложно без особых усилий. Возможно, сообщение нам о целевом оборудовании и вашем компиляторе поможет людям, по крайней мере, дать вам уже существующий опыт и т. Д. Что касается использования целых чисел, я подозреваю, что вы могли бы создать своего родаfixed_point
шаблонный класс, который значительно упростил бы такую работу.float
скорость увеличивается, но обычноdouble
нет.Ответы:
Увы, я могу дать вам только ответ "смотря по обстоятельствам" ...
По моему опыту, есть много, много переменных для производительности ... особенно между целочисленными математиками и математикой с плавающей запятой. Она сильно различается от процессора к процессору (даже в пределах одного семейства, такого как x86), потому что разные процессоры имеют разную длину «конвейера». Кроме того, некоторые операции обычно очень просты (например, сложение) и имеют ускоренный маршрут через процессор, а другие (например, деление) занимают намного больше времени.
Другая большая переменная - это место, где находятся данные. Если вам нужно добавить только несколько значений, тогда все данные могут находиться в кеше, откуда их можно быстро отправить в ЦП. Очень и очень медленная операция с плавающей запятой, данные которой уже находятся в кеше, будет во много раз быстрее, чем целочисленная операция, когда целое число нужно скопировать из системной памяти.
Я предполагаю, что вы задаете этот вопрос, потому что работаете над приложением, критичным к производительности. Если вы разрабатываете для архитектуры x86 и вам нужна дополнительная производительность, вы можете изучить возможность использования расширений SSE. Это может значительно ускорить арифметические операции с плавающей запятой одинарной точности, поскольку одна и та же операция может выполняться одновременно с несколькими данными, а для операций SSE существует отдельный * банк регистров. (Я заметил, что во втором примере вы использовали «float» вместо «double», заставляя меня думать, что вы используете математику с одинарной точностью).
* Примечание. Использование старых инструкций MMX фактически замедлило бы выполнение программ, потому что эти старые инструкции фактически использовали те же регистры, что и FPU, что делает невозможным одновременное использование FPU и MMX.
источник
double
-precision FP. При наличии всего двух 64-битныхdouble
s на регистр потенциальное ускорение меньше, чемfloat
для кода, который хорошо векторизуется. Скалярноfloat
иdouble
используйте регистры XMM на x86-64, при этом устаревшая версия x87 используется только дляlong double
. (Итак, @ Dan: нет, регистры MMX не конфликтуют с обычными регистрами FPU, потому что обычный FPU на x86-64 является блоком SSE. MMX был бы бессмысленным, потому что если вы можете использовать целочисленный SIMD, вам нужно 16-байтовоеxmm0..15
вместо 8 -байтmm0..7
, а современные процессоры имеют пропускную способность MMX хуже, чем SSE.)Например (меньшие числа быстрее),
64-битный Intel Xeon X5550 @ 2,67 ГГц, gcc 4.1.2
-O3
32-битный двухъядерный процессор AMD Opteron (tm) 265 @ 1,81 ГГц, gcc 3.4.6
-O3
Как Дэн отметил , даже , как только вы нормализуют для тактовой частоты (которая может вводить в заблуждение себя в конвейерных конструкций), результаты будут отличаться друг от друга на основе архитектуры процессора (индивидуального ALU / FPU производительности , а также фактического количества АЛУ / FPUs в расчете на одного core в суперскалярных схемах, который влияет на количество независимых операций, которые могут выполняться параллельно - последний фактор не учитывается в приведенном ниже коде, поскольку все операции ниже являются последовательно зависимыми.)
Тест работы бедняков FPU / ALU:
источник
volatile
чтобы убедиться. На Win64, ФП не используется и MSVC не будет генерировать код для него, поэтому он компилируется с использованиемmulss
иdivss
инструкции XMM там, которые 25x быстрее , чем FPU в Win32. Тестовая машина - Core i5 M 520 @ 2,40 ГГцv
очень быстро достигнет 0 или +/- inf, что может или не может (теоретически) рассматриваться как особый случай / fastpatheed некоторыми реализациями fpu.v
). В последних разработках Intel разделение вообще не конвейерно (divss
/divps
имеет задержку 10–14 циклов и такую же обратную пропускную способность).mulss
однако это задержка в 5 циклов, но может выдавать один за каждый цикл. (Или два за цикл на Haswell, поскольку порт 0 и порт 1 имеют множитель для FMA).Вероятно, будет значительная разница в реальной скорости между математическими вычислениями с фиксированной и плавающей запятой, но теоретическая пропускная способность ALU и FPU в лучшем случае совершенно не имеет значения. Вместо этого количество целочисленных регистров и регистров с плавающей запятой (реальных регистров, а не имен регистров) в вашей архитектуре, которые иначе не используются в ваших вычислениях (например, для управления циклом), количество элементов каждого типа, которые помещаются в строку кэша возможна оптимизация, учитывая различную семантику для целочисленной математики и математики с плавающей запятой - эти эффекты будут преобладать. Зависимости данных вашего алгоритма играют здесь значительную роль, поэтому никакое общее сравнение не сможет предсказать разницу в производительности для вашей проблемы.
Например, сложение целых чисел является коммутативным, поэтому, если компилятор видит цикл, аналогичный тому, который вы использовали для теста (при условии, что случайные данные были подготовлены заранее, чтобы они не скрывали результаты), он может развернуть цикл и вычислить частичные суммы с помощью никаких зависимостей, затем добавьте их, когда цикл завершится. Но с плавающей запятой компилятор должен выполнять операции в том же порядке, который вы запрашивали (у вас есть точки последовательности, поэтому компилятор должен гарантировать тот же результат, что запрещает переупорядочивание), поэтому существует сильная зависимость каждого добавления от результат предыдущего.
Вероятно, вы также поместите в кеш больше целочисленных операндов за раз. Таким образом, версия с фиксированной точкой может на порядок превзойти версию с плавающей запятой даже на машине, где FPU теоретически имеет более высокую пропускную способность.
источник
Добавление происходит намного быстрее
rand
, поэтому ваша программа (особенно) бесполезна.Вам необходимо определить горячие точки производительности и постепенно модифицировать вашу программу. Похоже, у вас есть проблемы со средой разработки, которые необходимо решить в первую очередь. Невозможно запустить вашу программу на ПК из-за небольшого набора проблем?
Как правило, выполнение заданий FP с помощью целочисленной арифметики - рецепт медленного.
источник
timespec_t
или что-то подобное. Запишите время в начале и в конце цикла и определите разницу. Затем выведитеrand
генерацию данных из цикла. Убедитесь, что ваш алгоритм получает все данные из массивов и помещает все свои данные в массивы. Это само по себе получает ваш фактический алгоритм и получает настройки, malloc, печать результатов, все, кроме переключения задач и прерываний, из вашего цикла профилирования.TIL Это варьируется (много). Вот некоторые результаты с использованием компилятора gnu (кстати, я также проверил, компилируя на машинах, gnu g ++ 5.4 от xenial чертовски быстрее, чем 4.6.3 от linaro, если точнее)
Intel i7 4700MQ xenial
Intel i3 2370M имеет похожие результаты
Intel (R) Celeron (R) 2955U (Chromebook Acer C720 под управлением xenial)
DigitalOcean 1GB Droplet Intel (R) Xeon (R) CPU E5-2630L v2 (работает надежно)
Процессор AMD Opteron (tm) 4122 (точный)
Это использует код из http://pastebin.com/Kx8WGUfg как
benchmark-pc.c
Я выполнил несколько проходов, но похоже, что общие числа совпадают.
Одним примечательным исключением является ALU mul vs FPU mul. Сложение и вычитание кажутся тривиальными.
Вот диаграмма выше (щелкните, чтобы увидеть полный размер, чем меньше, тем быстрее и предпочтительнее):
Обновите, чтобы разместить @Peter Cordes
https://gist.github.com/Lewiscowles1986/90191c59c9aedf3d08bf0b129065cccc
i7 4700MQ Linux Ubuntu Xenial 64-бит (применены все исправления до 13.03.2018) AMD Opteron (tm) Processor 4122 (точный, общий хостинг DreamHost) Intel Xeon E5-2630L v2 @ 2,4 ГГц (Trusty 64-бит, DigitalOcean VPS)источник
benchmark-pc
измерение некоторой комбинации пропускной способности и задержек? На вашем Haswell (i7 4700MQ) целочисленное умножение равно 1 на пропускную способность такта, задержка в 3 цикла, но целочисленное добавление / добавление составляет 4 на пропускную способность такта, задержка в 1 цикл ( agner.org/optimize ). Так что, по-видимому, есть много накладных расходов на цикл, разбавляющих эти числа, чтобы add и mul выходили так близко (длинное добавление: 0,824088 против long mul: 1,017164). (gcc по умолчанию не разворачивает циклы, за исключением полной разворачивания очень малого количества итераций).int
, толькоshort
иlong
? В Linux x86-64short
это 16 бит (и, следовательно, в некоторых случаях имеет место частичное замедление работы регистров), тогда какlong
иlong long
оба являются 64-битными типами. (Возможно, он разработан для Windows, где x86-64 по-прежнему использует 32-разрядную версиюlong
? Или, может быть, она предназначена для 32-разрядного режима.) В Linux x32 ABI имеет 32-разрядную версиюlong
в 64-разрядном режиме , поэтому, если у вас установлены библиотеки , используйтеgcc -mx32
компилятор для ILP32. Или просто используйте-m32
и посмотрите наlong
цифры.addps
регистров xmm вместоaddss
, для параллельного добавления 4 FP в одну инструкцию, которая работает так же быстро, как скалярaddss
. (Используйте,-march=native
чтобы разрешить использование любых наборов инструкций, поддерживаемых вашим процессором, а не только базовых показателей SSE2 для x86-64).Два момента для рассмотрения -
Современное оборудование может перекрывать инструкции, выполнять их параллельно и переупорядочивать их для наилучшего использования оборудования. А также любая значимая программа с плавающей запятой, вероятно, также будет иметь значительную целочисленную работу, даже если она только вычисляет индексы в массивах, счетчике цикла и т. Д., Поэтому даже если у вас есть медленная инструкция с плавающей запятой, она вполне может работать на отдельном бите оборудования перекрывается с некоторой целочисленной работой. Я хочу сказать, что даже если инструкции с плавающей запятой медленнее, чем целочисленные, ваша программа в целом может работать быстрее, потому что она может использовать больше оборудования.
Как всегда, единственный способ быть уверенным - это профилировать свою фактическую программу.
Во-вторых, большинство современных процессоров имеют инструкции SIMD для операций с плавающей запятой, которые могут работать с несколькими значениями с плавающей запятой одновременно. Например, вы можете загрузить 4 числа с плавающей запятой в один регистр SSE и выполнить на них 4 умножения параллельно. Если вы можете переписать части своего кода для использования инструкций SSE, то, скорее всего, он будет быстрее, чем целочисленная версия. Visual c ++ предоставляет встроенные функции компилятора для этого, см. Http://msdn.microsoft.com/en-us/library/x5c07e2a(v=VS.80).aspx для получения дополнительной информации.
источник
Версия с плавающей запятой будет намного медленнее, если не будет операции с остатком. Поскольку все добавления являются последовательными, процессор не сможет распараллелить суммирование. Задержка будет критической. Задержка добавления FPU обычно составляет 3 цикла, а целочисленное сложение - 1 цикл. Однако делитель для оператора остатка, вероятно, будет критической частью, поскольку он не полностью конвейеризован на современных процессорах. поэтому, если предположить, что инструкция деления / остатка будет занимать большую часть времени, разница из-за добавления задержки будет небольшой.
источник
Если вы не пишете код, который будет вызываться миллионы раз в секунду (такой как, например, рисование линии на экране в графическом приложении), целочисленная арифметика или арифметика с плавающей запятой редко являются узким местом.
Обычно первый шаг к вопросам эффективности - профилирование кода, чтобы увидеть, на что действительно тратится время выполнения. Для этого используется команда linux
gprof
.Редактировать:
Хотя я полагаю, что вы всегда можете реализовать алгоритм рисования линий, используя целые числа и числа с плавающей запятой, вызовите его много раз и посмотрите, имеет ли это значение:
http://en.wikipedia.org/wiki/Bresenham's_algorithm
источник
Сегодня целочисленные операции обычно немного быстрее операций с плавающей запятой. Поэтому, если вы можете выполнить вычисления с одинаковыми операциями с целым числом и с плавающей запятой, используйте целое число. ОДНАКО вы говорите: «Это вызывает множество неприятных проблем и добавляет много раздражающего кода». Похоже, вам нужно больше операций, потому что вы используете целочисленную арифметику вместо с плавающей запятой. В этом случае с плавающей запятой будет работать быстрее, потому что
как только вам нужно больше целочисленных операций, вам, вероятно, понадобится гораздо больше, поэтому небольшое преимущество в скорости более чем съедено дополнительными операциями
код с плавающей запятой проще, а это значит, что его писать быстрее, а это значит, что если он критичен по скорости, вы можете потратить больше времени на оптимизацию кода.
источник
Я провел тест, который просто добавил к числу 1 вместо rand (). Результаты (на x86-64) были:
источник
Основываясь на этом очень надежном «кое-что, что я слышал», в былые времена целочисленные вычисления были примерно в 20-50 раз быстрее, чем с плавающей запятой, а в наши дни они менее чем в два раза быстрее.
источник