Я думаю, що запитувачеві було б корисніше отримати більш диференційовану відповідь, тому що я бачу кілька недосліджених припущень у питаннях та в деяких відповідях чи коментарях.
Отриманий в результаті відносний час переміщення та множення не має нічого спільного з C. Коли я кажу, я не маю на увазі екземпляр конкретної реалізації, наприклад, тієї чи іншої версії GCC, а мову. Я не маю на увазі сприйняття цього абсурду, але використовувати крайній приклад для ілюстрації: ви могли б реалізувати цілком сумісний стандартний компілятор C і мультиплікація займає годину, а зміна займає мілісекунди - або навпаки. Мені невідомі такі обмеження продуктивності в C або C ++.
Ви можете не перейматися цією технічністю в аргументації. Ваш намір, ймовірно, був лише перевірити відносну ефективність зміни зрушення проти множення, і ви вибрали C, оскільки він, як правило, сприймається як мова програмування низького рівня, тому можна очікувати, що його вихідний код перекладеться у відповідні інструкції безпосередньо. Такі питання дуже поширені, і я думаю, що хороша відповідь повинна вказувати на те, що навіть у C ваш вихідний код не перекладається в інструкції так прямо, як ви можете подумати в даному екземплярі. Я дав вам декілька можливих результатів компіляції нижче.
Тут надходять коментарі, які ставлять під сумнів корисність заміни цієї еквівалентності в реальному програмному забезпеченні. Деякі ви можете побачити в коментарях до свого запитання, наприклад, від Еріка Ліпперта. Це відповідає реакції, яку ви, як правило, отримаєте від більш досвідчених інженерів у відповідь на такі оптимізації. Якщо ви використовуєте двійкові зсуви у виробничому коді як ковдрний засіб множення та ділення, люди, швидше за все, будуть чіплятись за вашим кодом і матимуть певну ступінь емоційної реакції ("Я чув, що це безглузде твердження, зроблене щодо JavaScript заради неба") на це може не мати сенсу для початківців програмістів, якщо вони краще не зрозуміють причин цих реакцій.
Ці причини - це насамперед поєднання зменшеної читабельності та марності такої оптимізації, як ви, можливо, з’ясували вже при порівнянні їх відносних показників. Однак я не думаю, що люди мали би настільки сильну реакцію, якби заміна зсуву на множення була єдиним прикладом таких оптимізацій. Питання, подібні до вашого, часто виникають у різних формах і в різних контекстах. Я думаю, що більш старші інженери насправді реагують настільки рішуче, принаймні, як це я часом, - це те, що існує потенціал для набагато ширшого діапазону шкоди, коли люди використовують такі мікрооптимізації вільно за кодовою базою. Якщо ви працюєте в такій компанії, як Microsoft, на великій базі коду, ви витратите багато часу, читаючи вихідний код інших інженерів, або намагаєтесь знайти в ньому певний код. Це може бути навіть ваш власний код, який ви будете намагатися зрозуміти через кілька років, особливо в деякі найбільш невідповідні часи, наприклад, коли вам доведеться виправити відключення виробництва після дзвінка, який ви отримали на пейджері обов'язок в ніч на п’ятницю, ось-ось вирушайте на ніч розваг з друзями… Якщо ви витратите стільки часу на читання коду, ви оціните це як можна читати. Уявіть, що читаєте ваш улюблений роман, але видавець вирішив випустити нове видання, де вони використовують abbrv. всі ovr th plc bcs Your thnk це svs spc. Це схоже на реакції, які можуть мати інші інженери на ваш код, якщо ви посипаєте їх такими оптимізаціями. Як вказували інші відповіді, краще чітко зазначити, що ви маєте на увазі,
Навіть у таких середовищах ви можете вирішити питання про інтерв'ю, де, як очікується, знаєте цю чи іншу еквівалентність. Знання їх не є поганим, і хороший інженер усвідомлює арифметичний ефект бінарного зсуву. Зауважте, що я не сказав, що це робить хорошого інженера, але що хороший інженер знав би, на мою думку. Зокрема, ви все ще можете знайти менеджера, як правило, наприкінці свого циклу інтерв'ю, який широко посміхнеться перед вами в очікуванні радості, щоб розкрити вам цей розумний інженерний "трюк" у кодовому питанні та довести, що він / вона теж раніше був або є одним з кмітливих інженерів, а не "просто" менеджером. У таких ситуаціях просто намагайтеся вразити враження і подякуйте йому / їй за просвітлююче інтерв'ю.
Чому ви не бачили різниці швидкостей у С? Найімовірнішою відповіддю є те, що обидва вони призвели до одного і того ж коду збірки:
int shift(int i) { return i << 2; }
int multiply(int i) { return i * 2; }
Можна обидва компілювати в
shift(int):
lea eax, [0+rdi*4]
ret
У GCC без оптимізацій, тобто, використовуючи прапор "-O0", ви можете отримати таке:
shift(int):
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov eax, DWORD PTR [rbp-4]
sal eax, 2
pop rbp
ret
multiply(int):
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov eax, DWORD PTR [rbp-4]
add eax, eax
pop rbp
ret
Як бачите, передача "-O0" GCC не означає, що він не буде дещо розумним щодо того, який код він виробляє. Зокрема, зауважте, що навіть у цьому випадку компілятор уникав використання інструкції множення. Ви можете повторити той же експеримент зі зрушеннями на інші числа і навіть множення на числа, що не мають сили двох. Цілком ймовірно, що на вашій платформі ви побачите комбінацію змін і доповнень, але ніяких множин. Мабуть, збіг обставин для компілятора очевидно уникає використання множин у всіх тих випадках, якщо множення та зсуви дійсно мали однакову вартість, чи не так? Але я не маю на увазі висловлювати припущення для доказування, тому перейдемо далі.
Ви можете повторно перевірити тест із наведеним вище кодом і побачити, чи помічаєте зараз різницю швидкості. Навіть тоді ви не випробовуєте зсув проти множення, як ви можете бачити через відсутність множення, але код, який був створений з певним набором прапорів GCC для операцій C зсуву та множення в конкретному екземплярі . Отже, в іншому тесті ви можете відредагувати код складання вручну і замість цього використати інструкцію "imul" у коді для методу "множити".
Якщо ви хочете перемогти деякі з цих розумних можливостей компілятора, ви можете визначити більш загальний метод зсуву та множення, і вийде щось подібне:
int shift(int i, int j) { return i << j; }
int multiply(int i, int j) { return i * j; }
Що може дати наступний код складання:
shift(int, int):
mov eax, edi
mov ecx, esi
sal eax, cl
ret
multiply(int, int):
mov eax, edi
imul eax, esi
ret
Ось ми нарешті, навіть на найвищому рівні оптимізації GCC 4.9, вираження в інструкціях по збірці, яких ви могли очікувати, коли ви спочатку відправилися на свій тест. Я думаю, що саме по собі може бути важливим уроком оптимізації продуктивності. Ми можемо побачити різницю, яку він змінив для заміни змінних на конкретні константи в нашому коді, з точки зору розумних можливостей, які компілятор може застосувати. Мікрооптимізації, такі як підміна підміни зсуву, є деякими оптимізаціями низького рівня, які компілятор зазвичай може легко зробити сам. Інші оптимізації, які значно впливають на продуктивність, вимагають розуміння наміру кодущо компілятор часто недоступний або про це може лише здогадуватися якийсь евристичний. Саме тут ви, як інженер програмного забезпечення, приїжджаєте, і це, звичайно, не передбачає заміни множин зі змінами. Він включає такі фактори, як уникнення зайвого дзвінка до служби, яка виробляє введення-виведення та може блокувати процес. Якщо ви перейдете на свій жорсткий диск або, не дай Бог, у віддалену базу даних, щоб отримати додаткові дані, які ви могли б отримати з того, що у вас вже є в пам'яті, час, який ви витрачаєте на очікування, перевищує виконання мільйона інструкцій. Тепер я думаю, що ми трохи віддалилися від вашого початкового запитання, але я думаю, що це вказало на запитання, особливо якщо ми припускаємо, що хтось тільки починає розуміти переклад та виконання коду,
Отже, який з них буде швидше? Я думаю, що це вдалий підхід, який ви вирішили перевірити на різниці в продуктивності. Загалом, легко здивуватися виконанням деяких змін коду. Сучасні процесори використовують багато методик, і взаємодія між програмним забезпеченням також може бути складною. Навіть якщо ви отримаєте корисні результати роботи для певної зміни в одній ситуації, я вважаю, що небезпечно зробити висновок, що такий тип змін завжди принесе переваги ефективності. Я думаю, що небезпечно запускати такі тести один раз, сказати "Гаразд, тепер я знаю, який з них швидше!" а потім без розбору застосовувати ту саму оптимізацію до виробничого коду, не повторюючи ваші вимірювання.
Що робити, якщо зсув швидше, ніж множення? Звичайно, є вказівки, чому це було б правдою. Як ви бачите вище, GCC, здається, думає (навіть без оптимізації), що уникати прямого множення на користь інших інструкцій є хорошою ідеєю. Intel 64 і IA-32 Архітектури Optimization Довідкове керівництво дасть вам уявлення про відносну вартості команд процесора. Інший ресурс, більш орієнтований на затримку та пропускну здатність інструкцій, - http://www.agner.org/optimize/instruction_tables.pdf. Зауважте, що вони не є хорошим прогнозатором абсолютного виконання, але виконання інструкцій відносно один одного. У тісному циклі, як моделює ваш тест, показник "пропускної здатності" повинен бути найбільш релевантним. Це кількість циклів, до яких зазвичай прив'язується блок виконання при виконанні даної інструкції.
Що робити, якщо зсув НЕ швидше, ніж множення? Як я вже говорив вище, сучасна архітектура може бути досить складною, і такі речі, як передбачення гілок, кешування, конвеєрне обладнання та паралельне виконання, можуть важко передбачити відносну ефективність двох логічно еквівалентних фрагментів коду. Я дуже хочу наголосити на цьому, тому що саме тут я не задоволений більшістю відповідей на подібні запитання та з табором людей, що прямо говорять про те, що перехід швидше, ніж примноження, просто не відповідає дійсності.
Ні, наскільки я знаю, ми не винайшли якийсь секретний інженерний соус у 1970-х роках або коли-небудь, щоб раптом анулювати різницю у вартості одиниці множення та трохи змінити. Загальне множення з точки зору логічних воріт і, безумовно, з точки зору логічних операцій, все ще складніше, ніж зсув зі зсувом бочки у багатьох сценаріях, у багатьох архітектурах. Як це перетворюється на загальний час роботи на настільному комп'ютері, може бути трохи непрозорим. Я точно не знаю, як вони реалізовані в конкретних процесорах, але ось пояснення множення: чи дійсно множинне множення дійсно така ж швидкість, як додавання в сучасний процесор
Поки тут є пояснення перемикача бочки . Документи, на які я посилався в попередньому пункті, дають ще одне уявлення про відносну вартість операцій, через довіреність інструкцій процесора. У інженерів Intel часто виникають подібні запитання: тактові цикли на форумах для розробників Intel для цілого множення та додавання в двоядерний процесор core 2
Так, у більшості сценаріїв реального життя, і майже напевно в JavaScript, спроба використовувати цей еквівалент заради досягнення продуктивності - це, мабуть, марно. Однак, навіть якщо ми змусили використовувати інструкції щодо множення, а потім не побачили різниці в часі виконання, це більше пояснюється характером метрики витрат, яку ми використовували, якщо бути точною, а не тому, що різниці у витратах немає. Кінцевий час виконання - це один показник, і якщо це єдиний, про який ми піклуємося, все добре. Але це не означає, що всі різниці у витратах між множенням та зміщенням просто зникли. І я вважаю, що це, безумовно, не годиться донести цю ідею допитуваному, неявно чи іншим чином, який, очевидно, тільки починає розуміти фактори, пов'язані з часом роботи та вартістю сучасного коду. Інжиніринг завжди стосується компромісів. Запит та пояснення того, які компроміси сучасні процесори домоглися демонструвати час виконання, який ми, як бачимо користувачі, може дати більш диференційований відповідь. І я думаю, що більш диференційована відповідь, ніж "це просто не відповідає дійсності", є обґрунтованою, якщо ми хочемо, щоб менше інженерів перевіряли мікрооптимізований код, що знищує читабельність, оскільки це потребує більш загального розуміння природи таких "оптимізацій", щоб помічайте різні, різноманітні втілення, ніж просто посилання на деякі конкретні випадки як застарілі.