Коли я перевіряю різницю у часі між зміщенням та множенням на C, різниці немає. Чому?


28

Мене вчили, що зміщення у двійковій формі набагато ефективніше, ніж множення на 2 ^ k. Тому я хотів експериментувати, і я використав наступний код, щоб перевірити це:

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

int main() {
    clock_t launch = clock();
    int test = 0x01;
    int runs;

    //simple loop that oscillates between int 1 and int 2
    for (runs = 0; runs < 100000000; runs++) {


    // I first compiled + ran it a few times with this:
    test *= 2;

    // then I recompiled + ran it a few times with:
    test <<= 1;

    // set back to 1 each time
    test >>= 1;
    }

    clock_t done = clock();
    double diff = (done - launch);
    printf("%f\n",diff);
}

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


47
Хто б тебе не вчив, явно помилявся. Ця віра не є правдою з 1970-х років для типово використовуваних компіляторів для типово використовуваних архітектур. Добре для вас, що випробували цю претензію. Я чув цю безглузду претензію щодо JavaScript заради неба.
Ерік Ліпперт

21
Найкращий спосіб відповісти на подібні запитання - переглянути код складання, який виробляє компілятор. У компіляторів зазвичай є можливість створити копію мови складання, яку вони створюють. Для компіляторів GNU GCC це "-S".
Чарльз Е. Грант

8
Слід зазначити, що після перегляду цього gcc -S, код для test *= 2насправді компілюється в " shll $1, %eax Коли викликається gcc -O3 -S", навіть немає циклу. Два дзвінки на годинниковій лінії є один від одного:callq _clock movq %rax, %rbx callq _clock

6
"Мене вчили, що зміщення у двійковій формі набагато ефективніше, ніж множення на 2 ^ k"; нас навчають багато речей, які виявляються помилковими (або принаймні застарілими). Розумний компілятор буде використовувати однакову операцію зсуву для обох.
Джон Боде

9
Завжди завжди перевіряйте створений код складання, працюючи над подібною оптимізацією, щоб бути впевненим, що ви вимірюєте те, що ви думаєте, що вимірюєте. Величезна кількість запитань "чому я бачу ці часи" на SO так закінчується, що компілятор повністю виключає операції, оскільки результати не використовуються.
Рассел Борогов,

Відповіді:


44

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

Це дуже загальне правило при оптимізації: Більшість "оптимізацій" насправді помилково збирають компіляцію про те, що ви насправді маєте на увазі, і, можливо, навіть зменшите продуктивність.

Оптимізуйте лише тоді, коли ви помітили проблеми з продуктивністю та виміряли, у чому проблема. (і більшість кодів, які ми пишемо, не виконуються так часто, тому нам не потрібно турбуватися)

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


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

2
Чи коли-небудь (практично кажучи) виникне потреба в оптимізації множення до оператора зсуву в програмному забезпеченні високого рівня? Здається, оскільки компілятор вже оптимізує, що єдиний час, коли корисно мати ці знання, - це програмування на дуже низькому рівні (принаймні, нижче компілятора).
NicholasFolk

11
@NicholasFolk nope. Робіть те, що найпростіше зрозуміти. Якщо ви писали збірку безпосередньо, це може бути корисним ... або якщо ви пишете оптимізуючий компілятор, знову це може бути корисним. Але поза цими двома випадками його хитрість, яка затьмарює те, що ви робите, і змушує наступного програміста (хто вбивство сокири, хто знає, де ви живете ), проклинати ваше ім’я і думати зайнятися хобі.

2
@NicholasFolk: Оптимізація на цьому рівні майже завжди затьмарюється або відображається аргументом архітектури процесора. Кого хвилює, якщо ви збережете 50 циклів, коли лише витяг аргументів із пам’яті та їх записування забирає понад 100? Такі мікрооптимізації мали сенс, коли пам’ять працювала на (або близькій до) швидкості процесора, але сьогодні не так сильно.
TMN

2
Тому що я втомився бачити, що це 10% цієї цитати, і тому, що вона потрапляє в цвях по голові: "Немає сумнівів, що грааль ефективності призводить до зловживань. Програмісти витрачають величезну кількість часу на роздуми або на занепокоєння. про швидкість некритичних частин їхніх програм, і ці спроби ефективності насправді мають сильний негативний вплив при розгляді налагодження та обслуговування. Слід забути про невелику ефективність, скажімо, про 97% часу: передчасна оптимізація - корінь все зло ...
cHao

25

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


Компілятор розпізнає константи, які мають потужність 2 ...., і перетворює на зрушення. Не всі константи можуть бути змінені в зміни.
quick_now

4
@quickly_now: Вони можуть бути перетворені в комбінації зсувів і додавання / віднімання.
Мехрдад

2
Класична помилка оптимізатора компілятора полягає в перетворенні поділів в правильні зміни, що працює для позитивних дивідендів, але виключається на 1 для негативних.
ddyer

1
@quickly_now Я вважаю, що термін "де можливо" охоплює думку про те, що деякі константи не можуть бути переписані як зрушення.
Фарап

21

Чи буде переміщення швидше, ніж множення, залежить від архітектури вашого процесора. Ще в часи Пентію і раніше переміщення було часто швидше, ніж множення, залежно від кількості 1 біта у вашому мультиплікації. Наприклад, якщо ваш мультиплікація становила 320, це 101000000, два біти.

a *= 320;               // Slower
a = (a<<7) + (a<<9);    // Faster

Але якщо у вас було більше двох біт ...

a *= 324;                        // About same speed
a = (a<<2) + (a<<7) + (a<<9);    // About same speed

a *= 340;                                 // Faster
a = (a<<2) + (a<<4) + (a<<7) + (a<<9);    // Slower

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

a  *= 2;   // Exactly the same speed
a <<= 1;   // Exactly the same speed

a  *= 4;   // Faster
a <<= 2;   // Slower

Зауважте, що це протилежне тому, що було вірно на старих процесорах Intel.

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

a  *= 4;   // 
b  *= 4;   // 

a <<= 2;   // Both lines execute in a single cycle
b <<= 2;   // 

5
+1 "Переміщення швидше, ніж множення, залежить від архітектури вашого процесора." Дякую, що насправді трохи пішли в історію і показали, що більшість комп'ютерних міфів насправді мають певну логічну основу.
Фарап

11

У вас є кілька проблем із тестовою програмою.

По-перше, ви фактично не використовуєте значення test. У рамках стандарту C жодним чином це не testмає значення. Оптимізатор це абсолютно безкоштовно, щоб видалити його. Як тільки його зняли, петля насправді порожня. Єдиний видимий ефект був би встановити runs = 100000000, але runsтакож не використовується. Так оптимізатор може (і повинен!) Видалити всю петлю. Просте виправлення: також надрукуйте обчислене значення. Зауважте, що достатньо визначений оптимізатор все ще може оптимізувати подальше цикл (він повністю покладається на константи, відомі під час компіляції).

По-друге, ви робите дві операції, які скасовують одна одну. Оптимізатору дозволено помітити це та скасувати їх . Знову залишаємо порожню петлю, і знімаємо. Це прямо-важко виправити. Ви можете перейти до unsigned int(тому переповнення не є невизначеною поведінкою), але це, звичайно, призводить до 0. А простих речей (наприклад, скажімо test += 1) досить просто, щоб оптимізатор зрозумів, і це робить.

Нарешті, ви припускаєте, що test *= 2насправді збирається множитися. Це дуже проста оптимізація; якщо бітштифт швидше, оптимізатор використовуватиме його замість цього. Щоб обійти це, вам доведеться використовувати щось на зразок конкретної вбудованої збірки.

Або, я гадаю, просто перевірте свій мікропроцесорний аркуш, щоб побачити, що швидше.

Коли я перевірив збірний результат компіляції вашої програми за gcc -S -O3допомогою версії 4.9, оптимізатор насправді переглядав кожну просту версію, наведену вище, та ще кілька. У всіх випадках це вилучило цикл (призначаючи константу до test), єдине, що залишилося - це виклики clock(), перетворення / віднімання та printf.


1
Зауважте також, що оптимізатор може (і буде) оптимізувати операції на константах (навіть у циклі), як показано в sqrt c # vs sqrt c ++, де оптимізатору вдалося замінити цикл, що підсумовує значення, фактичною сумою. Щоб перемогти цю оптимізацію, вам потрібно використовувати щось визначене під час виконання (наприклад, аргумент командного рядка).

@MichaelT Так. Це те, що я мав на увазі під «зауважте, що достатньо визначений оптимізатор все ще може оптимізувати подальше цикл (він повністю покладається на константи, відомі під час компіляції)».
дероберт

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

@AkshayLAradhya Я не можу сказати, що робить ваш компілятор, але я знову підтвердив, що gcc -O3(зараз 7,3) все ще повністю усуває цикл. (Обов’язково перейдіть на довгий замість int, якщо це потрібно, інакше це оптимізує його у нескінченний цикл через переповнення).
дероберт

8

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

Отриманий в результаті відносний час переміщення та множення не має нічого спільного з 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, спроба використовувати цей еквівалент заради досягнення продуктивності - це, мабуть, марно. Однак, навіть якщо ми змусили використовувати інструкції щодо множення, а потім не побачили різниці в часі виконання, це більше пояснюється характером метрики витрат, яку ми використовували, якщо бути точною, а не тому, що різниці у витратах немає. Кінцевий час виконання - це один показник, і якщо це єдиний, про який ми піклуємося, все добре. Але це не означає, що всі різниці у витратах між множенням та зміщенням просто зникли. І я вважаю, що це, безумовно, не годиться донести цю ідею допитуваному, неявно чи іншим чином, який, очевидно, тільки починає розуміти фактори, пов'язані з часом роботи та вартістю сучасного коду. Інжиніринг завжди стосується компромісів. Запит та пояснення того, які компроміси сучасні процесори домоглися демонструвати час виконання, який ми, як бачимо користувачі, може дати більш диференційований відповідь. І я думаю, що більш диференційована відповідь, ніж "це просто не відповідає дійсності", є обґрунтованою, якщо ми хочемо, щоб менше інженерів перевіряли мікрооптимізований код, що знищує читабельність, оскільки це потребує більш загального розуміння природи таких "оптимізацій", щоб помічайте різні, різноманітні втілення, ніж просто посилання на деякі конкретні випадки як застарілі.


6

Те, що ви бачите, - це ефект оптимізатора.

Завдання оптимізаторів полягає в тому, щоб зробити отриманий складений код або меншим, або швидшим (але рідко обом одночасно ... але, як і багато речей ... ВІДЗВИТАЄ, що таке код).

У ПРИНЦИПІ будь-який виклик до бібліотеки множення або, часто, навіть використання апаратного множника буде повільніше, ніж просто побітове зміщення.

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

Однак оптимізатори є для виявлення шаблонів і з'ясування способів зробити код меншим / швидшим / будь-яким іншим. І те, що ви бачили, - це компілятор, який виявляє, що * 2 - це те саме, що і зсув.

Що стосується інтересу, я щойно дивився на створений асемблер для деяких операцій, таких як * 5 ... насправді не дивлячись на це, а на інші речі, і попутно помічаю, що компілятор перетворив * 5 на:

  • зрушення
  • зрушення
  • додати оригінальний номер

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

Мистецтво оптимізаторів-компіляторів - це ціла окрема тема, наповнена магією і справді правильно зрозуміла близько 6 людей на всій планеті :)


3

Спробуйте встановити його за допомогою:

for (runs = 0; runs < 100000000; runs++) {
      ;
}

Компілятор повинен визнати, що значення testне змінюється після кожної ітерації циклу, а остаточне значення testне використовується, і цілком усуває цикл.


2

Множення - це поєднання зрушень і доповнень.

У випадку, про який ви згадали, я не вважаю, що це має значення, оптимізує це компілятор чи ні - "помножити xна два" можна реалізувати як:

  • Змініть шматочки xодного місця вліво.
  • Додати xв x.

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

Змініть його на "помножити xна чотири", (або будь-яке 2^k, k>1), і це трохи інакше:

  • Зсуньте біти xдвох місць вліво.
  • Додати xдо xі подзвонити y, додати yдо y.

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

Спробуйте останнє (або будь-яке 2^k, k>1) з відповідними варіантами, щоб запобігти оптимізації їх як одне і те ж саме в реалізації. Ви повинні знайти зсув швидше, приймаючи O(1)порівняно з повторним додаванням в O(k).

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


1
Що таке "основна атомна операція"? Хіба не можна стверджувати, що за зміну операція може бути застосована до кожного біта паралельно, тоді як, крім того, крайній лівий біт залежить від інших бітів?
Бергі

2
@ Bergi: Я здогадуюсь, що він означає, що і shift, і add - це одна інструкція з машини. Вам доведеться переглянути документацію з набору інструкцій, щоб побачити кількість циклів для кожного, але так, додавання часто є операцією багато циклу, тоді як зсув зазвичай виконується в одному циклі.
TMN

Так, це може бути так, але множення - це також одна машинна інструкція (хоча, звичайно, може знадобитися більше циклів)
Бергі,

@ Бергі, це теж залежить від арки. Яку арку ви думаєте про те, що цей зсув менший цикл, ніж 32-бітове додавання (або x-біт, якщо це доречно)?
OJFord

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

1

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

Слід зазначити, однак, що поділ потенційно негативних підписаних значень не рівнозначно зміщенню вправо. Вираз на зразок (x+8)>>4не рівносильний (x+8)/16. Перший, у 99% компіляторів, буде відображати значення від -24 до -9 до -1, -8 до +7 до 0 та +8 до +23 до 1 [округлення чисел майже симетрично приблизно до нуля]. Останній відобразить карту від -39 до -24 до -1, -23 до +7 до 0 і +8 до +23 до +1 [грубо несиметрично, і, швидше за все, не за призначенням]. Зауважте, що навіть коли значення не очікуються негативними, використання >>4коду, швидше за все, дасть швидший код, ніж /16якщо компілятор не зможе довести, що значення не можуть бути негативними.


0

Ще трохи інформації я щойно перевірив.

На x86_64 оппод MUL має затримку 10 циклів та пропускну здатність 1/2 циклу. MOV, ADD і SHL мають затримку в 1 цикл з прохідністю 2,5, 2,5 та 1,7 циклу.

Для множення на 15 потрібно мінімум 3 SHL та 3 ADD, і, можливо, пара MOV.

https://gmplib.org/~tege/x86-timing.pdf


0

Ваша методологія є вадою. Сама приріст циклу та перевірка стану займає стільки часу.

  • Спробуйте запустити порожній цикл і виміряти час (називайте його base).
  • Тепер додайте операцію 1 зміни та виміряйте час (називайте її s1).
  • Далі додайте 10 операцій зміни та вимірюйте час (називайте його s2)

Якщо все виходить правильно, base-s2має бути в 10 разів більше, ніж base-s1. Інакше тут грає щось інше.

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

int main(){

    int test = 2;
    clock_t launch = clock();

    test << 6;
    test << 6;
    test << 6;
    test << 6;
    //.... 1 million times
    test << 6;

    clock_t done = clock();
    printf("Time taken : %d\n", done - launch);
    return 0;
}

І там ви маєте свій результат

1 мільйон операцій зсуву за менше 1 мілісекунди? .

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

Результат роботи оператора Shiftwise

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.