DEV: Рубиновые тона – Telegram
DEV: Рубиновые тона
3.22K subscribers
143 photos
2 videos
8 files
976 links
Анонсы новых видео о программировании (Ruby/Rails, Solidity/Ethereum, Python, JS и не только), практические советы, обзор полезных инструментов и новости из мира IT
Download Telegram
Как числа представлены в компьютере?

Это немаловажный вопрос, ответ на который состоит из нескольких частей. Дело в том, что у нас есть обычные неотрицательные целые числа (то есть от 0 и далее, без дробной части), просто целые числа (которые могут быть отрицательными или положительными), а также дробные, которые тоже могут быть отрицательными или положительными.

Проще всего, конечно, с целыми неотрицательными числами. Так как мы, опять же, работаем только с нулями и единицами, то привычные нам десятичные числа в компьютере могут быть закодированы в виде вектора, состоящего из набора 0-1, в общем случае:

[ x(w - 1), x(w - 2), ... x(0) ]


w - длина вектора. Естественно, чем больше длина этого вектора, тем больше чисел мы можем с его помощью закодировать. В этом векторе наиболее значимый бит x(w-1) находится слева, а наименее значимый x(0) - справа. Если бит имеет значение 1, то он "привносит" в закодированное значение 2 ** i, где i - его порядковый номер. Собственно говоря, именно на этом факте строится процесс перевода из двоичного вида в десятичный.

Чтобы было проще, возьмём w = 4 и двоичное число 1010. Наиболее значимый бит имеет порядковый номер 3 (считаем с нуля), также у нас установлен в значение 1 бит под номером 1. Следовательно:

2 ** 3 + 2 ** 1 = 10


Это можно легко проверить, например, в Rust:

let number: u8 = 10;

println!("{number:b}");


Таким образом, числу 10 соответствует вектор 1010, но верно и обратное. Больше того, это соответствие "один ко одному", то есть в данном случае 1010 никакое другое число не представляет. Такая штука называется биекция.

При w = 4 минимальное число, которое мы можем закодировать 0 0 0 0, то есть просто 0, а максимальное - 1 1 1 1, то есть 15 (аналогично, для w = 8 максимальное число - это 255).

В общем, здесь всё просто и довольно очевидно. Интереснее становится, когда мы переходим к **целым числам, имеющим знак** (то есть они могут быть отрицательными). Где и как нам этот знак хранить? Раньше мы несколько обходили этот момент, говоря, что отдельный бит резервируется под хранение знака. Это некое упрощение.

На самом деле, вариантов представления чисел со знаком есть некоторое количество (в том числе, и случай, когда наиболее значимый бит просто говорит "есть минус или нет минуса"). Но, пожалуй, самый распространённый принцип - это так называемый "two's complement". Суть довольно проста.

У нас опять есть вектор из нулей и единиц указанной длины, предположим, 1 0 1 1.

Все биты, за исключением наиболее значимого, интерпретируются как и раньше, то есть "привносят" в значение 2 ** i:

2 ** 1 + 2 ** 0 = 2 + 1 = 3


Наиболее значимый бит (запишем его как X) имеет специальное назначение, и он фигурирует в выражении:

-X * (2 ** (w - 1))


В нашем случае выходит:

-1 * (2 ** 3) = -8


Полученное значение затем просто суммируется с тем, что мы получили после обработки всех битов, кроме наиболее значимого:

-8 + 3 = - 5


Следовательно, 1011 = -5.

Если X = 0, то формула выше обращается в ноль, и наше число будет неотрицательным. К примеру, 0 1 0 1 = 2 ** 2 + 2 ** 0 = 5.

Можно сказать, что при такой кодировке X "тянет" наше число в сторону отрицательных значений, а другие биты "тянут" его обратно на положительную сторону. Следовательно, наименьшее значение получится, когда X тянет нас в отрицательную сторону, но все остальные биты отсутствуют:

1 0 0 0 = -(2 ** 3) = -8


То есть при w = 4 наименьшее значение будет -8.

Максимальное значение получается, когда "знаковый" бит равен нулю, зато есть все остальные, которые тянут нас в положительную сторону:

0 1 1 1 = 2 ** 2 + 2 ** 1 + 2 ** 0 = 4 + 2 + 1 = 7


Аналогично, при w = 8 диапазон будет от -128 до 127. К примеру,

let number: i8 = -128;
println!("{number:b}");


вернёт 10000000.

А теперь важный момент: мы понимаем, что **наш вектор из битов можно рассматривать по-разному**. Если мы считаем, что наиболее значимый бит используется для хранения информации о знаке, то, к примеру, число 10110100 равно -76. Но если мы считаем, что речь идёт о неотрицательных числах, то это 180!
🔥752👍2🥰1
Иными словами, нам нужно объяснить компьютеру, с чем мы имеем дело и как хотим интерпретировать этот вектор. Собственно говоря, именно поэтому нам нужно быть очень аккуратными, если мы конвертируем числа со знаком в числа без знака:

let number: i8 = -6;

println!("{number:b}");

let number2: u8 = number as u8;

println!("{number2}");
println!("{number2:b}");


Результат выполнения:

11111010
250
11111010


То есть последовательность битов та же, но интерпретация другая! Это происходит не только в Rust, но и в других языках, к примеру, в C.

С u8 ситуация может быть не сильно лучше:

let number: u8 = 255;

let number2: i8 = number as i8;

println!("{number2}");


На экран будет выведено -1.

Из всего этого следует важный вывод: конвертировать числа таким образом стоит с большой осторожностью, потому что это может привести к неожиданным проблемам и серьёзным багам. Инструмент Clippy даже выдаст предупреждение на моменте number as i8. Если вам нужно просто "отбросить" знак, то следует использовать функцию "модуль" .abs().
👍121
Кстати. Пожалуйста, добавьтесь в чат для отправки комментов https://news.1rj.ru/str/+MxYT6-01eeA1NTYy К сожалению, без добавления нельзя, этим пользуются спамеры.
👍31👌1🥴1
Теперь поговорим о дробных числах и их представлении в компьютере, в частности, о том, как их описывает стандарт IEEE 754, принятый в 1985 году.

Чтобы было проще, начнём с обычного десятичного числа 35.42, у которого, как можно видеть, имеется дробная часть. Как ещё можно записать это число? Ну, к примеру, вот так:

3 * (10 ** 1) + 5 * (10 ** 0) + 4 * (10 ** -1) + 2 * (10 ** -2)


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

При этом цифрам, стоящим после точки, мы просто присваиваем отрицательные индексы. В принципе, это же число можно представить как 35 + (42 / 100), суть та же.

Ясное дело, этот принцип можно применить и двоичным числам. К примеру, если взять двоичное 101.11, то у нас выйдет:

1 * (2 ** 2) + 0 * (2 ** 1) + 1 * (2 ** 0) + 1 * (2 ** -1) + 1 * (2 ** -2)


Или, проще говоря,

4 + 0 + 1 + (1/2) + (1/4)


То есть мы видим, что после точки у нас появляются дроби со степенями двойки: 1/2, 1/4, 1/8, и так далее. Так мы можем легко представить дробь 3/16 (или 0.1875), это будет 0.0011. Хотя такой подход в теории можно использовать, он не слишком удобен, особенно для очень больших чисел.

Поэтому стандарт IEEE 754 представляет дробные числа в виде формулы:

((-1) ** s) * M * (2 ** E)


Здесь три параметра, и в компьютере каждый хранится в своём поле.

s равно либо 0, либо 1. Это информация о знаке, соответствующее поле занимает всегда 1 бит.

M - это мантисса, дробное число, обычно меньше 1. Его представляет поле frac, занимает оно n бит и содержит последовательность`f(n-1), ..., f(1), f(0)`.

Ну, а E - это экспонента, которая может быть как положительной, так и отрицательной. Её представляет поле exp длиной k бит с последовательностью e(k-1), ..., e(1), e(0).

Итак, эти числа состоят аж из трёх полей сразу, и обычно имеют либо одинарную (single, 32 бита), либо двойную (double, 64 бита) точность. К примеру, для одинарной точности s занимает один бит номер 31, exp - с 23 по 30 биты, остальное (с 0 по 22) отводится под мантиссу.

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

Первый случай самый типичный, он описывает нормированные значения. Это случай, когда экспонента не состоит целиком из нулей или целиком из единиц. Говорят, что экспонента хранится в смещённой (biased) форме, а само её значение считается как:

E = e - B


e - число без знака с последовательностью e(k-1), ..., e(1), e(0). B - это значение bias, которое равно 2 ** (k - 1) - 1, то есть для одинарной точности оно равно 127, потому что в этом случае длина поля exp составляет 8 бит.

Следовательно, финальное значение E будет лежать в пределах от -126 (тк e - число без знака, и оно точно больше нуля в данном случае) до 127 для одинарной точности.

Поле frac в этом случае описывает дробную часть, то есть его значение f в десятичном виде лежит от 0 (включительно) до 1 (не включительно), это довольно важно, процесс формирования f увидим в примере ниже. Финальное значение мантиссы M = 1 + f.

Следующий случай - денормированные значения, это когда в поле exp содержатся все нули. В этом случае E = 1 - B, M = f.

Такие денормированные значения, в частности, используются, чтобы представить значение 0. А чего бы нам для нуля не использовать первый случай? Ну потому, что там M = 1 + f, то есть в любом случае M >= 1.

Кстати, тут получается интересный момент: у нас может быть как +0, так и -0. Почему? Потому что хотя биты в exp и frac занулены, s всё равно может иметь значение как 0, так и 1.

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

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

Если в frac записаны одни нули, то таким образом мы представляем бесконечность (плюс или минус бесконечность, зависит от знака s) - примеру, если мы поделили на ноль.
👍4
Если в frac указано что-то иное (не все нули), то это значение называется "not a number" (NaN). Оно может вылезти, если результат невозможно представить реальными числами (настоящие джедаи помнят про мнимые числа, это не тот случай), либо если происходит что-то странное в духе "бесконечность минус бесконечность".

В качестве примера можно взять 8-битный формат, где на exp отводится k=4 бит, а на frac даётся n=3 бит (понятно, что на знак в любом случае 1 бит). В этом случае значение bias B = 2 ** (4-1) - 1 = 7.

Как мы представим ноль? Ну, очевидно вот так (биты разграничены по соответствующим полям):

0 0000 000


e=0, E = 1 - 7 = -6.

f можно записать как 0 / 8 (тк в поле frac у нас все нули), M = f = 0 / 8. Выходит формула:

((-1) ** 0) * (0/8) * (2 ** -6) = 0


Классно. Теперь число 7/8, то есть 0.875. Его представление:

0 0110 110


e = 0110 = 6, тогда E = 6 - 7 = -1.

Теперь f. У нас три бита, 2 ** 3 = 8, а само значение frac = 110, то есть 6. Выходит, что f = 6/8, M = 14 / 8.

Подставляем:

((-1) ** 0) * (14 / 8) * (2 ** -1) = 0.875


Попробуем ещё взять вот такое число, это в нашем случае самое большое из денормированных:

0 0000 111


Выходит, что e = 0, E = -6. При этом f = M = 7/8. Подставим:

((-1) ** 0) * (7/8) * (2 ** -6) ~ 0.013671875


А если самое маленькое из нормированных?

0 0001 000


Тут e = 1, E = -6, f = 0 / 8, M = 8 / 8. Подставляем:

((-1) ** 0) * (8/8) * (2 ** -6) ~ 0.015625


Аналогично, самое маленькое позитивное число, которое мы можем представить (грубо говоря, самое близкое к нулю):

0 0000 001


Тут e = 0, E = -6, f = M = 1/8. В формуле:

((-1) ** 0) * (1/8) * (2 ** -6) = 0.001953125


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

Кстати, тут один интересный момент. Смотрите, что у нас вышло:

0 0000 000 = 0

0 0000 001 = 0.001953125

0 0000 111 = 0.013671875

0 0001 000 = 0.015625

0 0110 110 = 0.875


Я расположил эти числа по возрастанию от самого маленького десятичного к самому большому. Но при этом можно видеть, что их двоичные аналоги тоже стоят по возрастанию! Это вовсе не случайно: стандарт создавался с учётом того, что программистам наверняка потребуется сортировать такие числа по тем же принципам, что и для целых чисел (хотя тут есть небольшая проблема, если в первом бите появляется 1, то есть число отрицательное).

Собственно говоря, из примера выше мы видим, что представить "любое" число мы с помощью такого стандарта не можем. К примеру, у нас идёт "перескок" от 0.013671875 сразу к 0.015625, и сделать с этим особенно ничего не получится. Да, можно использовать не 8 бит, а 32 или даже 64 (двойная точность), но, как вы понимаете, всё равно покрыть все возможные случаи никак не выйдет. Поэтому в том же Rust есть понятие "эпсилон" (мы его с вами видели на стриме), то есть определённая погрешность, которую стоит учитывать.
👍4
В этом уроке мы попробуем ответить на важный вопрос: как именно хранятся числа в компьютере, если современные процессоры оперируют только нулями и единицами? Мы узнаем о принципах хранения целых чисел без знака, со знаком, а также о стандарте IEEE 754, описывающем хранение дробных чисел. https://www.youtube.com/watch?v=Pe3GCa3WKBU
❤‍🔥82
О, кажется у меня круглое число 😂
👍15🎉82🎄1
У меня тут новая статья, в этот раз про scraping на питоне с шутками да прибаутками - в частности, сбор результатов из поиска google для последующего анализа (может быть полезно, например, для отслеживания позиций интересующих доменов по разным ключевым словам) https://www.scrapingbee.com/blog/how-to-scrape-google-search-results-data-in-python-easily/
❤‍🔥10👍7👀21
В этом уроке по Ethereum мы посмотрим, что нового появилось в OpenZeppelin Defender v2, что изменилось, и как с ним работать. С помощью этого решения вы сможете легко администрировать контракты, настраивать автоматизированные действия, создавать сценарии выполнения, и многое другое. https://youtu.be/tlhsIZX7LI0
12🔥4
Записал первые три главы аудиокниги "Воспоминания" (Тэффи). Если не знаете, что почитать (послушать), то это отличный вариант. Это не нудные мемуары, а интересное повествование о быте и проблемах во времена Гражданской войны в 1918 году. Здесь нет больших исторических личностей и значительных монологов, только жизнь обычных людей. Как мне кажется, во многом эти события (иногда до степени неотличимости) перекликаются и с тем, что происходит сейчас https://www.youtube.com/watch?v=zzYHjCnOsNc
🙏8👍7🔥2👏1🥱1
А сегодня у нас интересный стрим по Solidiy!

В этом стриме по Solidity мы рассмотрим новое решение от OpenZeppelin под названием AccessManaged. Оно позволяет настраивать права доступа через отдельный контракт, который сообщает, может ли определённая роль вызвать ту или иную функцию. Кроме того, AccessManaged позволяет делать аналог Timelock, то есть определённые роли должны поставить вызов функции в очередь, только спустя некоторое время её выполнить.

https://youtube.com/live/pL6tpKNfmJM?feature=share
👍12🔥2👏1
Что ж, мне очень приятно, что первые три главы (совершенно неожиданно) так заинтересовали слушателей, так что буквально только что закончил запись ещё двух глав. В них рассказывается о том, что же происходило в странном городке, где ветчину едят только днём, и как избежать карантина у немцев. https://www.youtube.com/watch?v=NQvV_Kfbs-A
👍13👎1🔥1😱1
Производил тут небольшой анализ в плане SEO от нечего делать. Обнаружил некоторые забавные вещи - в частности, кто-то создал автоматизированный клон YT, куда льются видео по теме разработки и не только, прямо сплошняком, без стеснения. Ну, что сказать, это предсказуемо.

С большим удивлением узнал, что книга Modern CSS, одним из автором которой я был, теперь доступа в довольно крупном издательстве O'Reilly. Впрочем, думается, что CSS там уже не совсем modern, так как ей лет шесть.

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

Если же в поиске обнаружился другой тёзка, но с отчеством Михайлович, то это мой относительно близкий родственник, учёный 🤪

В общем, иногда любопытно погуглить, что находится по своему имени-фамилии, попробуйте

https://www.youtube.com/watch?v=55nPncn6rRk&list=OLAK5uy_l_2JrkrjWXP2riZrzs4pHk5E8X565K7Yg&index=10
👍5😁31🤯1
В этом уроке по Rust мы поговорим об одной из самых неочевидных и необычных тем - о lifetimes, аналога которым в большинстве языков не встречается. Мы попробуем разобраться, зачем это нужно, рассмотрим примеры и порешаем задачи rustlings. https://www.youtube.com/watch?v=usnj6UZTsc4
👍9❤‍🔥2👎1🔥1
Я вот буквально вчера получил странное сообщение в LinkedIn, в котором неизвестный мне джентльмен сообщал, что планирует податься на работу к нам в Lokalise и почему-то просил меня передать его резюме в отдел HR (?). Да, в моём имени была ошибка, но это уже частности.

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

Но ведь я этого человека даже не знаю - как же я могу его рекомендовать?.. Больше того, он подаётся на должность, которая ко мне не имеет отношения. Странно это. Или это и правда типичная история теперь?

Мы вот говорили с нашими студентами какое-то время назад насчёт работы, такой трюк, кажется, никто не упоминал 😂 Впрочем, чего говорить, время сейчас не самое простое, увольнения продолжаются, ИИ шагает широкой поступью...
😁7👍21🤷‍♂1