Це як реалізовано оператор + в C?


79

При розумінні того, як примітивні оператори , такі як +, -, *і /реалізуються в C, я знайшов такий уривок із листа цікавого відповіді .

Здається, ця функція демонструє, як +насправді працює у фоновому режимі. Однак для мене це занадто заплутано, щоб зрозуміти це. Я вважав, що такі операції виконуються з використанням директив збірки, створених компілятором протягом тривалого часу!

Чи +реалізований оператор як код, розміщений на реалізаціях MOST ? Чи використовує це перевага доповнення двох або інших функцій, що залежать від реалізації?


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

3
Так, +оператор, швидше за все, використовує переваги визначених реалізацією функцій. Вони називаються "машинна мова" та "процесор". Яке ваше запитання? Якщо ви хочете знати, як вирази перетворюються на машинний код, прочитайте про побудову компілятора.
занадто чесний для цього сайту

Ви маєте на увазі, що код, який я опублікував, абсолютно марний, тому +що він переводиться до директив збірки, таких як addкомпілятор?
nalzok

11
Більшість +операцій буде зведено до якогось варіанту (або комбінації)add інструкцій машинного коду . Ваш код заплутаний і марний у будь-якому реальному сценарії, але він може слугувати для навчання бінарним операціям.
Андерс Торнблад

11
Хоча це не так, як це робить C (див. Відповіді нижче), але це досить близько до того, як задіяні схеми можуть додавати на найнижчому рівні. Спробуйте обробити це на папері та в ручці для невеликих двійкових значень (скажімо, 3- або 4-бітових байтів) і подивіться, як це працює. А тепер уявіть, як схема може зробити те саме з електричними імпульсами. А тепер уявіть, що ви робите всі біти паралельно, а не цикл. Тепер ви готові побудувати комп’ютер епохи 1940-х: D
Джон Ханна

Відповіді:


184

Щоб бути педантичним, специфікація C не визначає, як реалізується додавання.

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

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


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

5
@orlp насправді, логічні схеми процесора можуть бути скомпільовані з HDL, і ви, швидше за все, згенеруєте суматор, використовуючи цикли та бітові зсуви, неясно схожі на пропозицію OP (але лише неясно). Зазначені шлейфи та бітові зрушення описують компонування апаратного забезпечення та спосіб їх з'єднання. Знову ж таки, на апаратному забезпеченні верхнього рівня хтось може розгортати цикли та бітові зсуви, або навіть покінчити з HDL і вручну розкласти схему для чогось настільки важливого для продуктивності, як суматор.
Якк - Адам Неврамонт

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

2
@usr не просто розгорнуто, а кожен "крок" відбувається одночасно.
OrangeDog

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

77

Коли ви додаєте два біти, виходить такий результат: (таблиця правди)

Отже, якщо ви зробите побітове xor, ви можете отримати суму без перенесення. І якщо ви зробите побітове, і ви можете отримати біти для перенесення.

Розширення цього спостереження для багаторозрядних чисел aіb

Одного разу bє 0:

Отже, алгоритм зводиться до:

Якщо ви позбудетеся рекурсії і перетворите її в цикл


З урахуванням вищезазначеного алгоритму пояснення з коду має бути простішим:

Носіть шматочки. Біт перенесення - 1, якщо 1 біт праворуч в обох операндах - 1.

Додавання без перенесення (біти для перенесення ігноруються)

Повторно використовуйте x, щоб встановити його на перенесення

Повторюйте, поки є більше бітів перенесення


Рекурсивною реалізацією (легшою для розуміння) буде:

Здається, ця функція демонструє, як + насправді працює у фоновому режимі

Ні. Зазвичай (майже завжди) ціле додавання перекладається на додавання машинних інструкцій. Це просто демонструє альтернативну реалізацію з використанням побітових xor та та.


5
Це найкраща відповідь imo, всі інші стверджують, що це зазвичай перекладається в одну інструкцію, але це робить це, а також пояснює задану функцію.
Нік Світінг

@NickSweeting Дякую. Питання може бути витлумачено двояко, і, на мою думку, прийнята відповідь інтерпретувала його правильно, як те, що хотів поставити ОП.
Мохіт Джейн

25

Здається, ця функція демонструє, як + насправді працює у фоновому режимі

Ні. Це перекладено на рідну addмашинну інструкцію, яка фактично використовує апаратний суматор, уALU .

Якщо вам цікаво, як додається комп’ютер, ось основний суматор.

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

Базовий підручник з логічних входів та додавачів див. У цьому . Відео надзвичайно корисне, хоч і довге.

У цьому відео показано основний напівсуматор. Якщо вам потрібен короткий опис, ось він:

Половинний суматор додає два біти. Можливі комбінації:

  • Додайте 0 і 0 = 0
  • Додайте 1 і 0 = 1
  • Додайте 1 і 1 = 10 (двійкові)

Отже, як працює напівсуматор? Ну, він складається з трьох логічних входів and, xorі nand. nand Дає позитивний струм , якщо обидва входи є негативними, так що це означає , що це вирішує випадку 0 і 0. xorдає позитивний вихід один на вході позитивний, а інший негативний, так що це означає , що вона вирішує проблему 1 і 0.and дає позитивний результат лише в тому випадку, якщо обидва входи позитивні, тож це вирішує проблему 1 і 1. Отже, в основному, ми отримали наш напівсуматор. Але ми все ще можемо додавати лише біти.

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

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

Здивований? Так відбувається насправді. Це виглядає як тривалий процес, але комп’ютер робить це за частки наносекунди, а якщо бути більш точним, то за півгодинного циклу. Іноді це виконується навіть за один такт. В основному комп'ютер має ALU(основну частину CPU), пам'ять, шини тощо.

Якщо ви хочете вивчити апаратне забезпечення комп’ютера, з логічних входів, пам’яті та ALU, і змоделювати комп’ютер, ви можете побачити цей курс, з якого я все це навчився: Створення сучасного комп’ютера з перших принципів

Безкоштовно, якщо ви не хочете отримати електронний сертифікат. Друга частина курсу вийде навесні цього року


11
Кілька мілісекунд? Для одного додавання?
JAB

2
Додавання двох зареєстрованих значень, як правило, здійснюється за один годинник.
Коді Грей

5
@Tamoghna Chowdhury: Спробуйте деякі частки наносекунди. Зареєструвати додаток - це IIRC на одному годиннику на останніх процесорах Intel, так що з тактовою частотою кілька ГГц ... І це не враховуючи конвеєризацію, суперскалярне виконання тощо.
jamesqf

Цей додаток для перенесення пульсацій додав би занадто велику затримку, тому він навіть не реалізований таким чином в апаратному забезпеченні.
труба

Суматор для пульсацій не використовується процесорами протягом десятиліть, оскільки він занадто повільний. Натомість вони використовують більш складні суматори, які можуть виконувати роботу за один такт (або навіть півциклу, у випадку з двома тактовими АЛУ Intel). (Ну, більшість процесорів не використовують його. Вбудовані центральні процесори низького класу все ще можуть використовувати його для низького числа транзисторів.)
Позначте

15

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

Але в більшості реалізацій C +між двома цілими числами, меншими за розмір машинного цілого числа, буде перетворено в інструкцію збірки (після багатьох кроків). Інструкція з монтажу буде перекладена в машинний код і вбудована у ваш виконуваний файл. Асамблея - це мова, "вилучена на один крок" із машинного коду, призначена для легшого читання, ніж купа запакованих двійкових файлів.

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

Арифметичний логічний блок може мати окремі суматори та множники або мішати їх між собою.

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

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

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

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

Ось публікація SO про 8-розрядний суматор. Ось публікація , що не відповідає SO, де ви зауважите, що деякі суматори просто використовуються operator+в HDL! (HDL сам розуміє +і генерує код суматора нижчого рівня для вас).


14

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

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

Вираз x & y(побітове І) дає біти, загальні для x та y. Вираз x ^ y(побітове виключення АБО) дає біти, які є унікальними для одного з x або y.

Суму x + yможна переписати як суму двох разів спільних бітів (оскільки і x, і y вносять ці біти) плюс біти, унікальні для x або y.

(x & y) << 1 вдвічі перевищує загальну кількість бітів (лівий зсув на 1 фактично помножується на два).

x ^ y - це біти, які є унікальними для одного з x або y.

Отже, якщо замінити x першим значенням, а y другим, сума повинна бути незмінною. Ви можете думати про перше значення як про перенесення побітових додавань, а про друге як про біт нижчого порядку побітових додавань.

Цей процес триває до тих пір, поки x не дорівнює нулю, в цей момент y тримає суму.


14

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

У звичайному житті ви використовуєте десяткові числа і навчилися їх додавати: Щоб додати два числа, ви додаєте найменші дві цифри. Якщо результат менше 10, ви записуєте результат і переходите до наступної цифри. Якщо результат 10 або більше, ви записуєте результат мінус 10, переходите до наступної цифри, купуючи пам'ятаєте, додати ще 1. Наприклад: 23 + 37, ви додаєте 3 + 7 = 10, ви записуєте 0 і пам’ятаєте додати ще 1 для наступної позиції. У позиції 10 секунд ви додаєте (2 + 3) + 1 = 6 і записуєте це. Результат - 60.

Ви можете зробити те саме те саме з двійковими числами. Різниця полягає в тому, що єдиними цифрами є 0 і 1, тому єдино можливими сумами є 0, 1, 2. Для 32-бітового числа ви б обробляли одну цифру позиції за іншою. І саме так це могло б зробити справді примітивне комп'ютерне обладнання.

Цей код працює по-різному. Ви знаєте, що сума двох двійкових цифр дорівнює 2, якщо обидві цифри дорівнюють 1. Отже, якщо обидві цифри дорівнюють 1, тоді ви додасте ще 1 на наступну двійкову позицію і запишете 0. Ось що робить обчислення t: Він знаходить усі місця де обидві двійкові цифри дорівнюють 1 (це &) і переміщує їх у наступну цифру (<< 1). Потім виконується додавання: 0 + 0 = 0, 0 + 1 = 1, 1 + 0 = 1, 1 + 1 дорівнює 2, але ми записуємо 0. Це те, що робить ексклюзив або оператор.

Але всі одиниці, які вам довелося обробити в наступній цифрі, не оброблено. Їх ще потрібно додати. Ось чому код робить цикл: У наступній ітерації додаються всі зайві одиниці.

Чому жоден процесор не робить це так? Тому що це цикл, і процесори не люблять цикли, і це повільно. Це повільно, оскільки в гіршому випадку потрібно 32 ітерації: Якщо до числа 0xffffffff (32 1-біта) додати 1, то перша ітерація очищає біт 0 з y і встановлює x на 2. Друга ітерація очищає біт 1 з y і встановлює x на 4. І так далі. Щоб отримати результат, потрібно 32 ітерації. Однак кожна ітерація повинна обробляти всі біти x і y, для чого потрібно багато обладнання.

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

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

64-розрядний суматор складається з двох частин: По-перше, 32-розрядний суматор для найнижчого 32 біта. Цей 32-бітний суматор видає суму та "перенесення" (показник того, що 1 потрібно додати до наступної бітової позиції). По-друге, два 32-бітових суматора для вищих 32 бітів: Один додає x + y, інший додає x + y + 1. Всі три суматори працюють паралельно. Потім, коли перший суматор здійснив перенесення, центральний процесор просто вибирає, який з двох результатів x + y або x + y + 1 є правильним, і ви отримаєте повний результат. Отже, 64-розрядний суматор займає лише трохи більше часу, ніж 32-розрядний суматор, а не вдвічі довше.

32-бітові частини суматора знову реалізовані як умовні суматори суми, використовуючи безліч 16-бітових суматорів, а 16-бітові суматори - умовні суматори суми тощо.


13

Моє запитання: Чи реалізований оператор + як код, розміщений у реалізаціях MOST?

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

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

Простий приклад:

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

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

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


11

Якщо збій коду допомагає комусь іншому, візьміть приклад x=2, y=6:


xне дорівнює нулю, тому почніть додавати до y:

x & y = 2 оскільки

2 <<1 = 4тому що << 1зміщує всі біти вліво:

Підсумовуючи, сховайте цей результат 4, в, tз

Тепер застосуйте побітове XOR y^=x :

Отже x=2, y=4. Нарешті, підсумуйте t+y, скинувши x=tі повернувшись до початку whileциклу:

Коли t=0(або, на початку циклу, x=0), закінчуємо з


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

11

Просто з інтересу на процесорі Atmega328P із компілятором avr-g ++ наступний код реалізує додавання одного шляхом віднімання -1:

volatile char x;
int main ()
  {
  x = x + 1;  
  }

Створений код:

00000090 <main>:
volatile char x;
int main ()
  {
  x = x + 1;  
  90:   80 91 00 01     lds r24, 0x0100
  94:   8f 5f           subi    r24, 0xFF   ; 255
  96:   80 93 00 01     sts 0x0100, r24
  }
  9a:   80 e0           ldi r24, 0x00   ; 0
  9c:   90 e0           ldi r25, 0x00   ; 0
  9e:   08 95           ret

Зауважте, зокрема, що додавання виконується subi інструкцією (віднімання константи з реєстру), де 0xFF в цьому випадку фактично дорівнює -1.

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

Чи використовує це перевага доповнення двох або інших функцій, що залежать від реалізації?

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

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