Вычисления с плавающей запятой и целыми числами на современном оборудовании
Я делаю некоторые критические работы производительности в 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;
}
спасибо заранее!
Edit: платформа, о которой я забочусь, является обычной x86 или x86-64 работает на настольных компьютерах Linux и Windows.
Edit 2 (вставлено из комментария ниже): в настоящее время у нас есть обширная база кода. На самом деле я столкнулся с обобщением, что мы "не должны использовать float, так как целочисленный расчет быстрее", - и я ищу способ (если это даже верно) опровергнуть это обобщенное предположение. Я понимаю, что невозможно было бы предсказать точный результат для нас, не выполнив всю работу и не профилировав ее впоследствии.
в любом случае, спасибо за все ваши прекрасные ответы и помощь. Не стесняйтесь добавлять что-нибудь еще :).
11 ответов:
увы, я могу дать вам только ответ "это зависит"...
по моему опыту, есть много, много переменных для производительности...особенно между целым числом и математикой с плавающей запятой. Он сильно варьируется от процессора к процессору (даже в пределах одного семейства, например x86), потому что разные процессоры имеют разную длину "конвейера". Кроме того, некоторые операции, как правило, очень просты (например, сложение) и имеют ускоренный маршрут через процессор, а другие (например, разделение) займет гораздо, гораздо больше времени.
другая большая переменная находится там, где находятся данные. Если у вас есть только несколько значений для добавления, то все данные могут находиться в кэше, где они могут быть быстро отправлены в ЦП. Очень, очень медленная операция с плавающей запятой, которая уже имеет данные в кэше, будет во много раз быстрее, чем целочисленная операция, где целое число должно быть скопировано из системной памяти.
Я предполагаю, что вы задаете этот вопрос, потому что вы работаете на производительность критически важных приложений. Если вы разрабатываете архитектуру x86 и вам нужна дополнительная производительность, вы можете изучить использование расширений SSE. Это может значительно ускорить арифметику с плавающей запятой с одной точностью, так как одна и та же операция может выполняться сразу с несколькими данными, а также существует отдельный банк регистров для операций SSE. (Я заметил, что во втором примере вы использовали "float" вместо "double", заставляя меня думать, что вы используете одинарную точность математика.)
*Примечание: использование старых инструкций MMX фактически замедлило бы программы, потому что эти старые инструкции фактически использовали те же регистры, что и FPU, что делает невозможным одновременное использование как FPU, так и MMX.
например (меньшие числа быстрее),
64-разрядный Intel Xeon X5550 @ 2.67 GHz, gcc 4.1.2
-O3
short add/sub: 1.005460 [0] short mul/div: 3.926543 [0] long add/sub: 0.000000 [0] long mul/div: 7.378581 [0] long long add/sub: 0.000000 [0] long long mul/div: 7.378593 [0] float add/sub: 0.993583 [0] float mul/div: 1.821565 [0] double add/sub: 0.993884 [0] double mul/div: 1.988664 [0]
32-разрядный двухъядерный процессор AMD Opteron(tm) 265 @ 1.81 GHz, gcc 3.4.6
-O3
short add/sub: 0.553863 [0] short mul/div: 12.509163 [0] long add/sub: 0.556912 [0] long mul/div: 12.748019 [0] long long add/sub: 5.298999 [0] long long mul/div: 20.461186 [0] float add/sub: 2.688253 [0] float mul/div: 4.683886 [0] double add/sub: 2.700834 [0] double mul/div: 4.646755 [0]
как Дэн указал, даже когда вы нормализуете тактовую частоту (что может вводить в заблуждение само по себе в конвейерных конструкциях),результаты будут сильно отличаться в зависимости от архитектуры процессора (физ. АЛУ/FPU производительность,а также фактический количество ALUs / FPUs доступно на ядро в суперскалярной конструкции, которые влияют на сколько независимые операции могут выполняться параллельно -- последний фактор не выполняется кодом ниже, поскольку все операции ниже последовательно зависят.)
тест работы FPU/ALU бедного человека:
#include <stdio.h> #ifdef _WIN32 #include <sys/timeb.h> #else #include <sys/time.h> #endif #include <time.h> #include <cstdlib> double mygettime(void) { # ifdef _WIN32 struct _timeb tb; _ftime(&tb); return (double)tb.time + (0.001 * (double)tb.millitm); # else struct timeval tv; if(gettimeofday(&tv, 0) < 0) { perror("oops"); } return (double)tv.tv_sec + (0.000001 * (double)tv.tv_usec); # endif } template< typename Type > void my_test(const char* name) { Type v = 0; // Do not use constants or repeating values // to avoid loop unroll optimizations. // All values >0 to avoid division by 0 // Perform ten ops/iteration to reduce // impact of ++i below on measurements Type v0 = (Type)(rand() % 256)/16 + 1; Type v1 = (Type)(rand() % 256)/16 + 1; Type v2 = (Type)(rand() % 256)/16 + 1; Type v3 = (Type)(rand() % 256)/16 + 1; Type v4 = (Type)(rand() % 256)/16 + 1; Type v5 = (Type)(rand() % 256)/16 + 1; Type v6 = (Type)(rand() % 256)/16 + 1; Type v7 = (Type)(rand() % 256)/16 + 1; Type v8 = (Type)(rand() % 256)/16 + 1; Type v9 = (Type)(rand() % 256)/16 + 1; double t1 = mygettime(); for (size_t i = 0; i < 100000000; ++i) { v += v0; v -= v1; v += v2; v -= v3; v += v4; v -= v5; v += v6; v -= v7; v += v8; v -= v9; } // Pretend we make use of v so compiler doesn't optimize out // the loop completely printf("%s add/sub: %f [%d]\n", name, mygettime() - t1, (int)v&1); t1 = mygettime(); for (size_t i = 0; i < 100000000; ++i) { v /= v0; v *= v1; v /= v2; v *= v3; v /= v4; v *= v5; v /= v6; v *= v7; v /= v8; v *= v9; } // Pretend we make use of v so compiler doesn't optimize out // the loop completely printf("%s mul/div: %f [%d]\n", name, mygettime() - t1, (int)v&1); } int main() { my_test< short >("short"); my_test< long >("long"); my_test< long long >("long long"); my_test< float >("float"); my_test< double >("double"); return 0; }
вероятно, существует значительная разница в реальной скорости между математикой с фиксированной и плавающей точками, но теоретическая пропускная способность наилучшего случая ALU vs FPU совершенно не имеет значения. Вместо этого, количество целочисленных регистров и регистров с плавающей запятой (вещественные регистры, а не имена регистров) в вашей архитектуре, которые иначе не используются вашим вычислением (например, для управления циклом), количество элементов каждого типа, которые вписываются в строку кэша, возможные оптимизации с учетом различная семантика для целочисленной математики и математики с плавающей запятой-эти эффекты будут доминировать. Зависимости данных вашего алгоритма играют здесь значительную роль, так что никакое общее сравнение не будет предсказывать разрыв в производительности по вашей проблеме.
например, целочисленное сложение является коммутативным, поэтому, если компилятор видит цикл, как вы использовали для бенчмарка (предполагая, что случайные данные были подготовлены заранее, чтобы не скрывать результаты), он может развернуть цикл и вычислить частичное суммы без зависимостей, а затем добавить их, когда цикл завершается. Но с плавающей точкой компилятор должен выполнять операции в том же порядке, что и вы просили (у вас есть точки последовательности, поэтому компилятор должен гарантировать тот же результат, который запрещает переупорядочивание), поэтому существует сильная зависимость каждого добавления от результата предыдущего.
вы, вероятно, поместите больше целочисленных операндов в кэш одновременно. Таким образом, версия с фиксированной точкой может превзойти версия поплавка порядком величины даже на машине где FPU имеет теоретически более высокий объем.
добавление происходит гораздо быстрее, чем
rand
, поэтому ваша программа (особенно) бесполезна.вы должны определить горячие точки производительности и постепенно изменять свою программу. Похоже, у вас есть проблемы с вашей средой разработки, которые необходимо будет решить в первую очередь. Это невозможно запустить программу на вашем компьютере для небольшого набора проблем?
Как правило, попытка FP заданий с целочисленной арифметикой является рецептом для медленного.
пока это не изменится (много). Вот некоторые результаты использования компилятора gnu (кстати, я также проверил компиляцию на машинах, gnu g++ 5.4 от xenial намного быстрее, чем 4.6.3 от linaro на precise)
Intel i7 4700MQ xenial
short add: 0.822491 short sub: 0.832757 short mul: 1.007533 short div: 3.459642 long add: 0.824088 long sub: 0.867495 long mul: 1.017164 long div: 5.662498 long long add: 0.873705 long long sub: 0.873177 long long mul: 1.019648 long long div: 5.657374 float add: 1.137084 float sub: 1.140690 float mul: 1.410767 float div: 2.093982 double add: 1.139156 double sub: 1.146221 double mul: 1.405541 double div: 2.093173
Intel i3 2370M имеет аналогичные результаты
short add: 1.369983 short sub: 1.235122 short mul: 1.345993 short div: 4.198790 long add: 1.224552 long sub: 1.223314 long mul: 1.346309 long div: 7.275912 long long add: 1.235526 long long sub: 1.223865 long long mul: 1.346409 long long div: 7.271491 float add: 1.507352 float sub: 1.506573 float mul: 2.006751 float div: 2.762262 double add: 1.507561 double sub: 1.506817 double mul: 1.843164 double div: 2.877484
Intel(R) Celeron (R) 2955U (Acer C720 Chromebook под управлением xenial)
short add: 1.999639 short sub: 1.919501 short mul: 2.292759 short div: 7.801453 long add: 1.987842 long sub: 1.933746 long mul: 2.292715 long div: 12.797286 long long add: 1.920429 long long sub: 1.987339 long long mul: 2.292952 long long div: 12.795385 float add: 2.580141 float sub: 2.579344 float mul: 3.152459 float div: 4.716983 double add: 2.579279 double sub: 2.579290 double mul: 3.152649 double div: 4.691226
DigitalOcean 1GB Droplet Intel (R) Xeon (R) CPU E5-2630L v2 (работает надежный)
short add: 1.094323 short sub: 1.095886 short mul: 1.356369 short div: 4.256722 long add: 1.111328 long sub: 1.079420 long mul: 1.356105 long div: 7.422517 long long add: 1.057854 long long sub: 1.099414 long long mul: 1.368913 long long div: 7.424180 float add: 1.516550 float sub: 1.544005 float mul: 1.879592 float div: 2.798318 double add: 1.534624 double sub: 1.533405 double mul: 1.866442 double div: 2.777649
AMD Opteron(tm) процессор 4122 (точный)
short add: 3.396932 short sub: 3.530665 short mul: 3.524118 short div: 15.226630 long add: 3.522978 long sub: 3.439746 long mul: 5.051004 long div: 15.125845 long long add: 4.008773 long long sub: 4.138124 long long mul: 5.090263 long long div: 14.769520 float add: 6.357209 float sub: 6.393084 float mul: 6.303037 float div: 17.541792 double add: 6.415921 double sub: 6.342832 double mul: 6.321899 double div: 15.362536
это использует код из http://pastebin.com/Kx8WGUfg как
benchmark-pc.c
g++ -fpermissive -O3 -o benchmark-pc benchmark-pc.c
я запустил несколько проходов, но это, кажется, тот случай, когда общие числа одинаковы.
одним заметным исключением, по-видимому, является ALU mul vs FPU mul. Сложение и вычитание кажутся тривиально разными.
вот выше в форме диаграммы (нажмите для полного размера, ниже, быстрее и предпочтительнее):
обновление для размещения @Peter Cordes
https://gist.github.com/Lewiscowles1986/90191c59c9aedf3d08bf0b129065cccc
i7 4700MQ Linux Ubuntu Xenial 64-бит (все патчи к 2018-03-13 применены)Процессор AMD Opteron(tm) 4122 (precise, Dreamhost shared-hosting)short add: 0.773049 short sub: 0.789793 short mul: 0.960152 short div: 3.273668 int add: 0.837695 int sub: 0.804066 int mul: 0.960840 int div: 3.281113 long add: 0.829946 long sub: 0.829168 long mul: 0.960717 long div: 5.363420 long long add: 0.828654 long long sub: 0.805897 long long mul: 0.964164 long long div: 5.359342 float add: 1.081649 float sub: 1.080351 float mul: 1.323401 float div: 1.984582 double add: 1.081079 double sub: 1.082572 double mul: 1.323857 double div: 1.968488
Intel Xeon E5-2630L v2 @ 2.4 GHz (надежный 64-разрядный, DigitalOcean система VPS)short add: 1.235603 short sub: 1.235017 short mul: 1.280661 short div: 5.535520 int add: 1.233110 int sub: 1.232561 int mul: 1.280593 int div: 5.350998 long add: 1.281022 long sub: 1.251045 long mul: 1.834241 long div: 5.350325 long long add: 1.279738 long long sub: 1.249189 long long mul: 1.841852 long long div: 5.351960 float add: 2.307852 float sub: 2.305122 float mul: 2.298346 float div: 4.833562 double add: 2.305454 double sub: 2.307195 double mul: 2.302797 double div: 5.485736
short add: 1.040745 short sub: 0.998255 short mul: 1.240751 short div: 3.900671 int add: 1.054430 int sub: 1.000328 int mul: 1.250496 int div: 3.904415 long add: 0.995786 long sub: 1.021743 long mul: 1.335557 long div: 7.693886 long long add: 1.139643 long long sub: 1.103039 long long mul: 1.409939 long long div: 7.652080 float add: 1.572640 float sub: 1.532714 float mul: 1.864489 float div: 2.825330 double add: 1.535827 double sub: 1.535055 double mul: 1.881584 double div: 2.777245
два момента для рассмотрения -
современное оборудование может перекрывать инструкции, выполнять их параллельно и переупорядочивать их, чтобы наилучшим образом использовать оборудование. А также, любая значительная программа с плавающей запятой, вероятно, будет иметь значительную целочисленную работу, даже если она только вычисляет индексы в массивы, счетчик циклов и т. д. поэтому, даже если у вас есть медленная инструкция с плавающей запятой, она вполне может работать на отдельном бите оборудования, перекрывающемся с некоторой целочисленной работой. Я хочу сказать, что что даже если инструкции с плавающей запятой медленны, что целочисленные, ваша общая программа может работать быстрее, потому что она может использовать больше оборудования.
Как всегда, единственный способ быть уверенным, это в профиле свою реальную программу.
второй момент заключается в том, что большинство процессоров в эти дни имеют инструкции SIMD для плавающей точки, которые могут работать с несколькими значениями с плавающей точкой одновременно. Например, вы можете загрузить 4 поплавка в один регистр SSE и выполнить 4 умножения на них все параллельно. Если вы можете переписать части своего кода для использования инструкций SSE, то, скорее всего, это будет быстрее, чем целочисленная версия. Visual c++ предоставляет для этого встроенные функции компилятора, см. http://msdn.microsoft.com/en-us/library/x5c07e2a (v=VS.80).aspx для некоторой информации.
Я провел тест, который просто добавил 1 к числу вместо rand (). Результаты (на x86-64) были:
- короткий: 4.260 s
- int: 4.020 s
- long long: 3.350 s
- поплавок: 7.330 s
- двойной: 7.210 s
Если вы не пишете код, который будет вызываться миллионы раз в секунду (например, рисование линии на экране в графическом приложении), целочисленная арифметика против арифметики с плавающей запятой редко является узким местом.
обычно первым шагом к вопросам эффективности является профилирование кода, чтобы увидеть, где действительно тратится время выполнения. Команда linux для этого -
gprof
.Edit:
хотя я полагаю, что вы всегда можете реализовать алгоритм рисования линий с использованием целых чисел и чисел с плавающей запятой, назовите его большое количество раз и посмотрите, имеет ли это значение:
версия с плавающей запятой будет намного медленнее, если нет операции остатка. Поскольку все добавления являются последовательными, процессор не сможет распараллелить суммирование. Задержка будет критической. FPU add latency обычно составляет 3 цикла, а integer add-1 цикл. Однако делитель для оператора остатка, вероятно, будет критической частью, поскольку он не полностью конвейеризован на современных процессорах. поэтому, предполагая, что команда divide / remain будет потреблять большую часть времени, разница из-за добавления задержки будет небольшой.
сегодня целочисленные операции обычно немного быстрее, чем операции с плавающей запятой. Поэтому, если вы можете выполнить вычисление с теми же операциями в integer и с плавающей запятой, используйте integer. Однако вы говорите:"это вызывает много раздражающих проблем и добавляет много раздражающего кода". Похоже, вам нужно больше операций, потому что вы используете целочисленную арифметику вместо плавающей точки. В этом случае плавающая точка будет работать быстрее, потому что
Как как только вам понадобится больше целочисленных операций, вам, вероятно, понадобится намного больше, поэтому небольшое преимущество скорости более чем съедено дополнительными операциями
код с плавающей запятой проще, что означает, что быстрее писать код, а это означает, что если скорость критична, вы можете потратить больше времени на оптимизацию кода.