Чи дійсно швидше множення та ділення за допомогою операторів зсуву в C?


288

Множення та ділення можна досягти, наприклад, за допомогою бітових операторів

i*2 = i<<1
i*3 = (i<<1) + i;
i*10 = (i<<3) + (i<<1)

і так далі.

Насправді швидше використовувати кажуть, (i<<3)+(i<<1)щоб помножити на 10, ніж використовувати i*10безпосередньо? Чи є якісь дані, які не можна примножувати чи ділити таким чином?


8
Насправді можливий дешевий поділ на постійну силу, яка не є силою двох, але хитрий суб'єкт, до якого ви не здійснюєте справедливості з "/ Відділом ... / розділеним" у вашому питанні. Дивіться, наприклад, hackersdelight.org/divcMore.pdf (або отримайте книгу "Хакерське захоплення", якщо можете).
Паскаль Куок

46
Це звучить як щось, що можна було легко перевірити.
juanchopanza

25
Як завжди - це залежить. Колись я спробував це в асемблері на Intel 8088 (IBM PC / XT), де множення зайняло мільйони годин. Зміна та додавання виконується набагато швидше, тому це здалося гарною ідеєю. Однак під час множення одиниці шини було вільно заповнити чергу інструкцій, і наступна інструкція могла початись негайно. Після серії змін і додавання черга інструкцій буде порожньою, і процесору доведеться чекати, коли наступна інструкція буде отримана з пам'яті (один байт за один раз!). Міряй, міряй, міряй!
Бо Персон

19
Також майте на увазі, що зміщення праворуч визначено лише для непідписаних цілих чисел. Якщо у вас є підписане ціле число, не визначено, чи 0 або найвищий біт підкладені зліва. (І не забувайте час, який потребує того, щоб хтось інший (навіть ви самі) прочитали код через рік!)
Керрек СБ

29
Насправді, хороший компілятор, що оптимізує, реалізує множення та ділення зі зрушеннями, коли вони швидші.
Петро Г.

Відповіді:


487

Коротка відповідь: Неможливо.

Довга відповідь: у вашому компіляторі є оптимізатор, який вміє розмножуватися так швидко, наскільки здатна ваша цільова архітектура процесора. Ваша найкраща ставка - чітко сказати компілятору про свої наміри (тобто я * 2, а не я << 1), і дозволити йому вирішити, яка найшвидша послідовність складання / машинного коду. Можливо навіть, що сам процесор реалізував інструкцію множення як послідовність зрушень та доповнень у мікрокоді.

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


31
Так, як було сказано, можливі надбавки майже для кожної програми будуть значно перевищувати впроваджені неясності. Не турбуйтеся про подібну оптимізацію передчасно. Побудуйте те, що є чітко зрозумілим, визначте вузькі місця та оптимізуйте звідти…
Дейв,

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

5
Ці коментарі звучать так, ніби ви здаєтеся потенційній продуктивності, сказавши компілятору, як робити свою роботу. Це не так. Насправді ви отримуєте кращий код з gcc -O3на x86 з, return i*10ніж з версії shift . Оскільки хтось, хто багато дивиться на висновок компілятора (див. Багато моїх відповідей на оптимізацію / оптимізацію), я не здивований. Бувають випадки, коли це може допомогти перенести компілятор в один із способів виконання дій , але це не один із них. gcc хороший з цілої математики, тому що це важливо.
Пітер Кордес

Щойно завантажив ескіз ардуїно, який є millis() >> 2; Чи було б занадто багато просити просто розділити?
Пол Віланд

1
Я протестував i / 32vs i >> 5і i / 4vs i >> 2на gcc на cortex-a9 (який не має апаратного підрозділу) з оптимізацією -O3, і отримана збірка була точно такою ж. Спочатку мені не сподобалось використовувати підрозділи, але це описує мій намір, і результат такий же.
robsn

91

Просто конкретна міра: багато років тому я визначив дві версії свого алгоритму хешування:

unsigned
hash( char const* s )
{
    unsigned h = 0;
    while ( *s != '\0' ) {
        h = 127 * h + (unsigned char)*s;
        ++ s;
    }
    return h;
}

і

unsigned
hash( char const* s )
{
    unsigned h = 0;
    while ( *s != '\0' ) {
        h = (h << 7) - h + (unsigned char)*s;
        ++ s;
    }
    return h;
}

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

Зауважимо, що це було щось на кшталт 15 років тому. Сподіваємось, компілятори з тих пір тільки покращилися, тож ви можете майже розраховувати на те, що компілятор зробить правильно, можливо, краще, ніж ви могли. (Крім того, причина коду виглядає так C'ish, тому що це було більше 15 років тому. Я, очевидно, використовую std::stringі ітератори сьогодні.)


5
Можливо, вас зацікавить наступна публікація в блозі, в якій автор зазначає, що сучасні оптимізуючі компілятори, здається, зворотно розробляють загальні зразки, що програмісти можуть використовувати мислення їх більш ефективним у своїх математичних формах, щоб справді створити найбільш ефективну послідовність інструкцій для них . shape-of-code.coding-guidelines.com/2009/06/30/…
Паскаль Куок

@PascalCuoq Нічого насправді нового в цьому немає. Я виявив майже те саме, що для Sun CC близько 20 років тому.
Джеймс Канзе

67

На додаток до всіх інших хороших відповідей тут, дозвольте мені зазначити ще одну причину не використовувати зсув, коли ви маєте на увазі ділення або множення. Я ніколи не бачив, щоб хтось вводив помилку, забуваючи про відносну перевагу множення та додавання. Я бачив помилки, введені, коли програмісти з технічного обслуговування забули, що "множення" через зсув логічно є множенням, але не синтаксично того ж пріоритету, що і множення. x * 2 + zі x << 1 + zдуже різні!

Якщо ви працюєте над числами, тоді використовуйте такі арифметичні оператори + - * / %. Якщо ви працюєте над масивами бітів, використовуйте оператори подвійного подвійного перетворення, як-от & ^ | >>. Не змішуйте їх; вираз, який має як подвійне подвійне, так і арифметичне - це помилка, яка чекає цього.


5
Уникнути простих дужок?
Джоель Б

21
@Joel: Звичайно. Якщо ви пам’ятаєте, що вони вам потрібні. Моя думка, що легко забути, що ти робиш. Люди, які отримують ментальну звичку читати "x << 1" так, ніби це "x * 2", набувають ментальної звички думати, що << це той самий пріоритет, як множення, чого це не так.
Ерік Ліпперт

1
Ну, я вважаю, що вираз "(привіт << 8) + ло" є більш розкритим для намірів, ніж "привіт * 256 + ло". Напевно, це питання смаку, але іноді зрозуміліше писати біт-подвійність. У більшості випадків, хоча я повністю згоден з вашою думкою.
Іван Данилов

32
@Ivan: І "(привіт << 8) | ло" ще зрозуміліше. Встановлення малих бітів бітового масиву - це не додавання цілих чисел . Це встановлення бітів , тому напишіть код, який встановлює біти.
Ерік Ліпперт

1
Ого. Раніше не думав про це так. Дякую.
Іван Данилов

50

Це залежить від процесора та компілятора. Деякі компілятори вже оптимізують код таким чином, інші - ні. Тому вам потрібно перевіряти кожен раз, коли ваш код потрібно оптимізувати таким чином.

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


3
Просто, щоб додати приблизну оцінку: У типовому 16-бітовому процесорі (80C166) додавання двох входів відбувається на 1-2 цикли, множення на 10 циклів і ділення на 20 циклів. Плюс кілька операцій з переміщенням, якщо ви оптимізуєте i * 10 в декілька операцій (кожен цикл ще один цикл +1). Найпоширеніші компілятори (Keil / Tasking) не оптимізують, якщо не буде множення / ділення потужністю 2.
Йенс

55
І взагалі компілятор оптимізує код краще, ніж ви.
user703016

Я погоджуюсь, що при множенні "величин" оператор множення, як правило, краще, але при поділі підписаних значень на потужності 2, >>оператор швидше, /і, якщо підписані значення можуть бути негативними, він також часто семантично перевершує. Якщо потрібне значення, яке видавалося x>>4б, це набагато зрозуміліше x < 0 ? -((-1-x)/16)-1 : x/16;, і я не можу уявити, як компілятор міг би оптимізувати цей останній вираз до чогось приємного.
supercat

38

Чи насправді швидше використовувати say (i << 3) + (i << 1) для множення на 10, ніж безпосередньо використання i * 10?

Це може бути або не бути на вашій машині - якщо ви дбаєте, вимірюйте своє реальне використання.

Приклад - від 486 до основного i7

Бенчмаркінг дуже важко зробити змістовно, але ми можемо переглянути кілька фактів. З http://www.penguin.cz/~literakl/intel/s.html#SAL та http://www.penguin.cz/~literakl/intel/i.html#IMUL ми отримуємо уявлення про тактові цикли x86 необхідний для арифметичного зсуву та множення. Скажімо, ми дотримуємося "486" (найновіший у списку), 32-бітових регістрів та безпосередніх, IMUL займає 13-42 цикли, а IDIV 44. Кожен SAL займає 2 та додавання 1, так що навіть у кількох тих, хто разом зміщується, поверхово виглядає як переможець.

У наші дні з основним i7:

(від http://software.intel.com/en-us/forums/showthread.php?t=61481 )

Затримка становить 1 цикл для цілого додавання і 3 цикли для цілого множення . Ви можете знайти затримки та бітпут у Додатку С "Посібника з оптимізації архітектури Intel® 64 та IA-32", який розміщено на веб-сайті http://www.intel.com/products/processor/manuals/ .

(від деякого розмивання Intel)

Використовуючи SSE, Core i7 може видавати одночасні інструкції щодо додавання та множення, що призводить до максимальної швидкості 8 операцій з плаваючою комою (FLOP) за тактовий цикл

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

функціональність у вихідному коді та реалізації

Загалом, ваше запитання позначено тегами C і C ++. Як мови 3-го покоління, вони спеціально розроблені для приховування деталей базового набору ЦП. Щоб задовольнити свої мовні стандарти, вони повинні підтримувати операції множення та зсуву (та багато інших), навіть якщо базове обладнання не відповідає . У таких випадках вони повинні синтезувати необхідний результат, використовуючи багато інших інструкцій. Так само вони повинні надати програмну підтримку для операцій з плаваючою комою, якщо ЦП цього не вистачає, а FPU немає. Сучасні процесори всі підтримують* і<<, тому це може здатися абсурдно теоретичним та історичним, але важливим є те, що свобода вибору реалізації йде обома способами: навіть якщо ЦП має інструкцію, яка реалізує операцію, яку вимагають у вихідному коді, у загальному випадку, компілятор вільний виберіть щось інше, що йому надається перевагу, оскільки це краще для конкретного випадку, з яким стикається компілятор.

Приклади (з гіпотетичною мовою складання)

source           literal approach         optimised approach
#define N 0
int x;           .word x                xor registerA, registerA
x *= N;          move x -> registerA
                 move x -> registerB
                 A = B * immediate(0)
                 store registerA -> x
  ...............do something more with x...............

Інструкції на зразок ексклюзивного або ( xor) не мають жодного відношення до вихідного коду, але, якщо що-небудь із себе очищає всі біти, тому його можна використовувати для встановлення чого-небудь 0. Вихідний код, що передбачає адреси пам'яті, не може спричинити жодне використання.

Такі хаки використовувались до тих пір, поки комп'ютери були навколо. У перші дні 3GL, щоб забезпечити засвоєння розробником, висновок компілятора повинен був задовольнити існуючий жорсткий оптимізаційний вручну розробник мови мовлення. співтовариство, що створений код не був повільнішим, більш багатослівним або іншим чином гіршим. Компілятори швидко застосували багато великих оптимізацій - вони стали кращим централізованим сховищем, ніж це може бути будь-який програміст окремої мови монтажу, хоча завжди є ймовірність, що вони пропустять конкретну оптимізацію, яка може бути вирішальною у конкретному випадку - людина може іноді Зробити це і помацати щось краще, тоді як компілятори роблять так, як їм сказано, поки хтось не подасть досвід у них.

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

Технічне обслуговування

Якщо ви зміните обладнання, ви можете перекомпілювати його, і він перегляне цільовий процесор і зробить інший найкращий вибір, тоді як ви навряд чи захочете переглянути свої "оптимізації" або перелічити, у яких середовищах компіляції слід використовувати множення, а які слід змінювати. Подумайте про всі "оптимізації", написані ще 10 років тому, що не мають потужності, і які зараз уповільнюють код, який він працює, як це працює на сучасних процесорах ...!

На щастя, хороші компілятори, такі як GCC, як правило, можуть замінити серію розрядних змін і арифметику з прямим множенням, коли включена будь-яка оптимізація (тобто ...main(...) { return (argc << 4) + (argc << 2) + argc; }-> imull $21, 8(%ebp), %eax), тому перекомпіляція може допомогти навіть без виправлення коду, але це не гарантується.

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

Загальні рішення проти часткових рішень

Якщо у вас є кілька додаткових знань, наприклад, що ваш файл intбуде зберігати лише значення x, yі z, можливо, ви зможете опрацювати деякі вказівки, які працюють для цих значень, і ви отримаєте результат швидше, ніж коли у компілятора немає це розуміння і потребує реалізації, яка працює для всіх intцінностей. Наприклад, врахуйте своє запитання:

Множення та ділення можна досягти за допомогою бітових операторів ...

Ви ілюструєте множення, а як щодо поділу?

int x;
x >> 1;   // divide by 2?

Відповідно до стандарту C ++ 5.8:

-3- Значення E1 >> E2 - це позиції E1, зрушені праворуч E2. Якщо E1 має непідписаний тип або якщо E1 має підписаний тип і негативне значення, значенням результату є складова частина коефіцієнта E1, поділена на величину 2, підняту на потужність E2. Якщо E1 має підписаний тип і негативне значення, отримане значення визначається реалізацією.

Отже, ваш бітовий зсув має результат, визначений реалізацією, коли xнегативний: він може працювати не однаково на різних машинах. Але, /працює набагато передбачуваніше. (Це може бути і не зовсім послідовно, оскільки різні машини можуть мати різні подання від'ємних чисел, а отже, і різні діапазони, навіть якщо є однакова кількість бітів, що складають представлення.)

Ви можете сказати: "Мені все одно ... intце зберігає вік працівника, це ніколи не може бути негативним". Якщо у вас є таке особливе розуміння, то так - >>компілятор може передати вашу безпечну оптимізацію, якщо ви прямо не зробите це у своєму коді. Але це ризиковано і рідко корисно, оскільки багато часу ви не матимете такого розуміння, а інші програмісти, що працюють над тим самим кодом, не дізнаються, що ви зробили ставку на будинок на якісь незвичні очікування даних, які ви " Буду працювати ... те, що здається абсолютно безпечним для них зміною, може призвести до негативного впливу через вашу "оптимізацію".

Чи є якісь дані, які не можна примножувати чи ділити таким чином?

Так ... як було сказано вище, негативні числа мають певну реалізацію поведінки, коли вони "розділені" шляхом переміщення бітів.


2
Дуже приємна відповідь. Порівняння Core i7 проти 486 - це освічуюче!
Дрю Холл

У всіх звичних архітектурах intVal>>1буде однакова семантика, яка відрізняється від тієї, intVal/2що часом корисна. Якщо потрібно обчислити переносним способом значення, яке дало б банальна архітектура intVal >> 1, вираз повинен бути набагато складнішим і складнішим для читання, і, ймовірно, генеруватиме істотно неповноцінний код, ніж створений для intVal >> 1.
Supercat

35

Щойно спробував на моїй машині, компілюючи це:

int a = ...;
int b = a * 10;

При демонтажі він дає вихід:

MOV EAX,DWORD PTR SS:[ESP+1C] ; Move a into EAX
LEA EAX,DWORD PTR DS:[EAX+EAX*4] ; Multiply by 5 without shift !
SHL EAX, 1 ; Multiply by 2 using shift

Ця версія швидша за ваш оптимізований вручну код із чистим зрушенням та доповненням.

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


1
Ви б отримали велику оцінку за це, якби пропустили частину про вектор. Якщо компілятор може виправити множення, він також може побачити, що вектор не змінюється.
Бо Персон

Як компілятор може знати, що розмір вектора не зміниться, не роблячи дійсно небезпечних припущень? Або ви ніколи не чули про одночасність ...
Чарльз Гудвін

1
Гаразд, значить, ви переходите на глобальний вектор без замків? І я переглядаю локальний вектор, адреса якого не прийнято, і викликаю лише функції const-члена. Принаймні мій компілятор розуміє, що розмір вектора не зміниться. (і незабаром хтось, напевно, позначить нас для спілкування в чаті :-).
Бо Персон

1
@BoPersson Нарешті, після закінчення цього часу я видалив свою заяву про компілятор, який не в змозі оптимізувати vector<T>::size(). Мій упорядник був досить давнім! :)
user703016

21

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

Насправді трюк поділу, відомий як "магічний поділ", може насправді отримати великі виплати. Знову спершу слід спробувати профіль, щоб побачити, чи потрібно. Але якщо ви цим користуєтеся, є корисні програми, які допоможуть вам зрозуміти, які вказівки потрібні для тієї ж семантики поділу. Ось приклад: http://www.masm32.com/board/index.php?topic=12421.0

Приклад, який я підняв з потоку ОП на MASM32:

include ConstDiv.inc
...
mov eax,9999999
; divide eax by 100000
cdiv 100000
; edx = quotient

Генерує:

mov eax,9999999
mov edx,0A7C5AC47h
add eax,1
.if !CARRY?
    mul edx
.endif
shr edx,16

7
@ Зніміть, чомусь ваш коментар змусив мене сміятися і пролити каву. Дякую.
asawyer

30
Немає випадкових тем на форумах про те, як сподобатися математиці. Кожен, хто любить математику, знає, як важко створити справжню "випадкову" нитку на форумі.
Джоель Б

1
Напевно, варто зробити такі речі, якщо ви профайлювали та виявили, що це вузьке місце, і знову застосували альтернативи та профілі та отримаєте принаймні 10-кратну перевагу в роботі .
Лже Раян

12

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

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

#include <stdio.h>

int main(void)
{
    int i;

    for (i = 5; i >= -5; --i)
    {
        printf("%d / 2 = %d, %d >> 1 = %d\n", i, i / 2, i, i >> 1);
    }
    return 0;
}

Вихід:

5 / 2 = 2, 5 >> 1 = 2
4 / 2 = 2, 4 >> 1 = 2
3 / 2 = 1, 3 >> 1 = 1
2 / 2 = 1, 2 >> 1 = 1
1 / 2 = 0, 1 >> 1 = 0
0 / 2 = 0, 0 >> 1 = 0
-1 / 2 = 0, -1 >> 1 = -1
-2 / 2 = -1, -2 >> 1 = -1
-3 / 2 = -1, -3 >> 1 = -2
-4 / 2 = -2, -4 >> 1 = -2
-5 / 2 = -2, -5 >> 1 = -3

Тож якщо ви хочете допомогти компілятору, то переконайтеся, що змінна чи вираз у дивіденді явно не підписані.


4
Множини цілого числа мікрокодуються, наприклад, в PPU PlayStation 3, і зупиняють весь конвеєр. На деяких платформах рекомендується уникати цілого множення :)
Maister

2
Багато непідписаних підрозділів - якщо припустити, що компілятор знає, - реалізовані за допомогою безпідписаних множин. Один або два множення @ кілька тактових циклів кожен може виконувати ту ж роботу, що і ділення @ 40 циклів кожен і вище.
Олоф Форшелл

1
@Olof: правда, але діє лише для ділення на константи часу компіляції, звичайно
Paul R

4

Це повністю залежить від цільового пристрою, мови, призначення тощо.

Хрускіт пікселів у драйвері відеокарти? Дуже ймовірно, так!

.NET бізнес-додаток для вашого відділу? Абсолютно немає підстав навіть заглянути в це.

Для високопродуктивної гри для мобільного пристрою, можливо, варто заглянути, але лише після того, як будуть виконані більш прості оптимізації.


2

Не робіть, якщо вам абсолютно не потрібно, а ваш код має на увазі зміщення, а не множення / ділення.

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

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


1

Наскільки мені відомо, для деяких машин множення може потребувати до 16 до 32 машинного циклу. Отже, так , залежно від типу машини, оператори бітчіфтів швидше, ніж множення / ділення.

Однак деякі машини мають свій математичний процесор, який містить спеціальні інструкції щодо множення / ділення.


7
Люди, що пишуть компілятори для цих машин, також, ймовірно, читають Hackers Delight та оптимізують їх відповідно.
Бо Персон

1

Я погоджуюся з поміченою відповіддю Дрю Холл. Однак у відповіді можуть бути використані додаткові замітки.

Для переважної більшості розробників програмного забезпечення процесор і компілятор вже не стосуються питання. Більшість з нас далеко за межами 8088 та MS-DOS. Це, мабуть, актуально лише для тих, хто ще розробляє вбудовані процесори ...

У моїй програмі компанії Math (add / sub / mul / div) слід використовувати всю математику. Хоча Shift слід використовувати при перетворенні між типами даних, наприклад. угорте байт як n >> 8, а не n / 256.


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

0

У випадку підписаних цілих чисел та правого зсуву проти ділення це може змінити значення. Для від'ємних чисел зсув кругляє до негативної нескінченності, тоді як поділ - до нуля. Звичайно, компілятор змінить поділ на щось дешевше, але зазвичай він змінить його на те, що має таку саму поведінку округлення, як поділ, оскільки він або не в змозі довести, що змінна не буде негативною, або просто не турбота. Тож якщо ви зможете довести, що число не буде негативним або якщо вам не байдуже, яким чином воно буде закруглене, ви можете зробити цю оптимізацію способом, який, швидше за все, змінить значення.


або киньте номер наunsigned
Lie Ryan

4
Ви впевнені, що поведінка перемикання стандартизована? У мене було враження, що зміщення правої позиції на мінус-ints визначається реалізацією.
Керрек СБ

1
Хоча, можливо, ви повинні згадати той код, який спирається на будь-яку особливу поведінку для відхилення вправо від'ємних чисел, повинен задокументувати цю вимогу, перевага перед правильним зміщенням величезна в тих випадках, коли він, природно, дає потрібне значення і оператор поділу генерує код, щоб витрачати час обчислення небажаного значення, котрий код користувача потім повинен був би витрачати додатковий час на коригування, щоб отримати те, що зміна дала б в першу чергу. Насправді, якби я мав свої барабанщики, у компіляторів була б можливість простукувати при спробах виконати підписаний поділ, оскільки ...
supercat

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

0

Тест Пітона, що виконує одне і те ж множення 100 мільйонів разів проти однакових випадкових чисел.

>>> from timeit import timeit
>>> setup_str = 'import scipy; from scipy import random; scipy.random.seed(0)'
>>> N = 10*1000*1000
>>> timeit('x=random.randint(65536);', setup=setup_str, number=N)
1.894096851348877 # Time from generating the random #s and no opperati

>>> timeit('x=random.randint(65536); x*2', setup=setup_str, number=N)
2.2799630165100098
>>> timeit('x=random.randint(65536); x << 1', setup=setup_str, number=N)
2.2616429328918457

>>> timeit('x=random.randint(65536); x*10', setup=setup_str, number=N)
2.2799630165100098
>>> timeit('x=random.randint(65536); (x << 3) + (x<<1)', setup=setup_str, number=N)
2.9485139846801758

>>> timeit('x=random.randint(65536); x // 2', setup=setup_str, number=N)
2.490908145904541
>>> timeit('x=random.randint(65536); x / 2', setup=setup_str, number=N)
2.4757170677185059
>>> timeit('x=random.randint(65536); x >> 1', setup=setup_str, number=N)
2.2316000461578369

Отже, роблячи зміну, а не множення / ділення на потужність двох в пітоні, є незначне поліпшення (~ 10% для ділення; ~ 1% для множення). Якщо його потужність дві, ймовірно, значне уповільнення.

Знову ці # будуть змінюватися залежно від вашого процесора, вашого компілятора (або інтерпретатора - зроблено в python для простоти).

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


0

Існують оптимізації, які компілятор не може зробити, оскільки вони працюють лише для зменшеного набору входів.

Нижче наведено зразок коду c ++, який може зробити більш швидкий поділ, роблячи 64-бітне "Множення на зворотну". І чисельник, і знаменник повинні бути нижче певного порогу. Зауважте, що вона повинна бути складена для використання інструкцій на 64 біт, щоб бути фактично швидшим, ніж звичайне ділення.

#include <stdio.h>
#include <chrono>

static const unsigned s_bc = 32;
static const unsigned long long s_p = 1ULL << s_bc;
static const unsigned long long s_hp = s_p / 2;

static unsigned long long s_f;
static unsigned long long s_fr;

static void fastDivInitialize(const unsigned d)
{
    s_f = s_p / d;
    s_fr = s_f * (s_p - (s_f * d));
}

static unsigned fastDiv(const unsigned n)
{
    return (s_f * n + ((s_fr * n + s_hp) >> s_bc)) >> s_bc;
}

static bool fastDivCheck(const unsigned n, const unsigned d)
{
    // 32 to 64 cycles latency on modern cpus
    const unsigned expected = n / d;

    // At least 10 cycles latency on modern cpus
    const unsigned result = fastDiv(n);

    if (result != expected)
    {
        printf("Failed for: %u/%u != %u\n", n, d, expected);
        return false;
    }

    return true;
}

int main()
{
    unsigned result = 0;

    // Make sure to verify it works for your expected set of inputs
    const unsigned MAX_N = 65535;
    const unsigned MAX_D = 40000;

    const double ONE_SECOND_COUNT = 1000000000.0;

    auto t0 = std::chrono::steady_clock::now();
    unsigned count = 0;
    printf("Verifying...\n");
    for (unsigned d = 1; d <= MAX_D; ++d)
    {
        fastDivInitialize(d);
        for (unsigned n = 0; n <= MAX_N; ++n)
        {
            count += !fastDivCheck(n, d);
        }
    }
    auto t1 = std::chrono::steady_clock::now();
    printf("Errors: %u / %u (%.4fs)\n", count, MAX_D * (MAX_N + 1), (t1 - t0).count() / ONE_SECOND_COUNT);

    t0 = t1;
    for (unsigned d = 1; d <= MAX_D; ++d)
    {
        fastDivInitialize(d);
        for (unsigned n = 0; n <= MAX_N; ++n)
        {
            result += fastDiv(n);
        }
    }
    t1 = std::chrono::steady_clock::now();
    printf("Fast division time: %.4fs\n", (t1 - t0).count() / ONE_SECOND_COUNT);

    t0 = t1;
    count = 0;
    for (unsigned d = 1; d <= MAX_D; ++d)
    {
        for (unsigned n = 0; n <= MAX_N; ++n)
        {
            result += n / d;
        }
    }
    t1 = std::chrono::steady_clock::now();
    printf("Normal division time: %.4fs\n", (t1 - t0).count() / ONE_SECOND_COUNT);

    getchar();
    return result;
}

0

Я думаю, що в одному випадку, якщо ви хочете помножити або розділити на потужність дві, ви не можете помилитися з використанням операторів бітшіфті, навіть якщо компілятор перетворює їх на MUL / DIV, тому що деякі кодери процесорів мікрокодують (справді, a макрос) у будь-якому випадку, тож у цих випадках ви досягнете покращення, особливо якщо зсув більший за 1. Або більш чітко, якщо у процесора немає операторів бітсіфта, це все одно буде MUL / DIV, але якщо процесор має Оператори бітової передачі, ви уникаєте відділення мікрокоду, і це на кілька інструкцій менше.

Я зараз пишу якийсь код, який вимагає багато операцій подвоєння / скорочення вдвічі, тому що він працює на щільному бінарному дереві, і є ще одна операція, яка, як я підозрюю, може бути оптимальнішою, ніж додавання - ліва (потужність двох помножити ) зсув з додаванням. Це можна замінити лівим зсувом і xor, якщо зсув ширший за кількість бітів, які ви хочете додати, простим прикладом є (i << 1) ^ 1, який додає один до подвоєного значення. Це, звичайно, не стосується правого зсуву (потужність двох ділення), оскільки лише лівий (маленький ендіанський) зсув заповнює проміжок нулями.

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

Також в алгоритмах, про які я пишу, вони візуально представляють рухи, які відбуваються, тому в цьому сенсі вони насправді більш чіткі. Ліва частина бінарного дерева більша, а права - менша. Крім того, в моєму коді непарні і парні числа мають особливе значення, і всі ліворукі діти на дереві непарні, а діти праворуч, а корінь - парні. У деяких випадках, з якими я ще не стикався, але, о, власне, я навіть про це не думав, x & 1 може бути більш оптимальною операцією порівняно з x% 2. x & 1 на парне число дасть нуль, але створить 1 для непарного числа.

Якщо піти трохи далі, ніж просто непарна / парна ідентифікація, якщо я отримаю нуль для x & 3, я знаю, що 4 - це коефіцієнт нашого числа, і те саме для x% 7 для 8 тощо. Я знаю, що ці випадки, ймовірно, мають обмежену корисність, але приємно знати, що ви можете уникнути операції з модулем і використовувати натомість побітну логічну операцію, тому що бітові операції майже завжди є найшвидшими і, найменше, неоднозначними для компілятора.

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



0

Якщо ви порівняєте вихід для синтаксису x + x, x * 2 та x << 1 на компіляторі gcc, то ви отримаєте такий же результат у збірці x86: https://godbolt.org/z/JLpp0j

        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-4], edi
        mov     eax, DWORD PTR [rbp-4]
        add     eax, eax
        pop     rbp
        ret

Таким чином, ви можете вважати gcc розумним, щоб визначити його найкраще рішення незалежно від того, що ви ввели.


0

Я теж хотів подивитися, чи зможу я обіграти будинок. це більш загальне порозрядне значення для будь-якого числа на будь-яке множення чисел. макроси, які я зробив, приблизно на 25% вдвічі повільніші, ніж звичайне * множення. як говорять інші, якщо це близько до кратного 2 або складається з декількох кратних 2, ви можете виграти. як X * 23, складений з (X << 4) + (X << 2) + (X << 1) + X буде повільніше, ніж X * 65, складений з (X << 6) + X.

#include <stdio.h>
#include <time.h>

#define MULTIPLYINTBYMINUS(X,Y) (-((X >> 30) & 1)&(Y<<30))+(-((X >> 29) & 1)&(Y<<29))+(-((X >> 28) & 1)&(Y<<28))+(-((X >> 27) & 1)&(Y<<27))+(-((X >> 26) & 1)&(Y<<26))+(-((X >> 25) & 1)&(Y<<25))+(-((X >> 24) & 1)&(Y<<24))+(-((X >> 23) & 1)&(Y<<23))+(-((X >> 22) & 1)&(Y<<22))+(-((X >> 21) & 1)&(Y<<21))+(-((X >> 20) & 1)&(Y<<20))+(-((X >> 19) & 1)&(Y<<19))+(-((X >> 18) & 1)&(Y<<18))+(-((X >> 17) & 1)&(Y<<17))+(-((X >> 16) & 1)&(Y<<16))+(-((X >> 15) & 1)&(Y<<15))+(-((X >> 14) & 1)&(Y<<14))+(-((X >> 13) & 1)&(Y<<13))+(-((X >> 12) & 1)&(Y<<12))+(-((X >> 11) & 1)&(Y<<11))+(-((X >> 10) & 1)&(Y<<10))+(-((X >> 9) & 1)&(Y<<9))+(-((X >> 8) & 1)&(Y<<8))+(-((X >> 7) & 1)&(Y<<7))+(-((X >> 6) & 1)&(Y<<6))+(-((X >> 5) & 1)&(Y<<5))+(-((X >> 4) & 1)&(Y<<4))+(-((X >> 3) & 1)&(Y<<3))+(-((X >> 2) & 1)&(Y<<2))+(-((X >> 1) & 1)&(Y<<1))+(-((X >> 0) & 1)&(Y<<0))
#define MULTIPLYINTBYSHIFT(X,Y) (((((X >> 30) & 1)<<31)>>31)&(Y<<30))+(((((X >> 29) & 1)<<31)>>31)&(Y<<29))+(((((X >> 28) & 1)<<31)>>31)&(Y<<28))+(((((X >> 27) & 1)<<31)>>31)&(Y<<27))+(((((X >> 26) & 1)<<31)>>31)&(Y<<26))+(((((X >> 25) & 1)<<31)>>31)&(Y<<25))+(((((X >> 24) & 1)<<31)>>31)&(Y<<24))+(((((X >> 23) & 1)<<31)>>31)&(Y<<23))+(((((X >> 22) & 1)<<31)>>31)&(Y<<22))+(((((X >> 21) & 1)<<31)>>31)&(Y<<21))+(((((X >> 20) & 1)<<31)>>31)&(Y<<20))+(((((X >> 19) & 1)<<31)>>31)&(Y<<19))+(((((X >> 18) & 1)<<31)>>31)&(Y<<18))+(((((X >> 17) & 1)<<31)>>31)&(Y<<17))+(((((X >> 16) & 1)<<31)>>31)&(Y<<16))+(((((X >> 15) & 1)<<31)>>31)&(Y<<15))+(((((X >> 14) & 1)<<31)>>31)&(Y<<14))+(((((X >> 13) & 1)<<31)>>31)&(Y<<13))+(((((X >> 12) & 1)<<31)>>31)&(Y<<12))+(((((X >> 11) & 1)<<31)>>31)&(Y<<11))+(((((X >> 10) & 1)<<31)>>31)&(Y<<10))+(((((X >> 9) & 1)<<31)>>31)&(Y<<9))+(((((X >> 8) & 1)<<31)>>31)&(Y<<8))+(((((X >> 7) & 1)<<31)>>31)&(Y<<7))+(((((X >> 6) & 1)<<31)>>31)&(Y<<6))+(((((X >> 5) & 1)<<31)>>31)&(Y<<5))+(((((X >> 4) & 1)<<31)>>31)&(Y<<4))+(((((X >> 3) & 1)<<31)>>31)&(Y<<3))+(((((X >> 2) & 1)<<31)>>31)&(Y<<2))+(((((X >> 1) & 1)<<31)>>31)&(Y<<1))+(((((X >> 0) & 1)<<31)>>31)&(Y<<0))
int main()
{
    int randomnumber=23;
    int randomnumber2=23;
    int checknum=23;
    clock_t start, diff;
    srand(time(0));
    start = clock();
    for(int i=0;i<1000000;i++)
    {
        randomnumber = rand() % 10000;
        randomnumber2 = rand() % 10000;
        checknum=MULTIPLYINTBYMINUS(randomnumber,randomnumber2);
        if (checknum!=randomnumber*randomnumber2)
        {
            printf("s %i and %i and %i",checknum,randomnumber,randomnumber2);
        }
    }
    diff = clock() - start;
    int msec = diff * 1000 / CLOCKS_PER_SEC;
    printf("MULTIPLYINTBYMINUS Time %d milliseconds", msec);
    start = clock();
    for(int i=0;i<1000000;i++)
    {
        randomnumber = rand() % 10000;
        randomnumber2 = rand() % 10000;
        checknum=MULTIPLYINTBYSHIFT(randomnumber,randomnumber2);
        if (checknum!=randomnumber*randomnumber2)
        {
            printf("s %i and %i and %i",checknum,randomnumber,randomnumber2);
        }
    }
    diff = clock() - start;
    msec = diff * 1000 / CLOCKS_PER_SEC;
    printf("MULTIPLYINTBYSHIFT Time %d milliseconds", msec);
    start = clock();
    for(int i=0;i<1000000;i++)
    {
        randomnumber = rand() % 10000;
        randomnumber2 = rand() % 10000;
        checknum= randomnumber*randomnumber2;
        if (checknum!=randomnumber*randomnumber2)
        {
            printf("s %i and %i and %i",checknum,randomnumber,randomnumber2);
        }
    }
    diff = clock() - start;
    msec = diff * 1000 / CLOCKS_PER_SEC;
    printf("normal * Time %d milliseconds", msec);
    return 0;
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.