Name already in use
brestprog / topics / bitmasks / bitmasks.md
- Go to file T
- Go to line L
- Copy path
- Copy permalink
- Open with Desktop
- View raw
- Copy raw contents Copy raw contents
Copy raw contents
Copy raw contents
Как известно, числа в памяти компьютера представляются в двоичной системе счисления в виде последовательности битов. Один бит может иметь значение $$0$$ или $$1$$ . Можно провести аналогию между битами и значениями типа bool : $$0$$ обозначает false , а $$1$$ — true . По такой аналогии число (последовательность битов) можно представить как массив значений bool . Например, тип int может обозначать массив из $$32$$ значений bool , а long long — из $$64$$ .
Чаще всего массивы bool небольшого размера испольуются для обозначения некоторого подмножества объектов, выбранного из множества. Например, для обозначения элементов с индексами $$1$$ и $$4$$ (0-индексация), выбранных из множества из пяти элементов используется массив $$
При интерпретации такого значения как обычного числа, оно будет равно $$10010_2 = 18_<10>$$ . Но при интерпретации его как массива логических значений (битов), оно будет обозначать $$<0, 1, 0, 0, 1>$$ . С точки зрения C++ эти два значения равносильны, и то, является ли значение типа int числом, или массивом bool , зависит только от контекста, в котором оно используется.
При использовании значений типа int или long long как массивов из bool , такие значения называются битовыми масками.
Операции с битовыми масками
Для работы с масками используются побитовые операции $$and, or, xor$$ , и битовые сдвиги.
Пусть две маски обозначают два множества элементов, и нам нужно получить маску, содержащую элементы, входящие в оба множества (пересечение множеств). В новой маске true должны находится только в тех позициях, где в обеих масках находились true . Несложно заметить, что такое описание соответствует побитовой операции $$and$$ :
int c = a & b; // маска c равна пересечению масок a и b
Другой распространённой операцией является объединение множеств. При объединении в новой маске true должны находиться в тех позициях, в которых в хотя бы одной из масок находилось true . Такое описание соответствует побитовой операции $$or$$ :

int c = a | b; // маска c равна объединению масок a и b
Для получения значения индивидуального бита используется комбинация битового сдвига вправо и операции $$and$$ . Сначала нам нужно сдвинуть биты маски так, чтобы нужный бит оказался самым младшим (находился справа). Затем нам нужно откинуть все остальные биты маски. Реализуется это следующим образом:

int bit = (a >> idx) & 1; // bit равен значению idx-го бита в маске a
Для установки значения определённого бита маски в true мы должны применить к нему операцию $$or\ true$$ . Реализуется это так (ко всем остальным битам применяется $$or\ false$$ , не имеющая никакого эффекта):

int b = a | (1 << idx); // маска b — копия маски a, в которой idx-ый бит установлен в true
Для изменения значения определённого бита на противоположное, нужно применить к нему операцию $$xor\ true$$ . Ко всем остальным битам применяется $$xor\ false$$ , также не имеющая никакого эффекта:

int b = a ^ (1 << idx); // маска b — копия маски a с противоположным значением idx-го бита
Это самые распространённые операции, выполняемые с масками. Существует множество других, здесь не приведённых, но все они основаны на $$and, or, xor$$ , и сдвигах.
Динамическое программирование по подмножествам
Битовые маски часто используются как параметры для динамического программирования. Такой вид ДП называется ДП по маскам, или ДП по подмножествам.
Классической задачей на ДП по подмножествам является широко известная задача о коммивояжёре (англ. TSP — Travelling Salesman Problem). В общем виде она ставится следующим образом:
Существует $$N$$ городов, между некоторыми из которых есть дороги. Требуется обойти все города, вернувшись в первый, так, чтобы длина пути была минимальной.
Эта задача является классической NP-полной задачей, и ДП по подмножествам со сложностью порядка $$O(2^N * N^2)$$ — её оптимальное решение.
Для простоты реализации примем, что города являются точками на геометрической плоскости, и между каждой парой есть путь, длина которого равна расстоянию между точками.
Обозначим за $$dp[mask][i]$$ — длину кратчайшего пути, начинающегося в вершине $$0$$ , проходящего через все вершины $$mask$$ , и заканчивающегося в вершине $$i$$ ( $$0 \in mask; i \in mask$$ ). Начальные значения ДП можно записать следующим образом:
$$dp[mask][i] = \infty \\ dp[<0>][0] = 0$$
Запись $$<0>$$ обозначает маску, обозначающую множество, состоящее только из элемента $$0$$ .
Формула перехода для ДП выглядит так:
Запись $$mask \backslash i$$ обозначает множество $$mask$$ без элемента $$i$$ .
Формула перехода обозначает следующее: чтобы попасть в вершину $$i$$ , нужно перейти в неё из какой-либо другой вершины $$j$$ , также входящей в $$mask$$ . Из всех таких вершин нужно выбрать такую, что общая длина пути в $$i$$ будет наименьшей. Общая длина пути рассчитывается как длина пути в $$j$$ ( $$dp[mask \backslash i][j]$$ ) плюс длина пути из $$j$$ в $$i$$ ( $$dst(j, i)$$ ).
Чтобы найти минимальную длину цикла из всех вершин, нужно перебрать вершину $$i$$ , и выбрать такую, что длина цикла $$0 — \ldots — i — 0$$ минимальна. Длина такого цикла вычисляется по формуле $$dp[\mathbb
Запись $$\mathbb
Может показаться, что порядок пересчёта такого ДП будет достаточно сложным. На самом деле это не так: заметьте что для пересчета $$dp[mask][i]$$ нам нужно, чтобы уже был посчитан ответ для маски $$mask \backslash i$$ . Можно легко доказать, что в численном выражении $$mask \backslash i$$ будет гарантированно меньше $$mask$$ , так как на некоторой позиции в $$mask$$ будет находиться бит $$1$$ , а в $$mask \backslash i$$ бит $$0$$ . Значит, можно просто пересчитывать ДП в порядке возрастания значения $$mask$$ в численном выражении.
Реализация решения задачи о коммивояжёре
using namespace std;
const double INF = 1e9 + 7; //бесконечность
double x[20], y[20]; //координаты городов.
//Заметим, что для N элементов существует 2^N возможных подмножеств (масок) //значением от 0 до 2^N — 1. //Можно просто возвести 2 в произвольную степень с помощью битового сдвига: //2^N = 1 << N double dp[1 << 20][20];
O.3 – Битовые манипуляции с побитовыми операторами и битовыми масками
В предыдущем уроке о побитовых операторах (O.2 – Побитовые операторы) мы обсудили, как различные побитовые операторы применяют логические операции к каждому биту в операндах. Теперь, когда мы понимаем, как они работают, давайте посмотрим, как они чаще всего используются.
Битовые маски
Чтобы управлять отдельными битами (например, устанавливать их в 1 или сбрасывать в 0), нам нужен способ идентифицировать конкретные биты, которыми мы хотим манипулировать. К сожалению, побитовые операторы не умеют работать с битовыми позициями. Вместо этого они работают с битовыми масками.
Битовая маска – это предопределенный набор битов, который используется для выбора того, какие конкретные биты будут изменены при последующих операциях.
Рассмотрим случай из реальной жизни, когда вы хотите покрасить оконную раму. Если не проявить осторожность, вы рискуете покрасить не только оконную раму, но и само стекло. Вы можете купить малярный скотч и приклеить его к стеклу и другим частям, которые не нужно красить. Затем, когда вы будете красить, малярный скотч будет блокировать попадание краски на всё, что вы не хотите красить. В конце концов, окрашиваются только немаскированные части (те части, которые вы хотите покрасить).
Битовая маска, по сути, выполняет ту же функцию для битов – битовая маска блокирует побитовые операторы от прикосновения к битам, которые мы не хотим изменять, и позволяет получить доступ к тем, которые мы действительно хотим изменить.
Давайте сначала узнаем, как определять простые битовые маски, а затем мы покажем, как их использовать.
Определение битовых масок в C++14
Самый простой набор битовых масок – это определение одной битовой маски для каждой битовой позиции. Мы используем нули, чтобы замаскировать биты, которые нам не нужны, и единицы, чтобы обозначить биты, которые мы хотим изменить.
Хотя битовые маски могут быть литералами, их часто определяют как символьные константы, поэтому им можно дать осмысленное имя и легко использовать повторно.
Поскольку C++14 поддерживает двоичные литералы, определить эти битовые маски очень просто:
Теперь у нас есть набор символьных констант, представляющих каждую битовую позицию. Мы можем использовать их для манипулирования битами (вскоре мы покажем, как это сделать).
Определение битовых масок в C++11 или в более ранних версиях
Поскольку C++11 не поддерживает двоичные литералы, то для установки символьных констант мы должны использовать другие методы. Для этого есть два хороших способа. Менее понятным, но более распространенным является использование шестнадцатеричных чисел. Если вам нужно напомнить о шестнадцатеричной системе счисления, еще раз посмотрите урок «4.13 – Литералы».
Это может быть трудно читать. Один из способов упростить задачу – использовать оператор сдвига влево, чтобы сместить бит в нужное место:
Проверка бита (чтобы узнать, установлен ли он в 1, или нет)
Теперь, когда у нас есть набор битовых масок, мы можем использовать их вместе с переменной битовых флагов для управления этими битовыми флагами.
Чтобы определить, установлен ли бит в 1 (включен, on) или сброшен в 0 (выключен, off), мы используем побитовое И в сочетании с битовой маской для соответствующего бита:
Эта программа напечатает:
Установка бита в 1
Чтобы установить бит в 1, мы используем присваивание с побитовым ИЛИ (оператор |= ) в сочетании с битовой маской для соответствующего бита:
Эта программа напечатает:
Мы также можем установить в 1 несколько бит одновременно, используя побитовое ИЛИ:
Сброс бита в 0
Чтобы сбросить бит в 0, мы используем побитовое И и побитовое НЕ вместе:
Эта программа напечатает:
Мы можем сбросить в 0 несколько бит одновременно:
Инвертирование бита
Чтобы инвертировать (переключить на противоположное) состояние бита, мы используем побитовое исключающее ИЛИ:
Эта программа напечатает:
Мы можем инвертировать несколько бит одновременно:
Битовые маски и std::bitset
std::bitset поддерживает полный набор побитовых операторов. Таким образом, несмотря на то, что для изменения отдельных битов проще использовать функции ( test , set , reset и flip ), если хотите, вы можете использовать побитовые операторы и битовые маски.
Зачем это нужно? Функции позволяют изменять только отдельные биты. Побитовые операторы позволяют изменять сразу несколько битов.
Эта программа напечатает:
Делаем битовые маски более осмысленными
Присвоение нашим битовым маскам имен " mask1 " или " mask2 " говорит нам, какой бит обрабатывается, но не дает нам никакой индикации, для чего на самом деле используется этот битовый флаг.
Лучше всего давать вашим битовым маскам полезные имена, чтобы документировать значение ваших битовых флагов. Вот пример из игры, которую мы могли бы написать:
Вот тот же пример, реализованный с использованием std::bitset :
Два примечания: во-первых, std::bitset не имеет удобной функции, позволяющей запрашивать биты с использованием битовой маски. Поэтому, если вы хотите использовать битовые маски, а не индексы позиций, для запроса битов вам придется использовать побитовое И. Во-вторых, чтобы увидеть, остался ли запрошенный нами бит установленным или сброшенным, мы используем функцию any() , которая возвращает true , если какие-либо биты установлены в 1, и false в противном случае.
Когда битовые флаги наиболее полезны?
Проницательные читатели могут заметить, что приведенные выше примеры на самом деле не экономят память. 8 логических значений обычно занимают 8 байтов. Но в приведенных выше примерах используется 9 байтов (8 байтов для определения битовых масок и 1 байт для переменной флага)!
Битовые флаги имеют наибольший смысл, когда у вас много идентичных переменных-флагов. Например, в приведенном выше примере представьте, что вместо одного человека ( me ) у вас было бы 100. Если вы использовали 8 логических значений на человека (по одному для каждого возможного состояния), вы использовали бы 800 байт памяти. С битовыми флагами вы должны использовать 8 байт для битовых масок и 100 байт для переменных битовых флагов, в общей сложности 108 байт памяти – примерно в 8 раз меньше памяти.
Для большинства программ объем памяти, сэкономленный с помощью битовых флагов, не стоит дополнительного усложнения. Но в программах, где есть десятки тысяч или даже миллионы похожих объектов, использование битовых флагов может существенно сократить использование памяти. Это полезный способ оптимизации, если она вам понадобится.
Есть еще один случай, когда использование битовых флагов и битовых масок может иметь смысл. Представьте, что у вас есть функция, которая может принимать любую комбинацию из 32 различных параметров. Один из способов написать эту функцию – использовать 32 отдельных логических параметра:
Надеюсь, вы дадите своим параметрам более информативные имена, но суть здесь в том, чтобы показать вам, насколько плох длинный список параметров.
Затем, когда вы захотите вызвать функцию с параметрами 10 и 32, установленными в значение true , вам нужно будет сделать это следующим образом:
Это до смешного сложно читать (это параметр 9, 10 или 11 установлен в значение true ?), а также это означает, что вы должны помнить, какой аргумент соответствует какому параметру («флаг редактирования» устанавливается 9-ым, 10-ым или 11-ым параметром?). Это также может быть не очень производительным, поскольку каждый вызов функции должен копировать 32 логических значения из вызывающей функции в вызываемую функцию.
Если вместо этого вы определили функцию, используя битовые флаги, например:
Вы сможете использовать битовые флаги для передачи только тех параметров, которые вам нужны:
Это не только намного удобнее для чтения, но и, вероятно, будет более производительным, поскольку включает всего 2 операции (одно побитовое ИЛИ и одно копирование параметра).
Это одна из причин, по которой OpenGL, хорошо зарекомендовавшая себя библиотека трехмерной графики, решила использовать параметры битовых флагов вместо последовательностей множества логических параметров.
Вот пример вызова функции из OpenGL:
GL_COLOR_BUFFER_BIT и GL_DEPTH_BUFFER_BIT – это битовые маски, определенные следующим образом (в gl2.h ):
Битовые маски, включающие несколько битов
Хотя битовые маски часто используются для выбора одного бита, их также можно использовать для выбора нескольких битов. Давайте посмотрим на немного более сложный пример, в котором мы это делаем.
Цветные дисплеи, такие как телевизоры и мониторы, состоят из миллионов пикселей, каждый из которых может отображать точку цвета. Точка цвета состоит из трех световых лучей: красного, зеленого и синего (RGB – red, green, blue). Изменяя интенсивность этих цветов, можно получить любой цвет в цветовом спектре. Обычно яркость R, G и B для пикселя представлена 8-битовым целочисленным типом без знака. Например, красный пиксель будет иметь R = 255, G = 0, B = 0. У фиолетового пикселя R = 255, G = 0, B = 255. Средне-серый пиксель будет иметь R = 127, G = 127, B = 127.
При присвоении значений цвета пикселю, помимо R, G и B, часто используется 4-е значение, называемое A. «A» означает «альфа», и оно определяет, насколько прозрачным будет цвет. Если A = 0, цвет полностью прозрачный. Если A = 255, цвет непрозрачный.
R, G, B и A обычно хранятся как одно 32-битное целое число, в котором для каждого компонента используется 8 бит:
| биты 31-24 | биты 23-16 | биты 15-8 | биты 7-0 |
|---|---|---|---|
| RRRRRRRR | GGGGGGGG | BBBBBBBB | AAAAAAAA |
| красный | зеленый | синий | альфа |
Следующая программа просит пользователя ввести 32-битное шестнадцатеричное значение, а затем извлекает из него 8-битные цветовые значения для R, G, B и A.
Эта программа дает следующий результат:
В приведенной выше программе мы используем побитовое И для запроса интересующего нас набора из 8 бит, а затем сдвигаем их вправо в 8-битное значение, чтобы мы могли распечатать его как шестнадцатеричное значение.
Резюме
Обобщим, как устанавливать, сбрасывать, инвертировать и запрашивать битовые флаги:
Чтобы запросить состояния битов, мы используем побитовое И:
Для установки битов в 1 (включения) используем побитовое ИЛИ:
Чтобы сбросить биты в 0 (очистить, выключить), мы используем побитовое И с побитовым НЕ:
Чтобы инвертировать состояния битов, мы используем побитовое исключающее ИЛИ:
Небольшой тест
Вопрос 1
В этом тесте не используйте std::bitset . Мы используем std::bitset только для печати.
Для заданной программы:
a) Напишите строку кода, чтобы сделать статью просматриваемой (включить флаг option_viewed ).
b) Напишите строку кода, чтобы проверить, была ли удалена статья (флаг option_deleted ).
c) Напишите строку кода, чтобы сбросить у статьи статус избранная (сбросить флаг option_favorited ).
Ожидаемый результат (при условии, что вы выполнили задание (а):
Вопрос 2
Дополнительное задание: почему следующие две строки идентичны?
Закон Де Моргана гласит, что если мы распространяем НЕ, нам нужно поменять ИЛИ на И, и наоборот. Поэтому,
What is bit masking?
I am fairly new to C programming, and I encountered bit masking. What is the general concept and function of bit masking?
Examples are much appreciated.
![]()
2 Answers 2
A mask defines which bits you want to keep, and which bits you want to clear.
Masking is the act of applying a mask to a value. This is accomplished by doing:
- Bitwise ANDing in order to extract a subset of the bits in the value
- Bitwise ORing in order to set a subset of the bits in the value
- Bitwise XORing in order to toggle a subset of the bits in the value
Below is an example of extracting a subset of the bits in the value:
Applying the mask to the value means that we want to clear the first (higher) 4 bits, and keep the last (lower) 4 bits. Thus we have extracted the lower 4 bits. The result is:
Masking is implemented using AND, so in C we get:
Here is a fairly common use-case: Extracting individual bytes from a larger word. We define the high-order bits in the word as the first byte. We use two operators for this, & , and >> (shift right). This is how we can extract the four bytes from a 32-bit integer:
Notice that you could switch the order of the operators above, you could first do the mask, then the shift. The results are the same, but now you would have to use a different mask:
Двоичные и побитовые операции в PHP

Недавно я обратил внимание, что в разных проектах мне приходится активно писать побитовые операции на PHP. Это очень интересное и полезное умение, которое пригодится начиная с чтения двоичных файлов до эмуляции процессоров.
В PHP есть много инструментов, помогающих манипулировать двоичными данными, но хочу сразу предупредить: если вам нужно супернизкоуровневая эффективность, то этот язык не для вас.
Почему PHP может оказаться не лучшим кандидатом
Я люблю PHP, не поймите меня неправильно. И я уверен, что этот язык будет прекрасно работать в большинстве случаев. Но если вам нужна максимальная эффективность обработки двоичных данных, то PHP не потянет.
Поясню: я не говорю о том, что приложение может потреблять на пять или десять мегабайт больше, а о выделении конкретного количества памяти для хранения данных определённого типа.
Согласно официальной документации о целых числах, PHP представляет десятичные, шестнадцатеричные, восьмеричные и двоичные значения с помощью целочисленного типа (integer). Так что не имеет значения, какие данные вы туда положите, они всегда будут целочисленными.
Вероятно, вы уже знаете про ZVAL — это С-структура, представляющая каждую PHP-переменную. В ней есть поле zend_long для представления всех чисел. У этого поля тип lval , размер которого зависит от платформы: на 64-битных платформах поле будет представлено как 64-битное число, а на 32-битных платформах — как 32-битное число.
Суть вот в чём: не имеет значения, нужно ли вам хранить 0xff, 0xffff, 0xffffff или что-то другое. В PHP все эти значения будут храниться как long (lval) с длиной 32 или 64 бита.
К примеру, недавно я экспериментировал с эмуляцией микроконтроллеров. И хотя необходимо корректно обрабатывать содержимое памяти и операции, мне не требовалось слишком большой эффективности использования памяти, потому что моя хостинговая машина компенсировала расходы на порядки.
Конечно, всё меняется, если мы говорим о С-расширениях или FFI, но это и не входит в мои цели. Я рассказываю о чистом PHP.
Поэтому помните: он работает и может вести себя так, как вам нужно, но в большинстве случаев типы будут расходовать память неэффективно.
Быстрое введение в двоичное и шестнадцатеричное представление данных
Прежде чем разговаривать о том, как PHP обрабатывает двоичные данные, нужно сначала поговорить о том, что такое двоичность. Если вы думаете, что уже всё знаете об этом, то переходите к главе Двоичные числа и строки в PHP.
В математике есть понятие «основание». Оно определяет, как мы можем представлять количества в разных форматах. Люди обычно используют десятичное основание (основание 10), что позволяет нам представлять любое число с помощью цифр 0, 1, 2, 3, 4, 5, 6, 7, 8 и 9.
Чтобы пояснить следующий пример, я буду называть число 20 как «десятичное 20».
Двоичные числа (основание 2) могут представлять любое число, но только с помощью двух цифр: 0 и 1.
Десятичное 20 в двоичной форме выглядит так: 0b00010100. Вам не нужно преобразовывать его в привычный вид самостоятельно, пусть это делают компьютеры. 😉
Шестнадцатеричные числа (основание 16) могут представлять любые числа с помощью десяти цифр 0, 1, 2, 3, 4, 5, 6, 7, 8 и 9, а также дополнительных шести символов из латинского алфавита: a, b, c, d, e и f.
Десятичное 20 в шестнадцатеричной форме выглядит так: 0x14. Его преобразование тоже возложите на компьютеры, они в этом эксперты!
Важно понимать, что числа можно представлять по разным основаниям: двоичному (основание 2), восьмеричному (основание 8), десятичному (основание 10, наше обычное) и шестнадцатеричному (основание 16).
В PHP и многих других языках двоичные числа пишутся как и любые другие, но с префиксом 0b: десятичное 20 выглядит как 0b00010100. Шестнадцатеричные числа получают префикс 0x: десятичное 20 выглядит как 0x14.
Как вы уже можете знать, компьютеры не хранят литеральные данные. Они всё представляют в виде двоичных чисел, нулей и единиц. Символы, цифры, буквы, инструкции — всё представлено по основанию 2. Буквы являются лишь условностью числовых последовательностей. Например, буква «a» имеет номер 97 в ASCII-таблице.
Но хотя всё хранится в двоичном виде, программистам удобнее всего читать данные в шестнадцатеричном формате. Они так лучше выглядят. Вы только посмотрите:
Хотя двоичный формат визуально занимает много места, шестнадцатеричные данные очень похожи на двоичное представление. Поэтому обычно мы используем их в низкоуровневом программировании.
Операции переноса
Вы уже знакомы с концепцией переноса (carry), но я должен уделить ей внимание, чтобы мы могли использовать её с разными основаниями.
В десятичном наборе у нас есть десять отдельных цифр для представления чисел, от 0 до 9. Но когда мы пытаемся представить числе больше девяти, нам не хватает цифр! И тут применяется операция переноса: мы делаем для числа префикс из цифры 1, а правую цифру сбрасываем в 0.
Двоичное основание ведёт себя так же, только оно ограничено цифрами 0 и 1.
То же самое и с шестнадцатеричным основанием, только у него диапазон гораздо шире.
Как вы поняли, для операции переноса нужно больше цифр для представления определённых чисел. Это позволяет нам понять, как ограничены определённые типы данных и, поскольку они хранятся в компьютерах, как ограничено их представление в двоичной форме.
Представление данных в памяти компьютера
Как я упоминал выше, компьютеры всё хранят в двоичном формате. То есть они содержат в памяти только нули и единицы.
Проще всего визуализировать эту концепцию в виде большой таблицы из одной строки и множества колонок (столько, сколько позволяет ёмкость памяти. Каждая колонка представляет собой двоичное число (бит).
Представление нашего десятичного 20 в такой таблице с помощью 8 бит выглядит так:
| Позиция (адрес) | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| Бит | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 0 |
Беззнаковое 8-битное целое — это число, которое можно представить максимум с помощью 8 двоичных чисел. То есть 0b11111111 (десятичное 255) будет самым большим среди беззнаковых 8-битных чисел. Добавление к нему 1 потребует применения операции переноса, что уже нельзя представить с помощью того же количества цифр.
Зная это, мы можем легко разобраться, почему для чисел существует так много представлений в памяти и что они собой представляют: uint8 — это беззнаковые 8-битные целочисленные (десятичные 0—255), uint16 — беззнаковые 16-битные целочисленные (десятичные 0—65535). Есть также uint32, uint64 и, теоретически, более высокие.
Знаковые целые числа, которые могут представлять отрицательные значения, обычно используют последний бит для определения положительности (последний бит = 0) или отрицательности (последний бит = 1). Как вы понимаете, они позволяют хранить в том же объёме памяти более маленькие значения. Знаковое 8-битное целочисленное варьируется от —128 до десятичного 127.
Вот десятичное —20, представленное в виде знакового 8-битного целочисленного. Обратите внимание, что задан первый бит (адрес 0, значение 1), это означает отрицательное число.
| Позиция (адрес) | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| Бит | 1 | 0 | 0 | 1 | 0 | 1 | 0 | 0 |
Надеюсь, пока всё понятно. Это введение очень важно для понимания внутренней работы компьютеров. Помните об этом, и тогда всегда будете понимать, как PHP работает под капотом.
Арифметические переполнения
Выбранное представление числа (8-битное, 16-битное) определяет минимальное и максимальное значение диапазона. Всё дело в том, как числа хранятся в памяти: добавление 1 к двоичной цифре 1 приводит к операции переноса, то есть нужен другой бит в качестве префикса для текущего числа. Поскольку целочисленный формат очень тщательно определён, мы не можем полагаться на операции переноса, выходящие за заданные пределы (на самом деле это возможно, но довольно безумно).
| Позиция (адрес) | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| Бит | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 |
Здесь мы очень близки к 8-битному пределу (десятичному 255). Если мы добавим единицу, то получим десятичное 255 в двоичном представлении:
| Позиция (адрес) | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| Бит | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
Все биты назначены! Добавление 1 потребует операции переноса, которая будет невозможна, потому что у нас не хватает битов, все 8 уже назначены! Эта ситуация называется переполнением, мы выходим за какой-то предел. Двоичная операция 255 + 2 должна дать 8-битный результат 1.
| Позиция (адрес) | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| Бит | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
Такое поведение не случайно, новое значение вычисляется с помощью определённых правил, которые мы не будем здесь рассматривать.
Двоичные числа и строки в PHP
Вернёмся к PHP! Извините за этот большой экскурс, но я считаю его важным.
Надеюсь, у вас в голове уже начали собираться кусочки мозаики: двоичные числа, в каком виде они хранятся, что такое переполнение, как PHP представляет числа…
Десятичное 20, представленное в PHP в виде целочисленного значения, в зависимости от платформы может иметь два разных представления. На х86-платформе это будет 32-битное представление, на х64 — 64-битное, но в обоих случаях будет стоять знак (то есть значение может быть отрицательным). Мы знаем, что десятичное 20 может поместиться в 8-битное пространство, но PHP обращается с любым десятичным числом как с 32- или 64-битным.
Также в PHP есть двоичные строки, которые можно преобразовывать туда-обратно с помощью функций pack() и unpack().
В PHP главное отличие между двоичными строками и числами в том, что строки просто содержат данные, как буфер. Целочисленные значения (двоичные и не только) позволяют выполнять с собой арифметические операции, но и двоичные (побитовые), такие как AND, OR, XOR и NOT.
Двоичность: что использовать в PHP, числа или строки?
Для транспортировки данных мы обычно используем двоичные строки. Поэтому чтение двоичного файла или сетевое взаимодействие требует упаковки и распаковки двоичных строк.
Однако фактические операции, такие как OR и XOR, со строковыми не получится выполнять надёжно, поэтому нужно использовать числа.
Отладка двоичных значений в PHP
Теперь давайте развлечёмся и немного поиграем с PHP-кодом!
Сначала я покажу, как визуализировать данные. Надо ведь понять, с чем мы имеем дело.
Отлаживать целые числа очень-очень просто, мы можем использовать функцию sprintf(). У неё очень мощное форматирование, и она поможет нам быстро понять, с какими значениями мы работаем.
Давайте представим десятичное 20 в 8-битном двоичном формате и в 1-байтном шестнадцатеричном:
Формат %08b выводит переменную $n двоичном представлении ( b ) с восемью цифрами ( 08 ).
Формат %02X выводит переменную $n в шестнадцатеричном представлении ( X ) с двумя цифрами ( 02 ).
Визуализация двоичных строк
Хотя в PHP целые числа всегда длиной 32 или 64 бита, длина строк равна длине их содержимого. Чтобы декодировать их двоичные значения и визуализировать их, нам нужно исследовать и преобразовать каждый байт.
К счастью, в PHP строки не являются именоваными, как массивы, и каждая позиция указывает на символ размером в 1 байт. Вот пример обращения к символам:
Если считать, что один символ занимает 1 байт, мы можем вызвать функцию ord() для приведения к 1-байтному целому числу:
Теперь можно выполнить двойную проверку с помощью приложения для командной строки hexdump:
В первой колонке расположен только адрес, а во второй колонке мы видим шестнадцатеричные значения, представляющие символы p , h и p .
Также при обработке двоичных строк мы можем использовать функции pack() и unpack(), и у меня есть для вас отличный пример! Допустим, вам нужно прочитать JPEG-файл, чтобы извлечь какие-нибудь данные (например, EXIF). С помощью режима чтения двоичных данных можно открыть обработчик файла и сразу же прочитать первые два байта:
Чтобы извлечь значения в целочисленный массив, можно просто распаковать их:
Обратите внимание, что формат С в функции unpack() преобразует символ в строку $soi в виде беззнаковых 8-битных чисел. Модификатор * распаковывает всю строку.
Побитовые операции
PHP реализует все побитовые операции, какие вам могут понадобиться. Они встроены в качестве выражений, а результат их работы описан ниже:
Я объясню работу каждого из них!
Пусть $x = 0x20 и $y = 0x30 . Ниже я покажу примеры с использованием двоичной нотации.
Как работает Inclusive Or ($x | $y)
Операция inclusive OR (включительное ИЛИ) берёт все биты из обоих входных данных. То есть $x | $y должно вернуть 0x30 . Посмотрите:
Примечание: справа налево был задан шестой бит $x (1), а также пятый и шестой биты $y . Данные были объединены и сгенерировано значение заданными пятым и шестым битами: 0x30 .
Как работает Exclusive Or ($x ^ $y)
Операция exclusive OR (исключительное ИЛИ, также известное как XOR) берёт биты, имеющиеся только с одной стороны. То есть результатом вычисления $x ^ $y будет 0x10 :
Как работает AND ($x & $y)
Оператор AND гораздо проще для понимания. Он к каждому биту применяет операцию И, так что извлечены будут только те значения, которые равны друг другу с обеих сторон. Результатом вычисления $x & $y будет 0x20 :
Как работает NOT (
Операции NOT требуется один параметр, она просто меняет значения всех переданных битов. Все 0 она превращает в 1, а все 1 — в 0.:
Если вы выполнили эту операцию в PHP и решили отладить с помощью sprintf() , то, вероятно, заметили более широкие числа? В главе Нормализация чисел я объясню, что тут происходит и как это исправить.
Как работает Left SHIFT и Right SHIFT ($x << $n и $x >> $n)
Смещение битов аналогично умножению или делению чисел на степень двойки. Все биты переходят на $n позиций влево или вправо.
Возьмём маленькое двоичное число, чтобы было проще показать, например, $x = 0b0010 . Если мы однократно сместим $x влево, этот один бит должен передвинуться на одну позицию влево:
То же самое со смещением вправо:
То есть смещение числа $n раз влево равносильно умножению двое $n раз, а смещение числа $n раз вправо равносильно делению на два $n раз.
Что такое битовая маска
С этими операциями и прочими методиками можно сделать много интересного. Например, применить битовую маску. Так называется произвольное двоичное число на ваш выбор, созданные для извлечения очень специфической информации.
Например, возьмём идею, что 8-битное знаковое число является положительным, если не задан восьмой бит (0), и отрицательным, если бит задан. Является ли положительным или отрицательным число 0x20 ? А что насчёт 0x81 ?
Чтобы ответить на это, мы можем создать очень удобный байт с единственным заданным отрицательным битом ( 0b10000000 , эквивалентно 0x80 ) и применить к 0x20 операцию AND. Если результат равен 0x80 ( 0b10000000 , нашей маске), то это отрицательное число, в противном случае оно положительное:
Такое часто бывает нужно при работе с флагами. Можно даже найти примеры использования в самом PHP, например, флаги сообщения об ошибках.
Можно выбрать, какого рода ошибки будут выдаваться:
Что здесь происходит? Просто посмотрите на своё значение:
Когда PHP видит уведомление, которое можно передать, он проверяет нечто подобное:
И вы увидите это везде! Двоичные файлы, процессоры, всякие низкоуровневые вещи!
Нормализация чисел
В PHP есть одна особенность, связанная с обработкой двоичных чисел: целые числа имеют размер 32 или 64 бита. Это означает, что зачастую нам нужно нормализовать их, чтобы доверять своим вычислениям.
Например, исполнение этой операции на 64-битной машине даст странный (но ожидаемый) результат:
Что тут произошло? Операция NOT в 8-битном целом числе ( 0x20 ) превратила все нулевые биты в единицы. Угадайте, что у нас было нулями? Правильно, все остальные 56 битов слева, которые до этого игнорировались!
Повторюсь, причина в том, что в PHP длина целых чисел составляет 32 или 64 бита, вне зависимости от их значений!
Однако код работает ожидаемо. Например, результатом операции
0x20 & 0b11011111 === 0b11011111 будет булево значение (true). Но не забывайте, что эти биты слева никуда не деваются, иначе вы получите странное поведение кода.
Для решения этой проблемы можно нормализовать числа, применив битовую маску, которая очищает все нули. Например, для нормализации
0x20 в 8-битное целое число нужно применить AND с 0xFF ( 0b11111111 ), чтобы все предыдущие 56 битов превратились в нули.
Внимание! Не забывайте о том, что содержится в ваших переменных, иначе получите неожиданное поведение. Например, давайте взглянем, что произойдёт, когда мы смещаем вышеописанное значение вправо без 8-битной маски:
Поясню: с точки зрения PHP это является ожидаемым, потому что вы явно обрабатываете 64-битное число. Вы должны понимать, что ожидает ВАША программа.
Совет: избегайте подобных глупых ошибок, программируя в парадигме TDD.
Заключение: двоичность и PHP классные
Когда вооружишься такими инструментами, всё остальное превращается лишь в поиск правильной документации по поведению двоичных файлов или протоколов. Ведь всё является двоичными последовательностями.
Очень рекомендую почитать спецификации PDF или EXIF. Возможно, вы даже захотите поэкспериментировать с собственной реализацией формата сериализации MessagePack, или Avro, Protobuf… Возможности безграничны!