Двійкові цифри зрештою безпосередньо відповідають транзисторам у центральному процесорі (CPU) чи оперативній памʼяті (RAM), а також тому, чи кожен із них «увімкнений» чи «вимкнений».
Низькорівневе маніпулювання бітами, яке неформально називають «bit-twiddling», особливо важливе в системних мовах.
Високорівневі мови, як-от Julia, зазвичай абстрагують більшість цих деталей. Однак повний набір операцій на рівні бітів доступний уже в самій мові.
Примітка: Щоб побачити в REPL двійковий вивід у зручному для читання вигляді, майже всі наведені нижче приклади треба обгорнути у функцію bitstring(). Це відволікає візуально, тож із більшості прикладів цю функцію прибрано.
Типи цілих чисел, знакові чи беззнакові, можна подати як рядок із 1 і 0.
julia> bitstring(UInt8(5))
"00000101"
Побітові зсуви просто пересувають усе ліворуч або праворуч на вказану кількість позицій. У типах UInt частина бітів зникає з одного кінця, а інший кінець доповнюється нулями:
julia> ux::UInt8 = 5
5
julia> bitstring(ux)
"00000101"
julia> ux << 2 # left by 2
"00010100"
julia> ux >> 1 # right by 1
"00000010"
Кожен зсув ліворуч подвоює значення, а кожен зсув праворуч ділить його навпіл (з урахуванням усічення). Це помітніше в десятковому поданні:
julia> 3 << 2
12
julia> 24 >> 3
3
Такі побітові зсуви працюють значно швидше за «справжню» арифметику, що робить цей прийом дуже популярним у низькорівневому програмуванні.
Із знаковими цілими числами треба бути трохи обережнішими.
Зсуви ліворуч відносно прості:
julia> sx = Int8(5)
5
julia> sx # positive integer
"00000101"
julia> sx << 2
"00010100"
julia> -sx # negative integer
"11111011"
julia> -sx << 2
"11101100"
Отже, зсув ліворуч для додатних знакових цілих чисел такий самий, як для беззнакових.
Відʼємні значення зберігаються у формі доповняльного коду, тобто найлівіший біт у них дорівнює 1. Із зсувом ліворуч жодних проблем, але як під час зсуву праворуч доповнювати найлівіші біти?
julia> sx >> 2 # simple for positive values!
"00000001"
julia> -sx # negative integer
"11111011"
julia> -sx >> 2 # pad with repeated sign bit
"11111110"
julia> -sx >>> 2 # pad with 0
"00111110"
Оператор >> виконує арифметичний зсув, зберігаючи знаковий біт.
Оператор >>> виконує логічний зсув, доповнюючи нулями, ніби число було беззнаковим.
Раніше, у попередньому концепті, ми бачили, що оператори && (логічне і), || (логічне або) та ! (заперечення) застосовують до булевих значень (англ. Boolean).
Існують відповідні оператори & (порозрядне і), | (порозрядне або) і ~ (тильда, порозрядне не), щоб порівнювати біти двох цілих чисел.
julia> 0b1011 & 0b0010 # bit is 1 in both numbers
"00000010"
julia> 0b1011 | 0b0010 # bit is 1 in at least one number
"00001011"
julia> ~0b1011 # flip all bits
"11110100"
julia> xor(0b1011, 0b0010) # bit is 1 in exactly one number, not both
"00001001"
Тут xor() означає виключне або і застосовується як функція.
Наш друг щойно надіслав нам повідомлення з важливою таємницею. Щоб інші не могли легко його прочитати, повідомлення зашифрували, виконавши низку операцій над бітами. Нам потрібно буде написати функції, які допоможуть розшифрувати повідомлення.
Перший крок до розшифрування повідомлення - скасувати зсув, зроблений під час шифрування, зсунувши біти назад праворуч. На подальших кроках розшифрування вважається, що нулі вставляються з лівого боку.
Реалізуйте функцію shift_back(), яка приймає значення та кількість позицій для зсуву й виконує зсув.
shift_back(0b1001, 2) # => 0b0010
Далі, деякі біти потрібно встановити в 1.
Реалізуйте функцію set_bits(), яка приймає значення та маску й повертає результат встановлення бітів у значенні в 1.
Біт зі value потрібно встановити в 1 там, де біт у mask також дорівнює 1.
Усі інші біти слід залишити без змін.
set_bits(0b0110, 0b0101) # => 0b0111
Під час шифрування деякі біти перевертаються. Щоб розшифрувати повідомлення, їх потрібно перевернути назад.
Реалізуйте функцію flip_bits(), яка приймає значення та маску.
Маска вказує, які біти у значенні перевертати.
Якщо біт у mask дорівнює 1, відповідний біт у value перевертається.
Усі інші біти залишаються без змін.
flip_bits(0b1100, 0b0101) # => 0b1001
Нарешті, є також певні біти, які завжди розшифровуються в 0.
Реалізуйте функцію clear_bits(), яка приймає значення та маску.
Біти у value потрібно встановити в 0 там, де біт у масці дорівнює 1.
Усі інші біти слід залишити без змін.
clear_bits(0b0110, 0b0101) # => 0b0010
Зареєструйтеся на Exercism, щоб вивчати й опановувати Julia, а також 35 концепцій128 вправ та справжнє наставництво від людей, і все це безкоштовно.