Розрахунки з плаваючою точкою та цілими числами на сучасному обладнання


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

У будь-якому випадку, дякую за всі ваші чудові відповіді та допомогу. Сміливо додайте що-небудь ще :).


8
Те, що ви маєте як тест зараз, є банальним. Мабуть, також дуже мала різниця у складанні ( addlзамінюється fadd, наприклад). Єдиний спосіб по-справжньому отримати хороший показник - отримати основну частину вашої реальної програми та профілі різних її версій. На жаль, це може бути досить важко, не використовуючи тонни зусиль. Можливо, розповісти нам про цільове обладнання та ваш компілятор допоможе людям принаймні надати вам попередній досвід тощо. Щодо вашого цілого використання, я підозрюю, що ви могли б створити такий fixed_pointклас шаблонів, який би надзвичайно полегшив таку роботу.
GManNickG

1
Є ще багато архітектур, які не мають спеціального обладнання з плаваючою точкою - деякі теги, що пояснюють системи, які вам цікаві, допоможуть отримати кращі відповіді.
Карл Норум

3
Я вважаю, що в моєму пристрої HTC Hero (android) немає FPU, але апаратне забезпечення в Google NexusOne (android) є. яка ваша мета? настільний / серверний ПК? нетбуки (можливо arm + linux)? телефони?
SteelBytes

5
Якщо ви хочете швидкого FP на x86, спробуйте компілювати з оптимізацією та генерацією коду SSE. SSE (будь-яка версія) може принаймні плавати додавання, віднімання та множення за один цикл. Функції ділення, моди та вищі функції завжди будуть повільними. Також зауважте, що floatзбільшується швидкість, але зазвичай doubleце не так.
Майк Д.

1
Ціле число з фіксованою точкою наближає FP, використовуючи кілька цілих операцій, щоб уникнути переповнення результатів. Це майже завжди повільніше, ніж просто використання надзвичайно спроможних FPU, знайдених у сучасних настільних процесорах. наприклад, MAD, дешифратор mp3 з фіксованою точкою, повільніше, ніж libmpg123, і хоча це хороша якість для декодера з фіксованою точкою, libmpg123 все ще має меншу помилку округлення. wezm.net/technical/2008/04/mp3-decoder-libraries-порівнюється для орієнтирів на КПП 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точну 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 integer та 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 ГГц, гкк 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 в розрахунку на одного Ядро в суперскалярних конструкціях, яке впливає на те, скільки незалежних операцій може виконуватися паралельно - останній фактор не виконується кодом нижче, оскільки всі операції нижче послідовно залежать.)

Тест на роботу 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
чому ти змішав мульти та діви? Чи не повинно бути цікаво, якщо мульти, можливо, (або, як очікується?) Набагато швидше, ніж div?
Kyss Tao

13
Множення набагато швидше, ніж ділення як на цілі, так і випадки з плаваючою точкою. Продуктивність підрозділу також залежить від розміру чисел. Зазвичай я припускаю, що поділ ~ в 15 разів повільніше.
Sogartar

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

3
Цей "орієнтир" не має паралелізму даних для виконання поза замовлення, оскільки кожна операція робиться з одним і тим же акумулятором ( v). В останніх розробках Intel поділ взагалі не конвеєрний ( divss/ divpsмає 10-14 циклів затримки та однакову зворотну пропускну здатність). mulssпроте затримка 5 циклів, але можна видавати кожен цикл. (Або два за цикл на Haswell, оскільки порт 0 та порт 1 мають множник для FMA).
Пітер Кордес

23

Ймовірно, існує значна різниця у швидкості реального світу між математикою з фіксованою точкою та плаваючою комою, але теоретична пропускна здатність АЛУ проти ФПУ абсолютно не має значення. Натомість кількість реєстрів з цілочисельними числами і плаваючою комою (справжні регістри, а не імена регістрів) у вашій архітектурі, які не використовуються іншим чином для ваших обчислень (наприклад, для керування циклом), кількість елементів кожного типу, що входять у рядок кешу , можливі оптимізації з урахуванням різної семантики для математики цілого чи плаваючої точки - ці ефекти будуть домінувати. Тут залежать важливі ролі ваших алгоритмів даних, так що жодне загальне порівняння не може передбачити розрив у роботі щодо вашої проблеми.

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

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


4
+1 для вказівки, як наївні орієнтири можуть отримувати 0-разові цикли через непрокручені постійні цілі операції. Більше того, компілятор може повністю скинути цикл (ціле число або FP), якщо результат фактично не використовується.
vladr

Висновок до цього: потрібно викликати функцію, що має в якості аргументу циклічну змінну. Оскільки я думаю, жоден компілятор не міг бачити, що ця функція не робить нічого і що виклик можна ігнорувати. Оскільки накладні виклики, тільки різниці часу == (плаваючий час - цілий час) будуть істотними.
GameAlchemist

@GameAlchemist: Багато компіляторів усувають виклики до порожніх функцій, як побічний ефект вбудовування. Ви повинні докласти зусиль, щоб цього не допустити.
Ben Voigt

ОП звучало так, ніби він говорив про використання цілого числа для речей, де FP буде більш природним, тому знадобиться більше цілого коду, щоб досягти такого ж результату, як і FP-код. У цьому випадку просто використовуйте FP. Наприклад, на апараті з FPU (наприклад, настільний процесор) дешифратори MP3 з фіксованою точкою є повільнішими (і дещо більше помилок округлення), ніж декодери з плаваючою комою. Реалізації кодеків з фіксованою точкою в основному існують для запуску на збитих процесорах ARM без обладнання FP, лише повільно імітується FP.
Пітер Кордес

один приклад для першого пункту: на x86-64 з AVX-512 є лише 16 регістрів GP, але регістри 32 мм, так що скалярна математика з плаваючою комою може бути швидшою
phuclv

18

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

Потрібно визначити точки доступу та поступово змінити програму. Здається, у вас є проблеми із середовищем розвитку, які потрібно вирішити спочатку. Неможливо запустити програму на ПК для невеликого набору проблем?

Як правило, спроба FP завдань з цілою арифметикою є рецептом для повільних.


Так, як і перетворення цілого числа rand в плаваючу у версії з плаваючою комою. Будь-які ідеї щодо кращого способу перевірити це?
maxpenguin

1
Якщо ви намагаєтеся швидкості профілю, подивіться на POSIX timespec_tабо щось подібне. Запишіть час на початку та в кінці циклу та прийміть різницю. Потім перемістіть randгенерацію даних з циклу. Переконайтеся, що ваш алгоритм отримує всі свої дані з масивів і розміщує всі його дані в масивах. Це отримує ваш власний алгоритм сам і отримує налаштування, malloc, друк результатів, все, крім переключення завдань і перериває з вашого циклу профілювання.
Майк Д.

3
@maxpenguin: питання в тому, що ви тестуєте. Артем припускав, що ти займаєшся графікою, Карл розглядав, чи ти на вбудованій платформі, не FP, я вважаю, що ти кодуєш науку для сервера. Ви не можете узагальнити або "написати" орієнтири. Тести відбираються з фактичної роботи, яку виконує ваша програма. Одне, що я можу вам сказати, це те, що він не залишиться "по суті такою ж швидкістю", якщо торкнутися критично важливих для продуктивності елементів у вашій програмі, що б це не було.
Potatoswatter

хороший пункт і хороша відповідь. Наразі у нас є широка база коду. Дійсно, я виступив проти узагальнення, що ми "не повинні використовувати float, оскільки обчислення цілих чисел швидше" - і я шукаю спосіб (якщо це навіть вірно) спростувати це узагальнене припущення. Я усвідомлюю, що неможливо було б передбачити точний результат для нас, якщо ми не виконали всю роботу та профілювали її потім. У будь-якому випадку, дякую за допомогу.
maxpenguin

18

TIL Це варіюється (дуже багато). Ось декілька результатів за допомогою компілятора gnu (btw, я також перевіряв, компілюючи на машинах, 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 1 Гб дроплетів Intel (R) Xeon (R) процесор 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

Процесор 4122 AMD Opteron (tm) (точний)

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 проти FPU mul. Додавання і віднімання здаються тривіально різними.

Ось наведене вище у формі діаграми (натисніть на повний розмір, нижчий - швидше та бажано):

Діаграма вищезазначених даних

Оновлення для розміщення @Peter Cordes

https://gist.github.com/Lewiscowles1986/90191c59c9aedf3d08bf0b129065cccc

i7 4700MQ Linux Ubuntu Xenial 64-розрядний (застосовано всі патчі до 2018-03-13)
    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) процесор 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 ГГц (надійний 64-розрядний VPS DigitalOcean)
    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

gcc5, можливо, автоматично векторизує щось, що не було gcc4.6? Чи benchmark-pcвимірюється деяка комбінація пропускної здатності та затримки? У вашому Haswell (i7 4700MQ) ціле множення - 1 на тактову пропускну здатність, 3 циклу затримки, але ціле додавання / підпорядкування - 4 на тактову пропускну здатність, 1 цикл затримки ( agner.org/optimize ). Тому, мабуть, існує велика кількість накладних циклів, що розріджує ці числа для додавання та муль, щоб вийти так близько (довге додавання: 0,824088 проти довгої муль: 1,017164). (за замовчуванням gcc - це не розгортання циклів, за винятком повністю розгортання дуже низьких ітерацій).
Пітер Кордес

І BTW, чому він не тестує 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 для отримання деякої інформації.


Слід зазначити, що у Win64 інструкції FPU більше не генеруються компілятором MSVC. Плаваюча точка завжди використовує інструкції SIMD. Це спричиняє великі розбіжності у швидкості між Win32 та Win64 щодо флопів.
Джеймс Данн

5

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


4

Якщо ви не пишете код, який буде називатися мільйонами разів на секунду (наприклад, малювання лінії на екрані в графічному додатку), арифметика з цілим чи плаваючою комою рідко є вузьким місцем.

Звичайний перший крок до питань ефективності - це профіль вашого коду, щоб побачити, де реально витрачено час роботи. Команда linux для цього є gprof.

Редагувати:

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

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


2
Наукові програми використовують FP. Єдина перевага FP полягає в тому, що точність є інваріантною за шкалою. Це як наукове позначення. Якщо ви вже знаєте масштаб чисел (наприклад, що довжина рядка - це кількість пікселів), FP скасовується. Але перш ніж приступити до малювання лінії, це неправда.
Potatoswatter

4

Сьогодні цілі операції зазвичай трохи швидші, ніж операції з плаваючою точкою. Отже, якщо ви можете зробити обчислення з тими ж операціями в цілому чи плаваючій точці, використовуйте ціле число. ЯКЩО ви говорите: "Це спричиняє чимало дратівливих проблем і додає багато дратівливого коду". Це здається, що вам потрібно більше операцій, оскільки ви використовуєте цілу арифметику замість плаваючої точки. У цьому випадку плаваюча точка буде працювати швидше, оскільки

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

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


Тут є багато диких спекуляцій, не враховуючи жодного з вторинних ефектів, наявних у апаратному забезпеченні, які часто домінують у часі обчислень. Непогана відправна точка, але її потрібно перевіряти в кожній конкретній програмі за допомогою профілювання, а не викладати як євангелію.
Ben Voigt

3

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

  • короткий: 4.260s
  • int: 4.020s
  • довгий довгий: 3.350s
  • поплавок: 7.330с
  • подвійний: 7.210с

1
Джерело, параметри компіляції та спосіб синхронізації? Я трохи здивований результатами.
GManNickG

Той самий цикл, що і OP з "rand ()% 365" замінено на "1". Немає оптимізації. Користувацький час від команди "time".
dan04

13
"Без оптимізації" є ключовим. Ви ніколи не вимикаєте профіль з оптимізацією, завжди профіліруйте в режимі "реліз".
Дін Хардінг

2
Однак у цьому випадку відключення оптимізації примушує виникнути оп, і це робиться навмисно - цикл є для зменшення часу на розумну шкалу вимірювання. Використання константи 1 знімає вартість rand (). Досить розумний оптимізуючий компілятор побачить, що 1 додав 100 000 000 разів, не виходячи з циклу, і просто додати 100000000 в одній опції. Такий спосіб обходить всю ціль, чи не так?
Стен Роджерс

7
@Stan, зробіть змінну мінливою. Навіть компілятор, що оптимізує оптимізацію, повинен вшановувати кілька операцій.
vladr

0

Виходячи з того, настільки надійного «чогось, що я чув», ще в старі часи цілі обчислення були приблизно від 20 до 50 разів швидше, ніж плаваюча точка, а в наші дні це менше удвічі швидше.


1
Будь ласка, погляньте на це ще раз, ніж на думку (особливо з огляду на те, що думка, здається, летить перед зібраними фактами)
MrMesees

1
@MrMesees Хоча ця відповідь не дуже корисна, я б сказав, що вона відповідає тестам, які ви зробили. І історичні дрібниці, мабуть, теж чудові.
Jonatan Öström

Як хтось, хто працював з 286 тому назад, я можу підтвердити; "Так, вони були!"
Девід Н Паррі
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.