Плавающая точка по сравнению с целочисленными вычислениями на современных аппаратных средствах

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

Теперь, я не забываю читать о том, как вычисления с плавающей точкой были настолько медленными приблизительно приблизительно эти 386 дней, где я полагаю (IIRC), что был дополнительный co-proccessor. Но конечно в наше время с экспоненциально более сложными и мощными центральными процессорами это не имеет никакого значения в "скорости" при выполнении или целочисленного вычисления с плавающей точкой? Тем более, что фактическое время вычисления является крошечным по сравнению с чем-то как порождение останова конвейерной обработки или выборка чего-то от оперативной памяти?

Я знаю, что корректный ответ должен сравнить на целевых аппаратных средствах, каков был бы хороший способ протестировать это? Я записал две крошечных программы 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 (вставляемый из комментария ниже): у Нас в настоящее время есть обширная кодовая база. Действительно я натолкнулся на обобщение, что мы "не должны использовать плавание, так как целочисленное вычисление быстрее" - и я ищу путь (если это даже верно) опровергнуть это обобщенное предположение. Я понимаю, что было бы невозможно предсказать точный результат для нас за исключением выполнения всей работы и профилирования его впоследствии.

Так или иначе, спасибо за все Ваши превосходные ответы и справку. Не стесняйтесь добавлять что-либо еще :).

96
задан mskfisher 9 May 2012 в 16:23
поделиться

8 ответов

Увы, я могу дать вам только ответ "это зависит"...

По моему опыту, есть много, много переменных для производительности... особенно между целочисленной математикой и математикой с плавающей точкой. Она сильно варьируется от процессора к процессору (даже в пределах одного семейства, например, x86), потому что разные процессоры имеют разную длину "конвейера". Кроме того, некоторые операции, как правило, очень просты (например, сложение) и имеют ускоренный путь через процессор, а другие (например, деление) занимают намного, намного больше времени.

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

Я предполагаю, что вы задаете этот вопрос, потому что работаете над приложением, критичным к производительности. Если вы разрабатываете приложение для архитектуры x86, и вам нужна дополнительная производительность, возможно, вы захотите рассмотреть возможность использования расширений SSE. Это может значительно ускорить арифметику с плавающей запятой одинарной точности, поскольку одна и та же операция может выполняться над несколькими данными одновременно, плюс для операций SSE существует отдельный* банк регистров. (Я заметил, что в вашем втором примере вы использовали "float" вместо "double", что заставляет меня думать, что вы используете математику с одинарной точностью).

*Примечание: Использование старых инструкций MMX фактически замедляет работу программ, потому что эти старые инструкции фактически используют те же регистры, что и FPU, что делает невозможным одновременное использование FPU и MMX.

32
ответ дан 24 November 2019 в 05:39
поделиться

Два момента, которые следует учитывать -

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

Как всегда, единственный способ быть уверенным - это профилировать свою фактическую программу.

Во-вторых, большинство современных процессоров имеют инструкции SIMD для операций с плавающей запятой, которые могут одновременно работать с несколькими значениями с плавающей запятой. Например, вы можете загрузить 4 числа с плавающей запятой в один регистр SSE и выполнить на них 4 умножения параллельно. Если вы можете переписать части своего кода для использования инструкций SSE, то, скорее всего, он будет быстрее, чем целочисленная версия. Visual c ++ предоставляет встроенные функции компилятора для этого, дополнительную информацию см. В http://msdn.microsoft.com/en-us/library/x5c07e2a (v = VS.80) .aspx .

7
ответ дан 24 November 2019 в 05:39
поделиться

Например, (меньшие числа быстрее),

64-разрядный процессор Intel Xeon X5550 @ 2,67 ГГц, 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 ГГц, 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]

Как Дэн указал, даже после нормализации тактовой частоты (что само по себе может вводить в заблуждение в конвейерных проектах), результаты будут сильно различаться в зависимости от архитектуры процессора (отдельные ALU/производительность FPU, , а также фактическое количество ALU/FPU, доступных на ядро в суперскалярных проектах, которые влияют на то, сколько независимых операций могут выполняться параллельно - последний фактор не используется приведенным ниже кодом, поскольку все операции ниже последовательно зависят.)

Контрольный показатель работы 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;
}
48
ответ дан 24 November 2019 в 05:39
поделиться

Я провел тест, который просто добавил к числу 1 вместо rand (). Результаты (на x86-64) были:

  • короткие: 4,260 с
  • int: 4,020 с
  • длинные длинные: 3,350 с
  • float: 7,330 с
  • двойные: 7,210 с
3
ответ дан 24 November 2019 в 05:39
поделиться

Основываясь на этом очень надежном «кое-что, что я слышал», в былые времена целочисленные вычисления были примерно в 20-50 раз быстрее, чем с плавающей запятой, а в наши дни они менее чем в два раза быстрее.

0
ответ дан 24 November 2019 в 05:39
поделиться

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

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

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

22
ответ дан 24 November 2019 в 05:39
поделиться

Сложение выполняется намного быстрее, чем rand , поэтому ваша программа (особенно) бесполезна.

Вам необходимо определить «горячие точки» производительности и постепенно модифицировать вашу программу. Похоже, у вас есть проблемы со средой разработки, которые необходимо решить в первую очередь. Разве невозможно запустить вашу программу на вашем ПК для небольшого набора проблем?

Как правило, попытки выполнения заданий FP с целочисленной арифметикой - рецепт медленного.

18
ответ дан 24 November 2019 в 05:39
поделиться

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

Обычно первым шагом к решению вопросов эффективности является профилирование кода, чтобы увидеть, на что действительно тратится время выполнения. Команда linux для этого - gprof.

Edit:

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

http://en.wikipedia.org/wiki/Bresenham's_algorithm

4
ответ дан 24 November 2019 в 05:39
поделиться
Другие вопросы по тегам:

Похожие вопросы: