Научитесь писать оптимизированные смарт-контракты с использованием бинарной логики
Двоичные выражения являются основой самого программирования. Каждый процесс под капотом любого языка программирования сводится к манипулированию булевыми битами информации — равными либо true, либо false. Так почему бы нам не разобраться в языке процессоров и не сделать наш код более эффективным и быстрым? Эта статья прольет свет на побитовые операции в языке Solidity и варианты их использования: как примитивные, так и сложные.
Прежде чем двигаться дальше, я настоятельно рекомендую вам ознакомиться с подписанным бинарным принципом Solidity — модификация дополнений со знаком 2. Это концепция представления отрицательных двоичных чисел, созданная для ускорения вычислений. Важно понять, как образуется отрицательное двоичное число.
Двоичное преобразование
Каждый тип значения в Solidity основан на двоичных данных и может быть (визуально) преобразован в 1 и 0 с помощью внешних источников или написанных чистых функций Solidity. Таким образом, бинарная алгебра может применяться как к шестнадцатеричным байтамданным, так и к десятичным целым числам без знака или со знаком (адрес легко преобразуется в байты).
И ( & ) — союз
function and(int bar, int foo) public pure returns(int) { /* Performs bitwise AND to two integers (their binary form). Example: bar: 93 (10) = 01011101 (2) foo: 26 (10) = 00011010 (2) bar & foo: 00011000 (2) = 24 (10) */ return bar & foo; }
Результатом конъюнкции является поразрядное умножение двух двоичных выражений.
Характеристики
- Соединение двух одинаковых целых чисел всегда не больше любого из них.
- Отрицательное целое число, соединенное с положительным, всегда будет приводить к тому, что неотрицательное число будет лежать между ними.
ИЛИ ( | ) — дизъюнкция
function or(int bar, int foo) public pure returns(int) { /* Performs bitwise OR to two integers (their binary form). Example: bar: 93 (10) = 01011101 (2) foo: 26 (10) = 00011010 (2) bar | foo: 01011111 (2) = 95 (10) */ return bar | foo; }
Побитовое ИЛИ эквивалентно логическому ИЛИ ( || ), применяемому к каждой паре двух двоичных выражений в пределах одной цифры.
Характеристики
- Дизъюнкция двух одинаковых целых чисел всегда не меньше любого из них.
- Дизъюнкция с отрицательным целым числом всегда будет отрицательной.
НЕ ( ~ ) — отрицание
function not(int foo) public pure returns(int) { /* Performs bitwise negation. Example: foo: 93 (10) = 01011101 (2) ~foo: 10100010 (2) = 162 (10) */ return ~foo; }
Унарный оператор, инвертирующий каждый бит двоичного выражения.
Характеристики
- Отрицание меняет знак целого числа.
- Соединение целого числа и его отрицания всегда false.
- Дизъюнкция целого числа и его отрицания всегда истина.
XOR (^) — исключающая дизъюнкция
function xor(int bar, int foo) public pure returns (int) { /* Performs bitwise XOR to two integers (their binary form). Example: bar: 93 (10) = 01011101 (2) foo: 26 (10) = 00011010 (2) bar ^ foo: 01000111 (2) = 71 (10) */ return bar ^ foo; }
Двоичная операция, при которой результирующий бит равен 1, только если ОДНА из двух цифр дизъюнктного выражения равна 1.
Характеристики
- Любое целое число, подвергнутое XOR с нулем, приводит к неизменному целому числу.
- Любое целочисленное XOR с самим собой дает ноль.
Левый "шифт ( << )
function leftShift(int number, uint positions) public pure returns(int) { /* Performs left bit shift of `number` by `position` slots. Examples: int8(98) << 2 <--> 01100010 << 2 == 10001000 <--> 10001000 == -120 int(98) << 2 <--> 00...0011100010 << 2 == 00...001110001000 <--> 00...001110001000 == 392 == 98 * 2 ^ 2 int8(-17) << 3 <--> 11101111 << 3 == 01111000 <--> 01111000 == 120 int(-17) << 3 <--> 11...1111101111 << 3 == 11...1101111000 <--> 11...1110001000 == -136 == -17 * 2 ^ 3 */ return number << positions; }
Применение оператора сдвига влево к двоичному выражению 0b1001010 << k
приводит к смещению каждого его бита на k
слотов влево.
Характеристики
- Операция сдвига влево не является циклической (как вы можете ожидать), а это означает, что если биты сдвинуты на размер своего контейнера, они не повторяются в наименьших цифрах.
- Вместо этого свободные битовые слоты после сдвига заполняются нулями.
- Запрещен сдвиг на отрицательное число (поэтому в примере функция принимает параметр uint position).
- Может вызвать переполнение.
Сдвиг вправо ( ›› )
function rightShift(int number, uint positions) public pure returns(int) { /* Performs right bit shift of `number` by `position` slots. Examples: int*(89) >> 2 <--> 00...001100010 >> 2 == 00...0000011000 <--> 00...0000011000 == 24 == floor(98 / 4) int*(-17) >> 3 <--> 11...11101111 >> 3 == 11..11111101 <--> 11..11111101 == -3 */ return number >> positions; }
Применение оператора сдвига вправо к двоичному выражению 0b1001010 >> k
приводит к смещению каждого его бита на k
слоты вправо.
Характеристики
- Свободные слоты после применения сдвига заменяются первичным битом (знаковым битом) двоичного выражения, а это означает, что запасные биты после сдвига отрицательного числа заполняются единицами, тогда как в положительных будут 0.
- Запрещен сдвиг на отрицательное число (поэтому в примере функция принимает параметр uint position).
Поскольку результат двоичного сдвига усекается, проверки переполнения никогда не выполняются.
Случаи использования
Поскольку побитовые операции связаны с простыми действиями над бинарными выражениями, их варианты использования чреваты экономией газа и более эффективным потреблением памяти.
Даже странно
Побитовое И может помочь нам решить, является ли число четным или нечетным, без использования более дорогой операции по модулю.
function isEven(uint number) public pure returns(bool) { return number & 1 == 0; } function isEven(int number) public pure returns(bool) { if(number < 0) return number & 1 != 0; return number & 1 == 0; }
Соединение любого целого числа с 1 вернет последний бит целого числа (либо 0, либо 1), поэтому приводится bool. em> тип будет иллюстрировать это целое число четным или нечетным. Обратите внимание, что если `number` отрицательное, то приведение типа bool к его последнему биту приведет к противоположности его делимости на 2 с учетом правил дополнения до 2 со знаком.
Умножение
Сдвиг влево number << k
эквивалентен number * 2 ** k
.
function multiply(int number, uint k) external pure returns(int) { return number << k; }
Это следствие свойства преобразования двоичных чисел в десятичные. Например, 0b101001 = 2**0 + 2**3 + 2**5
. Сдвинем его влево на 2 слота:0b10100100 = 2**2 + 2**5 + 2**7 = 4 * (2**0 + 2**3 + 2**5)
. Получаем исходное утверждение, умноженное на 4, то есть 2 ** 2.
Разделение
Сдвиг вправо number >> k
эквивалентен floor(number / 2**k)
, где floor()
преобразует свой параметр в меньшее целое число.
function divide(int number, uint k) external pure returns(int) { return number >> k; }
Этот вариант использования также основан на свойствах преобразования и является самым дешевым и надежным способом деления любого числа на степень 2.
Обратите внимание, что деление по умолчанию округляется до нуля, тогда как деление сдвига округляется до наименьшего.
Битовые флаги
Битовые флаги — это сложный способ составления переменных состояния и обеспечения эффективности и скорости операций с ними.
Давайте рассмотрим пример субсидирования упрощенного контракта, который финансирует соответствующих получателей по их запросу.
constructor() { // masks: params["male"] = 1 << 0; // 0000000001 (2) params["female"] = 1 << 1; // 0000000010 (2) params["single"] = 1 << 2; // 0000000100 (2) params["married"] = 1 << 3; // 0000001000 (2) params["chlid-free"] = 1 << 4; // 0000010000 (2) params["1 kid"] = 1 << 5; // 0000100000 (2) params["2 kids"] = 1 << 6; // 0001000000 (2) params["employed"] = 1 << 7; // 0010000000 (2) params["freelance"] = 1 << 8; // 0100000000 (2) params["toRecieve"] = 1 << 9; // 1000000000 (2) CEO = msg.sender; }
Будет сохранен список параметров в mapping (string => uint) params
, где значение uint
равно степени двойки. Это оставляет нам группу битовых флагов, которые мы можем комбинировать для описания получателей, также использующих mapping (address => uint) recipients
. Такой подход также позволяет создавать битовые маски, которые используются в качестве требования к фонду.
function assignParams(address applicant, string[] memory qualities) external { require(msg.sender == CEO, "you dont have permission!"); for(uint i; i < 0; i++) { recipients[applicant] ^= params[qualities[i]]; } }
Позволит генеральному директору контракта Subsidizer назначать качества кандидатам с помощью XOR. Поэтому пример записи в отображении recipients
будет выглядеть как 0xAbC...5f9 -> 713 (1011001001 (2))
, что означает, что пользователь с адресом 0xAbC...5f9
является работающим женатым мужчиной с двумя детьми, который может запросить фонд. Эксклюзивный дизъюнктивный подход позволяет отзывать роли.
function getFunds() external { uint requirement = params["female"] | params["married"] | params["2 kids"] | params["toRecieve"]; uint receiverRoles = recipients[msg.sender]; require((receiverRoles != 0) && (receiverRoles & ~(requirement) == 0), "access denied"); recipients[msg.sender] -= 2 ** 9; payable(msg.sender).transfer(sum); }
Эта субсидия предназначена для замужних безработных женщин с двумя детьми (тогда маска 1001001010
). Проверит, удовлетворяет ли получатель требованиям, используя побитовое отрицание и конъюнкцию. Давайте посмотрим, должен ли наш работающий женатый мужчина получить фонд: инвертируя требование, мы получаем 0110110101
, а соединение с маской заявителя равно 0010000001
, что означает, что он не удовлетворяет требованиям, потому что он работает.
Затем мы должны отменить возможность запроса средств еще раз, вычитая 2**9
из ролей получателя. По законам булевой алгебры это превратит десятый бит из 1 в 0. После этого мы можем осуществить перевод средств получателю без риска повторного входа.
Заключение
Побитовые операции могут быть очень полезны в любом языке программирования, поскольку они могут ускорить вычисления и уменьшить сложность кода. Но для программистов Solidity особенно важно ознакомиться с ними, потому что более эффективный код может привести к огромной экономии газа как при развертывании, так и при взаимодействии.
Примеры договоров с пояснениями можно найти здесь.