Как компьютер вычитает числа

Обновлено: 05.07.2024

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

Я понимаю, что A будет иметь знаковый бит (MSB) как 0 для обозначения положительного значения, а B будет иметь знаковый бит как 1 для обозначения отрицательного целого числа.

Теперь, когда в программе C++ я хочу напечатать A + B , модуль сложения ALU (Арифметическая логическая единица) сначала проверяет бит знака, а затем решает выполнить вычитание и затем следовать процедуре вычитания. Как вычитание будет моим следующим вопросом.

Я хочу сделать A - B Компьютер возьмет 2 дополнения B и добавит A + 2 дополнения B и вернет это (после сброса лишнего бита слева)?

делать A - B Как компьютер делает в этом случае?

Таким образом, чтобы найти A-B , мы можем просто отрицать B и добавить; то есть, мы находим A + (-B) , и, поскольку мы используем дополнение 2, мы не беспокоимся, если (-B) является положительным или отрицательным, потому что алгоритм сложения работает одинаково в любом случае.

Компилятор при компиляции в двоичный файл сделает это:

Теперь, когда он хочет добавить 5, АЛУ получает 2 числа и добавляет их, простое дополнение.

Здесь необходимо помнить, что 2 дополнения были выбраны именно для того, чтобы не делать 2 отдельных процедуры для 2+ 3 и 2+ (-3).

Подумайте в терминах двух или трех бит, а затем поймите, что эти вещи масштабируются до 32 или 64 или любых других бит.

Сначала начнем с десятичной

Поскольку вы использовали 5 и 2, вы можете сделать 4 битную двоичную математику

Нам не нужно было переносить этот, но вы можете видеть, что работала математика, 5 + 2 = 7.

И если мы хотим добавить 5 и -2

И ответ будет 3, как ожидалось, не удивительно, но у нас было выполнение. И так как это было добавление с минус-цифрой в двух дополнениях, все это сработало, не было бита знака, а затем добавлено два дополнения, поэтому нам не нужно просто кормить сумку двумя операндами.

Теперь, если вы хотите сделать небольшую разницу, что, если вы хотите вычесть 2 из 5, вы выбираете команду вычитания, а не добавляете. Ну, мы все узнали, что отрицание в двойном дополнении означает инвертирование и добавление одного. И мы увидели выше, что двум входным сумматорам действительно нужен третий вход для переноса, чтобы он мог быть каскадным, насколько бы широким не был сумматор. Поэтому вместо того, чтобы делать две операции добавления, инвертируйте и добавьте 1, являясь первым добавлением реального добавления, все, что нам нужно сделать, это инвертировать и установить перенос:

Поймите, что нет логики вычитания, она добавляет отрицательный результат того, что вы его кормите.

Поймите, что логика добавления SAME используется для подписанной и неподписанной математики, это зависит от вашего кода, а не от логики, чтобы фактически определить, считались ли эти биты двумя дополнениями или без знака.

если/когда у вас есть команда add with carry, они принимают схему SAME с суммированием, и вместо того, чтобы подавать перенос в ноль, он получает флаг переноса. Аналогично, вычитание с заимствованием вместо того, чтобы подавать перенос в 1, имеет значение 1 или 0 на основании состояния флага переноса в регистре состояния.

В записи 2-го дополнения: не B = -B -1 или -B = (не B) + 1. Его можно проверить на компьютере или на бумаге.

Там есть трюк, чтобы неэффективно увеличивать и уменьшать, используя только nots и отрицания.
Например, если вы начинаете с номера 0 в регистре и выполняете:

Или как еще две формулы:

проверяет ли модуль сложения ALU (Arithmetic Logic Unit) сначала бит знака, а затем решает выполнить вычитание, а затем следует процедуре вычитания

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

Таким образом, ALU в основном выполняет A + (-B) для A - B , но ему не требуется отдельный шаг отрицания. Проектировщики использовать умный трюк, чтобы сделать оба сумматоры add и к sub в той же длины цикла, добавляя только мультиплексор и НЕ ворота вместе с новым входом Binvert для того, чтобы условно инвертировать второй вход. Вот простой пример ALU, который может делать AND/OR/ADD/SUB

b и переносит, производя сумму и результат. Это работает, понимая, что в двух дополнениях -B =

b + 1 , так что a - b = a +

b + 1 . Это означает, что нам просто нужно установить перенос в 1 (или отменить перенос для заимствования) и инвертировать второй вход (т.е. B). Этот тип ALU можно найти в различных книгах по компьютерной архитектуре, таких как

В одном дополнении -B =

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

С некоторой простой простой модификацией вышеупомянутого ALU они теперь могут делать 6 различных операций: ADD, SUB, SLT, AND, OR, NOR

Множественные операции -B it выполняются путем объединения нескольких отдельных -B it ALU, указанных выше. На самом деле ALU способны выполнять гораздо больше операций, но они созданы для экономии места по аналогичному принципу.

Принцип представления беззнаковых целых чисел в компьютере достаточно прост:

  • число должно быть переведено в двоичную систему счисления;
  • должен быть определен объем памяти для этого числа.
  • .byte - размещает каждое выражение как 1 байт
  • .short - 2 байта
  • .long - 4 байта
  • .quad - 8 байт

Если речь идет о переменной, а чаще так и бывает, необходимо определить диапазон, в котором будет меняться значение переменной и, исходя из этого, резервировать для нее память. Поскольку современные процессоры Intel ориентированы на операции с 64-битными числами то, оптимальнее, все же ориентироваться на переменные такой же размерности.

Рассмотрим программу на языке C ( 700.c ).

В данном программе задано четыре переменных: однобайтовая e , двухбайтовая c , четырехбайтовая t , восьмибайтовая a . С помощью этой программы, к выведем область памяти, где хранятся эти переменные. Вот дамп памяти, который выдает программа ( рисунок 1 ).

Внимательно посмотрев на данную цепочку байтов, вы без труда обнаружите все наши переменные. Что важного можно почерпнуть из данной последовательности?

  • Мы видим, что младшие байты чисел (переменных) в слове занимают в памяти младшие адреса. В свою очередь младшие слова в удвоенном слове — младший адрес. И, наконец, если рассматривать 64-битную переменную, то в ней младшее удвоенное слово должно занимать младший адрес. Это очень важный момент именно для анализа двоичного кода. В дальнейшем по одному виду области памяти вы сможете во многих случаях сразу идентифицировать переменные.
  • Как видно на все переменные затрачивается объем памяти, кратный четырехбайтовой величине. После каждой инициализированной переменной компилятор вставляет директиву выравнивания по 32-битной границе (Align 4). Впрочем, все совсем не так просто, и при другом порядке следования переменных выравнивание может быть иным.

И так. 16-битное число 0xА890 в памяти будет храниться как последовательность байтов 90 A8, 32-битное число 0x67896512 как последовательность 12 65 89 67. И, наконец, 64-битное число 0xF5C68990D1327650 — как 50 76 32 D1 90 89 C6 F5.

Числа со знаком в компьютере

Поскольку в памяти нет ничего, кроме двоичных разрядов, то вполне логично было бы выделить для знака числа отдельный бит. Например, имея одну ячейку, мы могли бы получить диапазон чисел от –127 до 127 ( 11111111 - 01111111 ). Подход был бы не так уж и плох. Вот только пришлось бы вводить отдельно сложение для знаковых и беззнаковых чисел. Существует и другой, альтернативный способ введения знаковых чисел. Алгоритм построения заключается в том, что мы объявляем некоторое число заведомо положительным и далее ищем для него противоположное по знаку исходя из очевидно тождества: a + (– a) = 0 .

На множестве однобайтовых чисел за единицу естественно взять двоичное число 00000001 . Решая уравнение 00000001 + x = 00000000 , мы приходим к неожиданному, на первый взгляд, результату x = 11111111, другими словами за -1 мы должны принять число 11111111 ( 255 в десятичном эквиваленте и FF в шестнадцатеричном). Попробуем развить нашу теорию. Очевидно, что -1 - (1) = -2 , т. е. по логике вещей, за -2 мы должны принять число 11111110 . Но с другой стороны число 00000010 вроде бы должно представлять +2 . Посмотрите 11111110 + 00000010 = 00000000 , т. е. выполняется очевидное тождество +2 + (-2) = 0 . Итак, мы на верном пути и процесс можно продолжить (см. Рисунок 2 ).

Внимательно посмотрите на таблицу (рисунок 2). Что же у нас получилось? Знаковые числа оказываются в промежутке -128 до 127 .

Таким образом, однобайтовое число можно интерпретировать и как число со знаком, и как беззнаковое число. Тогда, например, 11111111 в первом случае будет считаться -1 , а во втором случае 255 . Все зависит от нашей интерпретации. Еще интереснее операции сложения и вычитания. Эти операции будут выполняться по одной и той же схеме и для знаковых и для беззнаковых чисел. По этой причине и для операции сложения и для операции вычитания у процессора имеется всего по одной команде: add и sub . Разумеется, при выполнении действия может возникнуть переполнение или перенос в несуществующий разряд, но это отдельный разговор, и решить проблему можно, зарезервировав еще одну ячейку памяти. Все наши рассуждения легко переносятся на двух- четырех- и восьмибайтовые числа. Так максимальное двухбайтовое беззнаковое число будет 65 535 , а знаковые числа окажутся в промежутке от - 32 768 до 32 767 . Еще один интересный момент касается старшего бита. Как мы видим, по нему можно определить знак. Но в данной схеме бит совсем не изолирован и участвует в формировании значения числа вместе с остальными битами.

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

cmp rax, 0xFFFFFFFFFFFFFFFE

следует иметь в виду, что в действительности это, возможно, команда

Рассмотрим последовательность переменных:

signed char e=-2;
short int c=-3;
int b=-4;
__int64_t a=-5;

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

fe00 00 00 fd ff 00 00 fc ff ff ff 00 00 00 00 fb ff ff ff ff ff ff ff

Итак, значение однобайтовой переменной -2 в памяти компьютера представлено байтом 0xFE , значение двухбайтовой переменной -3, представлено последовательностью 0xFFFD , значение четырехбайтовой переменной -4 — последовательностью 0xFFFFFFFC , и, наконец, отрицательное восьмибайтовая переменная со значением -5 представлена байтами 0xFFFFFFFFFFFFFFFB . Напоминаю, что при представлении восьмибайтового числа младшие четыре байта должны находиться по адресу, меньшему, чем старшие.

Вещественные числа

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

Здесь ZN - знак числа, M - мантисса числа, обычно удовлетворяет условию M < 1 , N - основание системы счисления, q показатель, в общем случае может быть и положительным и отрицательным числом. Числа, представленные таким образом, называют еще числами с плавающей точкой (или числами с плавающей запятой ).

Рассмотрим конкретный пример. Попытаемся представить в нормализованном виде число 5,75 . Переведем вначале это число в двоичную систему счисления. В данном случае это делается достаточно легко. Действительно, 5 — это 101 , а 0,75 - это (1/2) + (1/4) . Другими словами 5,75 = 0b101,11 . Пишем далее 0b101.11 = 1.00111 * (2^3) . Таким образом, мы имеем следующие компоненты нормализованного числа:

Заметим, что первая цифра мантиссы в таком представлении всегда равна 1, а, следовательно, ее можно и вообще не хранить, и в формате Intel так и поступают. Кроме этого нужно иметь в виду, что показатель q в действительности (для процессора Intel) хранится в памяти в виде суммы с некоторым числом, так чтобы всегда быть положительным. Процессор Intelможет работать с тремя типами вещественных чисел:

  • короткое вещественное число . Всего для хранения отводиться 32 бита. Биты 0-22 резервируются для мантиссы. Биты 23-30 предназначены для хранения показателя q, сложенного с числом 127 . Последний 31 -й бит, предназначен для хранения знака числа ( 1 - знак отрицательный, 0 - положительный);
  • длинное вещественное число . Для хранения такого числа отводится 64 бита. Биты 0-51 нужны для хранения мантиссы. Биты 52-62 предназначены для хранения числа q , сложенного с числом 1024 . Последний 63-й бит определяет знак числа ( 1 - знак отрицательный, 0 - положительный);
  • расширенное вещественное число . Для хранения числа отводится 80 битов. Биты 0-63 - мантисса числа. Биты 64-78 — показатель q , сложенный с числом 16383 . 79 -й, последний бит отводится для знака числа ( 1 - знак отрицательный, 0 - положительный).

Очевидно, пришла пора разобрать конкретный пример представления в памяти вещественного числа. Итак, пусть в программе на языке Си имеем объявление переменной:

Тип float - это короткое вещественное число, т. е. в памяти оно, согласно выше записанному, будет занимать 32 бита. Попытаемся теперь нашим обычным способом заглянуть в память. Вот они, четыре байта, которые и призваны представлять наше число:

00 00 a1 c2

Чтобы легче было разбираться, представим последовательность из четырех байтов в двоичном виде:

00000000 00000000 10100001 11000010

Или более понятным способом, начиная со старшего байта для выделения мантиссы, показателя и знака:

11000010 10100001 00000000 00000000

Выделим мантиссу. На нее отводиться 23 бита. Имеем, таким образом, двоичное число 0100001 . Учтите, что биты мантиссы, отсчитываются, начиная со старшего (в данном случае 22 -го) бита, а оставшиеся нули естественно отбрасываются, поскольку вся мантисса располагается справа от запятой. Правда, это еще не совсем мантисса. Как ранее было сказано, единица перед запятой в представлении отбрасывается. Так что мы должны восстановить ее. Поэтому мантиссой будет число 0b1,0100001 . Знак всего числа, как мы видим, определяется единицей, следовательно, задает отрицательное число. А вот показатель нам следует получить из двоичного числа 0b10000101 . В десятичном представлении это число 133 . Вычитая число 127 (для короткого вещественного числа), получим 6 . Следовательно, для того чтобы получить из мантиссы истинное дробное число, нужно сместить в ней точку на шесть разрядов вправо. Окончательно получаем 0b1010000,1 . В шестнадцатеричной системе счисления это просто 0x50,8 , а в десятичной получаем как раз 80,5 .

В качестве тренировки я бы вам предложил следующую цепочку байтов:

00 80 fb 42

Попытайтесь доказать, что это есть ничто иное, как представление числа 125,75 .

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

Ассемблер вам в радость! Пока! Подписываемся на мой канал Old Programmer .



Старинная запись на квитанции в уплате подати («ясака»). Она означает сумму 1232 руб. 24 коп. Иллюстрация из книги: Яков Перельман «Занимательная арифметика»

Ещё Ричард Фейнман в книге «Вы конечно шутите, мистер Фейнман!» поведал несколько приёмов устного счёта. Хотя это очень простые трюки, они не всегда входят в школьную программу.

Например, чтобы быстро возвести в квадрат число X около 50 (50 2 = 2500), нужно вычитать/прибавлять по сотне на каждую единицы разницы между 50 и X, а потом добавить разницу в квадрате. Описание звучит гораздо сложнее, чем реальное вычисление.

52 2 = 2500 + 200 + 4
47 2 = 2500 – 300 + 9
58 2 = 2500 + 800 + 64

Молодого Фейнмана научил этому трюку коллега-физик Ханс Бете, тоже работавший в то время в Лос-Аламосе над Манхэттенским проектом.

Ханс показал ещё несколько приёмов, которые использовал для быстрых вычислений. Например, для вычисления кубических корней и возведения в степень удобно помнить таблицу логарифмов. Это знание очень упрощает сложные арифметические операции. Например, вычислить в уме примерное значение кубического корня из 2,5. Фактически, при таких вычислениях в голове у вас работает своеобразная логарифмическая линейка, в которой умножение и деление чисел заменяется сложением и вычитанием их логарифмов. Удобнейшая вещь.



Логарифмическая линейка

До появления компьютеров и калькуляторов логарифмическую линейку использовали повсеместно. Это своеобразный аналоговый «компьютер», позволяющий выполнить несколько математических операций, в том числе умножение и деление чисел, возведение в квадрат и куб, вычисление квадратных и кубических корней, вычисление логарифмов, потенцирование, вычисление тригонометрических и гиперболических функций и некоторые другие операции. Если разбить вычисление на три действия, то с помощью логарифмической линейки можно возводить числа в любую действительную степень и извлекать корень любой действительной степени. Точность расчётов — около 3 значащих цифр.

Чтобы быстро проводить в уме сложные расчёты даже без логарифмической линейки, неплохо запомнить квадраты всех чисел, хотя бы до 25, просто потому что они часто используются в расчётах. И таблицу степеней — самых распространённых. Проще запомнить, чем вычислять каждый раз заново, что 5 4 = 625, 3 5 = 243, 2 20 = 1 048 576, а √3 ≈ 1,732.

Ричард Фейнман совершенствовал свои навыки и постепенно замечал всё новые интересные закономерности и связи между числами. Он приводит такой пример: «Если кто-то начинал делить 1 на 1,73, можно было незамедлительно ответить, что это будет 0,577, потому что 1,73 — это число, близкое к квадратному корню из трёх. Таким образом, 1/1,73 — это около одной трети квадратного корня из 3».

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

В наше время люди достают калькулятор, чтобы просто поделить 76 на 3. Удивить окружающих стало гораздо проще. Во времена Фейнмана вместо калькулятора были деревянные счёты, на которых тоже можно было производить сложные операции, в том числе брать кубические корни. Великий физик уже тогда заметил, что использование таких инструментов, людям вообще не нужно запоминать множество арифметический комбинаций, а достаточно просто научиться правильно катать шарики. То есть люди с «расширителями» мозга не знают чисел. Они хуже справляются с задачами в «автономном» режиме.

Вот пять очень простых советов устного счёта, которые рекомендует Яков Перельман в методичке «Быстрый счёт» 1941 года издательства.

1. Если одно из умножаемых чисел разлагается на множители, удобно бывает последовательно умножать на них.

225 × 6 = 225 × 2 × 3 = 450 × 3
147 × 8 = 147 × 2 × 2 × 2, то есть трижды удвоить результат

2. При умножении на 4 достаточно дважды удвоить результат. Аналогично, при делении на 4 и 8, число делится пополам дважды или трижды.

3. При умножении на 5 или 25 число можно разделить на 2 или 4, а затем приписать к результату один или два нуля.

Здесь лучше сразу оценивать, как проще. Например, 31 × 25 удобнее умножать как 25 × 31 стандартным способом, то есть как 750+25, а не как 31 × 25, то есть 7,75 × 100.

При умножении на число, близкое к круглому (98, 103), удобно сразу умножить на круглое число (100), а затем вычесть/прибавить произведение разницы.

4. Чтобы возвести в квадрат число, оканчивающееся цифрой 5 (например, 85), умножают число десятков (8) на него же плюс единица (9), и приписывают 25.

Почему действует это правило, видно из формулы:

Приём применяется и к десятичным дробям, которые оканчиваются на 5:

5. При возведении в квадрат не забываем об удобной формуле

Читайте также: