Чи насправді швидше використовувати 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
це зберігає вік працівника, це ніколи не може бути негативним". Якщо у вас є таке особливе розуміння, то так - >>
компілятор може передати вашу безпечну оптимізацію, якщо ви прямо не зробите це у своєму коді. Але це ризиковано і рідко корисно, оскільки багато часу ви не матимете такого розуміння, а інші програмісти, що працюють над тим самим кодом, не дізнаються, що ви зробили ставку на будинок на якісь незвичні очікування даних, які ви " Буду працювати ... те, що здається абсолютно безпечним для них зміною, може призвести до негативного впливу через вашу "оптимізацію".
Чи є якісь дані, які не можна примножувати чи ділити таким чином?
Так ... як було сказано вище, негативні числа мають певну реалізацію поведінки, коли вони "розділені" шляхом переміщення бітів.