Вычисления с плавающей запятой и целочисленные на современном оборудовании

100

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

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

maxpenguin
источник
8
То, что у вас сейчас в качестве теста, тривиально. Также, вероятно, очень небольшая разница в сборке (например, addlзаменена faddна). Единственный способ получить действительно хорошие результаты - это получить основную часть вашей реальной программы и профилировать различные ее версии. К сожалению, это может быть довольно сложно без особых усилий. Возможно, сообщение нам о целевом оборудовании и вашем компиляторе поможет людям, по крайней мере, дать вам уже существующий опыт и т. Д. Что касается использования целых чисел, я подозреваю, что вы могли бы создать своего рода fixed_pointшаблонный класс, который значительно упростил бы такую ​​работу.
GManNickG
1
По-прежнему существует множество архитектур, в которых нет специального оборудования с плавающей запятой - некоторые теги, объясняющие системы, которые вам нужны, помогут вам получить более точные ответы.
Карл Норум
3
Я считаю, что в моем HTC Hero (Android) нет FPU, а в Google NexusOne (Android) есть. какова твоя цель? настольные / серверные ПК? нетбуки (можно arm + linux)? телефоны?
SteelBytes
5
Если вам нужен быстрый FP на x86, попробуйте скомпилировать с оптимизацией и генерацией кода SSE. SSE (в любой версии) может выполнять как минимум операции сложения, вычитания и умножения с плавающей запятой за один цикл. Функции Divide, Mod и более высокие всегда будут медленными. Также обратите внимание, что floatскорость увеличивается, но обычно doubleнет.
Майк Д.
1
Целое число с фиксированной точкой приближает FP с помощью нескольких целочисленных операций, чтобы результаты не переполнялись. Это почти всегда медленнее, чем использование исключительно мощных FPU в современных настольных процессорах. например, MAD, mp3-декодер с фиксированной запятой, работает медленнее, чем libmpg123, и даже несмотря на хорошее качество для декодера с фиксированной запятой, libmpg123 по-прежнему имеет меньшую ошибку округления. wezm.net/technical/2008/04/mp3-decoder-libraries-compared для тестов на PPC G5.
Питер Кордес

Ответы:

35

Увы, я могу дать вам только ответ "смотря по обстоятельствам" ...

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

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

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

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

Дэн
источник
8
А на некоторых процессорах математика FP может быть быстрее, чем целочисленная. В процессоре Alpha была инструкция деления FP, но не целочисленная, поэтому целочисленное деление приходилось выполнять программно.
Гейб
Ускоряет ли SSEx арифметику с двойной точностью? Извините, я не слишком знаком с SSE
Йоханнес Шауб - литб
1
@ JohannesSchaub-litb: SSE2 (базовая версия для x86-64) имеет упакованный double-precision FP. При наличии всего двух 64-битных doubles на регистр потенциальное ускорение меньше, чем 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.)
Питер Кордес
1
Но целочисленные инструкции MMX и SSE * / AVX2 действительно конкурируют за одни и те же исполнительные блоки, поэтому использование обоих сразу почти никогда не бывает полезным. Просто используйте более широкие версии XMM / YMM, чтобы выполнять больше работы. Использование целого числа SIMD и FP одновременно конкурирует за одни и те же регистры, но x86-64 имеет 16 из них. Но ограничения общей пропускной способности означают, что вы не можете выполнить вдвое больше работы, используя параллельно целочисленные и FP исполнительные блоки.
Питер Кордес
49

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

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 производительности , а также фактического количества АЛУ / FPUs в расчете на одного core в суперскалярных схемах, который влияет на количество независимых операций, которые могут выполняться параллельно - последний фактор не учитывается в приведенном ниже коде, поскольку все операции ниже являются последовательно зависимыми.)

Тест работы бедняков 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;
}
владр
источник
8
почему вы смешали mult и div? Разве не должно быть интересно, если mult, возможно (или ожидаемо?) Намного быстрее div?
Kyss Tao
13
Умножение происходит намного быстрее, чем деление, как в случае целых чисел, так и с плавающей запятой. Производительность деления зависит также от размера чисел. Я обычно предполагаю, что деление происходит примерно в 15 раз медленнее.
Согартар
4
pastebin.com/Kx8WGUfg Я взял ваш тест, выделил каждую операцию в отдельный цикл и добавил, volatileчтобы убедиться. На Win64, ФП не используется и MSVC не будет генерировать код для него, поэтому он компилируется с использованием mulssи divssинструкции XMM там, которые 25x быстрее , чем FPU в Win32. Тестовая машина - Core i5 M 520 @ 2,40 ГГц
Джеймс Данн
4
@JamesDunne, будьте осторожны, так как fp ops vочень быстро достигнет 0 или +/- inf, что может или не может (теоретически) рассматриваться как особый случай / fastpatheed некоторыми реализациями fpu.
vladr 03
3
В этом «тесте» нет параллелизма данных для выполнения вне очереди, потому что каждая операция выполняется с одним и тем же аккумулятором ( v). В последних разработках Intel разделение вообще не конвейерно ( divss/ divpsимеет задержку 10–14 циклов и такую ​​же обратную пропускную способность). mulssоднако это задержка в 5 циклов, но может выдавать один за каждый цикл. (Или два за цикл на Haswell, поскольку порт 0 и порт 1 имеют множитель для FMA).
Питер Кордес
23

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

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

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

Бен Фойгт
источник
4
+1 за указание на то, как наивные тесты могут давать нулевые циклы из-за развернутых постоянных целочисленных операций. Более того, компилятор может полностью отказаться от цикла (целочисленного или FP), если результат фактически не используется.
vladr
Вывод: нужно вызвать функцию, имеющую в качестве аргумента переменную цикла. Поскольку я думаю, что ни один компилятор не сможет увидеть, что функция ничего не делает и что вызов можно игнорировать. Поскольку есть накладные расходы на вызов, существенными будут только различия времени == (время с плавающей запятой - целое время).
GameAlchemist
@GameAlchemist: Многие компиляторы исключают вызовы пустых функций как побочный эффект встраивания. Вы должны приложить усилия, чтобы этого не произошло.
Бен Фойгт
OP звучал так, как будто он говорил об использовании целых чисел для вещей, для которых FP было бы более естественным, поэтому потребуется больше целочисленного кода для достижения того же результата, что и для кода FP. В этом случае просто используйте FP. Например, на оборудовании с FPU (например, ЦП настольного компьютера) целочисленные декодеры MP3 с фиксированной запятой работают медленнее (и немного больше ошибок округления), чем декодеры с плавающей запятой. Реализации кодеков с фиксированной точкой в ​​основном существуют для работы на урезанных процессорах ARM без аппаратного обеспечения FP, только с медленной эмуляцией FP.
Питер Кордес
один пример для первой точки: на x86-64 с AVX-512 есть только 16 регистров GP, но 32 регистра zmm, поэтому скалярная математика с плавающей запятой может быть быстрее
phuclv
18

Добавление происходит намного быстрее rand, поэтому ваша программа (особенно) бесполезна.

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

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

Potatoswatter
источник
Да, а также преобразование целого числа rand в число с плавающей запятой в версии с плавающей запятой. Есть идеи, как лучше это проверить?
maxpenguin
1
Если вы пытаетесь профилировать скорость, посмотрите POSIX timespec_tили что-то подобное. Запишите время в начале и в конце цикла и определите разницу. Затем выведите randгенерацию данных из цикла. Убедитесь, что ваш алгоритм получает все данные из массивов и помещает все свои данные в массивы. Это само по себе получает ваш фактический алгоритм и получает настройки, malloc, печать результатов, все, кроме переключения задач и прерываний, из вашего цикла профилирования.
Майк Д.
3
@maxpenguin: вопрос в том, что вы тестируете. Артем предполагал, что вы занимаетесь графикой, Карл размышлял, используете ли вы встроенную платформу без FP, я полагал, что вы пишете науку для сервера. Вы не можете обобщать или «писать» тесты. Тесты взяты из реальной работы, которую выполняет ваша программа. Одно я могу вам сказать, это то, что она не останется «практически той же скорости», если вы коснетесь критически важного для производительности элемента в своей программе, чем бы он ни был.
Potatoswatter
хороший момент и хороший ответ. В настоящее время у нас есть обширная кодовая база. На самом деле я столкнулся с обобщением, что мы «не должны использовать float, поскольку целочисленные вычисления выполняются быстрее» - и я ищу способ (если это даже правда) опровергнуть это обобщенное предположение. Я понимаю, что для нас было бы невозможно предсказать точный результат, если не сделаем всю работу и не профилируем ее потом. В любом случае спасибо за вашу помощь.
maxpenguin
18

TIL Это варьируется (много). Вот некоторые результаты с использованием компилятора gnu (кстати, я также проверил, компилируя на машинах, gnu g ++ 5.4 от xenial чертовски быстрее, чем 4.6.3 от linaro, если точнее)

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 (Chromebook Acer C720 под управлением 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-бит (применены все исправления до 13.03.2018)
    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
AMD Opteron (tm) Processor 4122 (точный, общий хостинг DreamHost)
    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
Intel Xeon E5-2630L v2 @ 2,4 ГГц (Trusty 64-бит, DigitalOcean VPS)
    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
MrMesees
источник
Может быть, gcc5 автоматически векторизует то, чего не делал gcc4.6? Является ли 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-64 shortэто 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).
Питер Кордес,
@cincodenada, пожалуйста, оставьте диаграммы, показывающие все 15 вверху, поскольку тогда это иллюстрирует производительность.
MrMesees
@PeterCordes Постараюсь посмотреть завтра, спасибо за усердие.
MrMesees
7

Два момента для рассмотрения -

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

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

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

jcoder
источник
Следует отметить, что в Win64 инструкции FPU больше не генерируются компилятором MSVC. Здесь с плавающей точкой всегда используются инструкции SIMD. Это приводит к большому расхождению в скорости между Win32 и Win64 в отношении провалов.
Джеймс Данн
5

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

Горан Д
источник
4

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

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

Редактировать:

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

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

Артем Соколов
источник
2
Научные приложения используют FP. Единственное преимущество FP состоит в том, что точность не зависит от масштаба. Это похоже на научное обозначение. Если вы уже знаете масштаб чисел (например, что длина строки - это количество пикселей), FP можно избежать. Но прежде чем вы начнете рисовать черту, это неправда.
Potatoswatter
4

Сегодня целочисленные операции обычно немного быстрее операций с плавающей запятой. Поэтому, если вы можете выполнить вычисления с одинаковыми операциями с целым числом и с плавающей запятой, используйте целое число. ОДНАКО вы говорите: «Это вызывает множество неприятных проблем и добавляет много раздражающего кода». Похоже, вам нужно больше операций, потому что вы используете целочисленную арифметику вместо с плавающей запятой. В этом случае с плавающей запятой будет работать быстрее, потому что

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

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

скряга729
источник
Здесь есть много диких предположений, не считая каких-либо вторичных эффектов, присутствующих в оборудовании, которые часто доминируют во времени вычислений. Неплохая отправная точка, но ее нужно проверять в каждом конкретном приложении с помощью профилирования, а не преподавать как евангелие.
Ben Voigt
3

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

  • короткое: 4.260 с
  • интервал: 4.020 с
  • long long: 3,350 с
  • float: 7.330 с
  • двойной: 7,210 с
dan04
источник
1
Источник, параметры компиляции и метод синхронизации? Я немного удивлен результатами.
GManNickG
Тот же цикл, что и OP, с заменой "rand ()% 365" на "1". Без оптимизации. Пользовательское время из команды "time".
dan04
13
«Никакой оптимизации» - вот ключ. Вы никогда не профилируете с отключенной оптимизацией, всегда профилируете в режиме «выпуска».
Дин Хардинг,
2
В этом случае, однако, отключение оптимизации приводит к тому, что операция должна выполняться, и делается это сознательно - цикл нужен для увеличения времени до разумного масштаба измерения. Использование константы 1 убирает стоимость функции rand (). Достаточно умный оптимизирующий компилятор увидел бы 1, добавленную 100000000 раз, без выхода из цикла, и просто добавил бы 100000000 за одну операцию. Такого рода цель обходит всю цель, не так ли?
Стэн Роджерс
7
@Stan, сделайте переменную изменчивой. Тогда даже умный оптимизирующий компилятор должен учитывать несколько операций.
vladr
0

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

Джеймс Карран
источник
1
Пожалуйста, подумайте о том, чтобы взглянуть на это снова, предлагая нечто большее, чем мнение (особенно с учетом того, что мнение, кажется,
противоречит
1
@MrMesees Хотя этот ответ не очень полезен, я бы сказал, что он согласуется с проведенными вами тестами. И исторические мелочи, наверное, тоже подойдут.
Jonatan Öström
Как человек, который когда-то работал с 286, я могу подтвердить; "Да они были!"
Дэвид Х. Парри,