Это как оператор + реализован в C?

Когда мы поняли, как примитивные операторы, такие как +, -, * и /, реализованы в C, я нашел следующий фрагмент из интересного ответа.

// replaces the + operator
int add(int x, int y) {
    while(x) {
        int t = (x & y) <<1;
        y ^= x;
        x = t;
    }
    return y;
}

Похоже, эта функция демонстрирует, как на самом деле работает + в фоновом режиме. Однако, это слишком запутанно для меня, чтобы понять это. Я считал, что такие операции выполняются с использованием директив сборки, сгенерированных компилятором в течение длительного времени!

Реализован ли оператор + в виде кода, размещенного в MOST реализациях? Использует ли это два дополнения или другие зависящие от реализации функции?

Ответ 1

Чтобы быть педантичным, спецификация C не определяет, как выполняется добавление.

Но чтобы быть реалистичным, оператор + на целочисленных типах, меньших или равных размеру слова вашего процессора, напрямую преобразуется в инструкцию добавления для CPU, а более крупные целые типы переводятся в несколько инструкций добавления с некоторыми дополнительные биты для обработки переполнения.

ЦП внутренне использует логические схемы для реализации добавления и не использует циклы, бит-сдвиги или что-либо, что имеет близкое сходство с тем, как работает C.

Ответ 2

Когда вы добавляете два бита, следующий результат: (таблица истинности)

a | b | sum (a^b) | carry bit (a&b) (goes to next)
--+---+-----------+--------------------------------
0 | 0 |    0      | 0
0 | 1 |    1      | 0
1 | 0 |    1      | 0
1 | 1 |    0      | 1

Итак, если вы побито xor, вы можете получить сумму без переноса. И если вы побитовы, и вы можете получить бит переноса.

Расширение этого наблюдения для многобайтовых чисел a и b

a+b = sum_without_carry(a, b) + carry_bits(a, b) shifted by 1 bit left
    = a^b + ((a&b) << 1)

Как только b есть 0:

a+0 = a

Таким образом, алгоритм сводится к следующему:

Add(a, b)
  if b == 0
    return a;
  else
    carry_bits = a & b;
    sum_bits = a ^ b;
    return Add(sum_bits, carry_bits << 1);

Если вы избавитесь от рекурсии и преобразуете ее в цикл

Add(a, b)
  while(b != 0) {
    carry_bits = a & b;
    sum_bits = a ^ b;

    a = sum_bits;
    b = carrry_bits << 1;  // In next loop, add carry bits to a
  }
  return a;

С приведенным выше алгоритмом в виду объяснение из кода должно быть проще:

int t = (x & y) << 1;

Переверните бит. Бит переноса - 1, если 1 бит вправо в обоих операндах равен 1.

y ^= x;  // x is used now

Дополнение без переноса (игнорируются несущие биты)

x = t;

Повторно используйте x, чтобы установить его для переноса

while(x)

Повторите, если есть больше бит переноса


Рекурсивная реализация (проще понять):

int add(int x, int y) {
    return (y == 0) ? x : add(x ^ y, (x&y) << 1);
}

Кажется, что эта функция демонстрирует, как + фактически работает в фон

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

Ответ 3

Кажется, что эта функция демонстрирует, как + фактически работает в фоновом режиме

Нет. Это переводится в командную инструкцию native add, которая на самом деле использует аппаратный сумматор в ALU.

Если вам интересно, как добавить компьютер, вот основной сумматор.

Все в компьютере выполняется с помощью логических ворот, которые в основном состоят из транзисторов. Полный сумматор имеет в нем полукомпенсаторы.

Для основного учебника по логическим затворам и сумматорам см. this. Видео очень полезно, хотя и долго.

В этом видео показан базовый сумматор. Если вам нужно краткое описание, вот оно:

Полуядер добавляет два бита. Возможные комбинации:

  • Добавить 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, а также имитировать компьютер, вы можете увидеть этот курс, из которого я узнал все это: Создайте современный компьютер из первых принципов

Это бесплатно, если вам не нужен электронный сертификат. Вторая часть курса идет в spring в этом году

Ответ 4

C использует абстрактную машину для описания кода C. Так как это работает, не указано. Существуют компиляторы C, которые фактически компилируют C на язык сценариев, например.

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

Этот машинный код (после многих шагов) затем интерпретируется целевой аппаратной платформой, где он интерпретируется декодером команд на CPU. Этот декодер команд принимает инструкцию и преобразует ее в сигналы для отправки по "линиям управления". Эти сигналы передают данные из регистров и памяти через центральный процессор, где значения часто складываются в арифметическом логическом блоке.

Арифметическая логическая единица может иметь отдельные сумматоры и множители или может смешивать их вместе.

Арифметический логический блок имеет связку транзисторов, которые выполняют операцию сложения, а затем выводят вывод. Указанный выход направляется через сигналы, генерируемые декодером команд, и сохраняется в памяти или регистры.

Схема упомянутых транзисторов как в арифметическом логическом блоке, так и в декодере команд (а также в деталях, которые я затушевывает) выгравирована в чипе на заводе. Шаблон травления часто создается путем компиляции языка описания аппаратных средств, который берет абстракцию того, что связано с тем, что и как они работают, и генерирует транзисторы и линии связи.

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

Вышеупомянутые блески по многим частям и слоям и содержат неточности. Это как из-за моей собственной некомпетентности (я написал как аппаратные, так и компиляторы, но я не эксперт ни в одном) и потому, что полная информация займет карьеру или две, а не SO-сообщение.

Здесь - это сообщение о 8-разрядном сумматоре. Здесь - это сообщение non-SO, где вы заметите, что некоторые из сумматоров просто используют operator+ в HDL! (Сам HDL понимает + и генерирует для вас код сумматора нижнего уровня).

Ответ 5

Почти любой современный процессор, который может запускать скомпилированный C-код, будет иметь встроенную поддержку для целочисленного добавления. Код, который вы опубликовали, - это умный способ выполнения целочисленного добавления без выполнения целого кода ввода кода операции, но не так, как обычно выполняется целочисленное добавление. Фактически, функция linkage, вероятно, использует некоторую форму целочисленного добавления для настройки указателя стека.

Приведенный вами код основывается на наблюдении, что при добавлении 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 будет содержать сумму.

Ответ 6

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

В обычной жизни вы используете десятичные числа, и вы узнали, как их добавить: чтобы добавить два числа, вы добавляете самые низкие две цифры. Если результат меньше 10, вы записываете результат и переходите к следующей позиции цифры. Если результат равен 10 или больше, вы записываете результат минус 10, переходите к следующей цифре, купите, помните, чтобы добавить еще 1. Например: 23 + 37, вы добавляете 3 + 7 = 10, вы записываете 0 и не забудьте добавить еще 1 для следующей позиции. В позиции 10s вы добавляете (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. То, что делает excludive или operator.

Но все 1, которые вы должны были обрабатывать в следующей позиции цифр, не были обработаны. Их еще нужно добавить. Вот почему код выполняет цикл: на следующей итерации добавляется все добавочное 1.

Почему процессор не делает этого? Потому что это цикл, а процессоры не любят циклы, и он медленный. Он медленный, потому что в худшем случае необходимы 32 итерации: если вы добавите 1 к числу 0xffffffff (32 1 бит), тогда первая итерация очистит бит 0 у y и устанавливает x в 2. Вторая итерация очищает бит 1 y и устанавливает x в 4. И так далее. Для получения результата потребуется 32 итерации. Однако каждая итерация должна обрабатывать все биты x и y, что требует много аппаратного обеспечения.

Примитивный процессор будет делать так же быстро, как вы делаете десятичную арифметику, от самого низкого положения до самого высокого. Он также занимает 32 шага, но каждый шаг обрабатывает только два бита плюс одно значение из предыдущей позиции бита, поэтому его намного проще реализовать. И даже на примитивном компьютере можно позволить себе это сделать, не выполняя циклов.

Современный, быстрый и сложный CPU будет использовать "условный сумматор". Особенно, если количество бит велико, например 64-разрядный сумматор, это экономит много времени.

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

Детали 32-разрядных сумматоров снова реализованы как сумматоры условных сумм, с использованием нескольких 16-разрядных сумматоров, а 16-разрядные сумматоры - это сумматоры условных сумм и т.д.

Ответ 7

Мой вопрос: Является ли оператор + реализован как код, размещенный в реализациях MOST?

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

Компилятор может свободно генерировать любой код, пока он ведет себя так, как если бы фактические операции выполнялись в соответствии со стандартом. Но то, что на самом деле происходит, может быть совершенно иным.

Простой пример:

static int
foo(int a, int b)
{
    return a + b;
}
[...]
    int a = foo(1, 17);
    int b = foo(x, x);
    some_other_function(a, b);

Здесь нет необходимости создавать какие-либо инструкции по добавлению. Это совершенно законно для компилятора, чтобы перевести это на:

some_other_function(18, x * 2);

Или, может быть, компилятор замечает, что вы вызываете функцию foo несколько раз подряд и что это простая арифметика, и она будет генерировать векторные инструкции для нее. Или, что результат добавления используется для индексации массива позже, и будет использоваться инструкция lea.

Вы просто не можете говорить о том, как выполняется оператор, потому что он почти никогда не используется один.

Ответ 8

В случае, если разбивка кода помогает кому-либо еще, возьмите пример x=2, y=6:


x не равно нулю, поэтому начните добавлять к y:

while(2) {

x & y = 2, потому что

        x: 0 0 1 0  //2
        y: 0 1 1 0  //6
      x&y: 0 0 1 0  //2

2 <<1 = 4, потому что << 1 сдвигает все биты влево:

      x&y: 0 0 1 0  //2
(x&y) <<1: 0 1 0 0  //4

Итак, запишите этот результат, 4, в t с помощью

int t = (x & y) <<1;

Теперь примените побитовый XOR y^=x:

        x: 0 0 1 0  //2
        y: 0 1 1 0  //6
     y^=x: 0 1 0 0  //4

So x=2, y=4. Наконец, суммируем t+y путем сброса x=t и возврата в начало цикла while:

x = t;

Когда t=0 (или, в начале цикла, x=0), закончите с

return y;

Ответ 9

Просто из интереса, на процессоре 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, что подразумевает, что дизайнеры полагали, что выполнение вычитания дополнения будет надлежащим образом обработано компилятором-сценаристами.

Использует ли это преимущества двух дополнений или других функций, зависящих от реализации?

Вероятно, было бы справедливо сказать, что авторы-компиляторы попытаются реализовать желаемый эффект (добавив один номер в другой) наиболее эффективным способом для этой, особенно архитектуры. Если это требует вычитания дополнения, пусть так и будет.