Побитовые операции

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

Где применяются?

Побитовые операции широко применяются при работе с битовыми полями (флагами) и битовыми масками. Битовые поля позволяют максимально эффективно хранить некоторые состояния, имеющие только два значения: «включен» и «выключен». Access Control List (ACL) — хороший пример битового поля, использующийся для управления доступом к объекту. Например, в Linux и Mac OS каждый файл в системе имеет свой ACL, определяющий полномочия пользователя по чтению, записи или запуску файла. Битовые поля позволяют хранить несколько значений, не расходуя лишнюю память (по одному биту на каждое значение, вместо 4-х или 8-ми). А битовые маски (и побитовые операции), в свою очередь, используются для извлечения необходимых данных из битовых полей.

Довольно трудно обойтись без побитовых операций при реализации сетевой коммуникации. Побитовые операции здесь используются для проверки чек-сумм, определения битов окончания операции, для объеденения команд и данных, сериализации и т.д. и т.п.

Не получится не использовать побитовые операции и при реализации алгоритмов компресии и шифрования. Если взглянуть на любой алгоритм сжатия (например Deflate) или шифрования (например, AES), то мы увидим, что в описании довольно часто всплывают именно биты.

Однозначно существуют области программирования, в которых без побитовых операций просто невозможно обойтись. Сюда можно отнести программирование микроконтроллеров, взаимодействие с реальными устройствами, написание драйверов для них. Также сюда относится реализация программ, эмулирующих реальные устройства, такие,как эмуляторы CD-ROM, игровых приставок или виртуальных компьютеров.

Побитовые операции являются частым гостем в случаях, когда требуется использовать разного рода оптимизации и микрооптимизации кода. Банальный пример:  2 << 4 в большинстве случаев быстрее. чем Math.Pow(2, 5).

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

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

Как работают побитовые операции?

Чтобы понять, как работают побитовые логические операции, нужно знать, что каждая операция работает с битами числа. И, в зависимости от типа битовой операции, действие проиходит над парой битов (в случае бинарной битовой операции) или над одним битом (в случае унарной битовой операции). И уже из результирующих битов составляется итоговое значение — результат битовой операции. Таким образом, побитовая логическая операция — это логическая операция, действие (в данном случае логическая операция, такая как конъюнкция, дизъюнкция или инверсия) которой применяется к каждому биту двоичного предстваления операнда.

 

Начнем, пожалуй, с самой простой  побитовой операции — побитовое отрицание (NOT) или побитовая инверсия. В подобных языках программирования этой операции соответствует символ  ~ . Пример:

Мы инвертировали каждый бит числа, т.е. если бит был включен (1), то результатом инверсии будет выключенный бит (0). Стоит отметить, что эта операция является обратимой, т.е.  ~(~c) = c .

 

Следующая логическая побитовая операция — побитовое И (AND). Это бинарная побитовая операция, которая работает с двумя наборами битов одинаковой длины. Если один из наборов бит содержит меньше бит, чем другой, то он дополняется незначащими нулями слева. В подобных языках программирования этой операции соответствует символ  & . Как видно из названия, в качестве логической операции используется конъюнкция. Другими словами, результатом побитовой операции И с двумя битами будет 1, тогда и только тогда, когда оба оперенда (оба бита) выставлены в 1; во всех остальных случаях результатом будет 0. Легко запомнить: если первый бит равен 1 И второй бит равен 1, то результатом будет 1, иначе 0. Рассмотрим пример:

 

Побитовое ИЛИ (OR) — это также бинарная логическая операция, требующая двух одинаковых по длине наборов бит. В С-подобных языках программирования для этой операции используется символ  | . По своей сути, эта операция эквивалентна применению логической операции дизъюнкции к каждой паре бит бинарного представления операндов. Результатом побитовой операции ИЛИ с двумя битами будет 1, если хотя бы один из битов выставлен (равен 1), иначе будет 0. Пример:

 

Последняя побитовая логическая операция — исключающее ИЛИ (XOR). Это побитовая логическая операция, также требующая два набора бит одинаковой длины, результатом которой будет в случае, если оба бита равны между собой (т.е. оба выставлены или оба не выставлены), а в противном случае будет 1. В C- подобных языках для этой операции используется символ  ^ . Стоит отметить, что эта операция является обратимой, т.е.  (a ^ b) ^ b = a . И пример:

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

 

Теперь самое время перейти к последней паре побитовых операций. Это операции побитового сдвига.

Побитовый сдвиг влево — это бинарная побитовая операция, результатом которой является сдвиг всех битов влево (от младшего бита к старшему) на заданное число (второй операнд). В C-подобных языках для обозначения данной операции используют символ  << . По своей сути, эта операция эквивалента умножению на 2k при сдвиге на k бит. Пример:

 

Побитовый сдвиг вправо — это бинарная побитовая операция, результатом которой является сдвиг всех битов вправо (от старшего бита к младшему) на заданное число (второй операнд). В C-подобных языках для обозначения данной операции используют символ  >> . Эквивалентом данной операции является целочисленное деление на 2k при сдвиге на k бит. Пример:

 

 

Вот мы и рассмотрели основные побитовые операции. Как видите, ничего особо сложного в них нет. Давайте рассмотрим несколько примеров, как можно использовать побитовые операции в повседневной жизни.

 

Примеры использования побитовых операций

Пожалуй, наиболее ярким примером использования побитовых операций является использование битовых полей и масок для использования битов в качестве значений, имеющих два состояния (включен или выключен) вместо каких-то символов, чисел или отдельных Boolean значений. Для этого попробуем реализовать систему прав доступа к файлам, подобную Unix-образным ОС. Первое, что нам потребуется, — это определить перечисляемый тип с флагами доступа на чтение, запись или запуск. Рассмотрим пример на языке C#:

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

Теперь мы можем «создать» файл и назначить ему некоторые права:

Благодаря тому, что мы пометили перечисляемый тип аттрибутом  FlagsC# понимает, что работает с битовым полем и в консоль выводит распарсенное значение, что очень удобно. Как же нам теперь извлечь необходимые данные из поля  permissions? Элементарно, и в этом нам, опять-таки, пригодятся побитовые операции. Давайте напишем функцию, которая проверяет, выставлен ли заданный бит.

С помощью побитовой операции И мы проверяем, установлен ли соответствующий бит маски. И результат этой операции сравниваем с заданной маской. Если необходимый бит установлен, то в результате мы получим значение, равное значению маски. Рассмотрим этот момент чуть подробнее:

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

Пробуем:

Как видите. все работает.

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

Этот код работает аналогично коду выше, в случае, если значение  mask состоит только из одного бита. А вот если оно представляет из себя составное значение, то этот код вернет  true , если хотя бы один из битов маски в проверяемом значении выставлен. В этом и есть основное отличии этого кода от кода выше, который вернет  true , только если все биты маски выставлены в проверяемом значении. Пример:

Этот момент стоит учитывать.

А как же нам теперь изменить какое-то из разрешений? Ведь пока что мы только задавали разрешения скопом. Получается, самое время рассмотреть эту возможность. Здесь нам, опять-таки, помогут побитовые операции. Давайте напишем функцию, которая будет выставлять значение необходимого бита в необходимое значение:

Добавим соответствующие сеттеры:

И пробуем новую функцию в деле:

Рассмотрим реализацию функции  SetPermissionBit чуть подробнее.

В случае, если мы хотим выставить некоторый бит, нам достаточно операции побитового ИЛИ и, вне зависимости от текущего состояния бита, после нее соответствующий бит будет выставлен. Несколько сложнее обстоит дело, если нам необходимо выключить соответствующий бит. Для этого мы сначала инвертируем маску, применяя операцию побитового отрицания. После чего все невыставленные биты маски будут включены, а все выставленные биты (в данном случаи один бит) — выключены.  И далее применяем побитовую операцию И с инвертированной маской. Тем самым мы не модифицируем ни один из выставленных битов; а также возвращаем в качестве целевого бита 0. Что, собственно, нам и требовалось. То же самое, но по действиям:

Таким образом, к текущему моменту мы рассмотрели основные моменты работы с битовыми полями.

Довольно часто побитовые операции позволяют оптимизировать код. Давайте рассмотрим такой пример. Предположим, у нас стоит задача написать функцию  bool IsPowerOfTwo(int x) , определяющую, является ли переданное число степенью двойки. Простым решением, которое приходит первым в голову, является следующий алгоритм: делим число  x на 2 до тех пор, пока результат деления —  четное число. И если мы закончим цикл с числом, равным 1, то  x является степенью двойки, иначе нет. Отдельно стоит учесть, если  x равно 0:

И все, вроде бы, с этой функцией хорошо. Она, кажется, даже работает. Но есть у нее значительный минус — ее сложность. Если присмотреться к этой функции, то ее сложность: O(logN). Это, вроде бы, неплохо, но можно (и нужно!) лучше. В этом нам и помогут побитовые операции:

Сложность этой реализации O(1)! Рассмотрим пару примеров:

Здорово, не правда ли?) Таких, и гораздо более сложных, трюков достаточно много.

 

Давайте попробуем побитовую операцию исключающее ИЛИ в деле. С помощью этой побитовой операции мы попробуем обменять значение двух переменных местами без использования дополнительной переменной. И для этого воспользуемся свойством обратимости побитовой операции исключающее ИЛИ —  (a ^ b) ^ b = a :

Выполним функцию  Swap вручную:

 

 

И последний пример на сегодня. Давайте рассмотрим цвета в формате RGB. Допустим, у нас есть цвет в формате RGB — 0xFF007F. И нам необходимо извлечь значение каждого из каналов. Как нам это сделать? Кажется, тут не обойтись без старых знакомых — побитовых операций.

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

 

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

Добавить комментарий