Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rubyのビット演算ツールボックス:基本の演算子から実践的な活用例、そして魔法のようなテクニックまで

Railsアプリの開発だけを続けていれば、ビット演算子を使う機会は一生訪れないかもしれません。プログラミングを学び始めたばかりの方なら、「ビット演算」という言葉自体を聞いたことがないという方もいるでしょう。

しかし、効率性が重視されリソースが限られたシステムに興味を持った瞬間から、ビット演算は不可欠な知識になります。ネットワークプロトコル、暗号技術、Unixのファイルパーミッション、組み込みシステムなど、さまざまな分野でビット演算が広く活用されています。

さらに言えば、コンピュータがどのようにして2つの数値を足し合わせているのかを本当に理解するには、ビット演算の仕組みを把握することが欠かせません。

Rubyをはじめとする多くの言語はビット演算をネイティブでサポートしています。将来ビット演算を目にしたときに読み解けるようにするためだけでも、ぜひツールボックスに加えておきたいスキルです。

本記事では、ビット演算の基礎知識から実際の適用例、Rubyでの効果的な使い方までを解説します。それでは始めましょう。

ビット演算とは何か?

コンピュータの最下層にあるのは、1と0のみです。これらは「ビット」と呼ばれます。Rubyをはじめとするプログラミング言語で行うあらゆる操作も、最終的には1と0の列として保存されます。コンピュータ内のソフトウェアが、私たちが目にするデータと実際に保存されているデータとの間を効率的に変換しているのです。たとえば、文字列「hello」も1と0の連なりとして格納されています。

ビット演算を使うと、通常は数値として表現されるこれらのビットに直接アクセスし、何らかの操作を行うことができます。うまく活用すれば、ソフトウェアの機能をよりエレガントかつ効率的に実装できるようになります。

ビット演算には、押さえておきたい重要な特徴が2つあります。1つは情報の格納が非常に効率的であること(ビットそのものを扱うため)、もう1つは実行速度が非常に速いことです。

基本的な動作は、一組のビットに対して演算子を適用することです。また、2組のビットに対して演算子を使用することもできます。それでは、代表的な演算を見ていきましょう。

NOT(否定)演算

NOTは単項演算子で、1組のビットに対してのみ適用されます。動作は非常にシンプルで、1を0に、0を1に入れ替えるだけです。記号は~で表されます。

~101000 = 010111

AND(論理積)演算

ANDは2組のビットに対して適用される演算で、記号は&です。以下のロジックに従います。

1 & 1 = 1
1 & 0 = 0
0 & 1 = 0
0 & 0 = 0

つまり、両方が1の場合のみ結果が1になり、それ以外はすべて0になります。同じ長さの2組のビットがある場合、このロジックを各ペアに個別に適用します。

0110 AND
0111
-----
0110

Rubyでの例:

25.to_s(2)           # 11001
30.to_s(2)           # 11110
(25 & 30).to_s(2)    # 11000

OR(論理和)演算

ANDと同様に2組のビットに対して適用されるのが、ビット単位のORです。記号は|で、以下のロジックに従います。

1 | 1 = 1
1 | 0 = 1
0 | 1 = 1
0 | 0 = 0

どちらか片方でも1なら結果は1、そうでなければ0。とてもシンプルですね。より多くのビットでのOR演算を見てみましょう。

0110 OR
0111
-----
0111

Rubyでの例:

25.to_s(2)           # 11001
30.to_s(2)           # 11110
(25 | 30).to_s(2)    # 11111

実践例1:権限管理

ここまで読んで、「こんな低レベルな操作に何の意味があるのだろう?」と思った方もいるかもしれません。ネットワークプロトコルやグラフィックス、暗号技術を直接扱う予定はないという方も多いでしょう。

しかし、権限(パーミッション)を扱った経験はきっとあるはずです。権限管理こそ、ビット演算が真価を発揮する分野のひとつです。ユーザーがドキュメントに対して実行できるアクションが複数あるシステムを想像してみてください。

  • 閲覧(View)
  • 編集(Edit)
  • 削除(Delete)
  • 他のユーザーの招待(Invite)

これらのアクションを、次のような役割(ロール)としてモデル化したいとします。

  • アシスタント: ドキュメントの閲覧と編集が可能。
  • オブザーバー: ドキュメントの閲覧のみ可能。
  • オーサー: すべてのアクションが可能。

これをどうモデル化すればよいでしょうか? 任意の時点で、ユーザーが特定のロールを持っているかどうかをどう判断するのでしょうか? その答えのひとつがビット演算です。

各ユーザーについて、保持している権限を表す1組のビットだけを保存します。

(右側から数えます)
第1ビット:閲覧
第2ビット:編集
第3ビット:削除
第4ビット:他のユーザーの招待

たとえば次のようになります。

0001 = 閲覧のみ可能。
0011 = 閲覧と編集が可能。
1001 = 閲覧はできるが、編集・削除はできず、他のユーザーの招待は可能。

ユーザーごとにこの値を設定しておけば、目的の権限を持っているかどうかを非常に高速に比較できます。ここで「ユーザーがドキュメントを編集できるか」をチェックする場面を想像してみましょう。ビット単位のAND演算を使います。

# これは「ビットマスク」と呼ばれるものです。
# チェックしたい値だけを含んでいます。ここでは編集権限に対応する第2ビットです。
EDIT_PERMISSION_MASK = 0b0010

# チェック用のメソッドを簡単に定義できます
def can_edit_document?(user_permisions)
  (EDIT_PERMISSION_MASK & user_permisions) != 0
end

つまり、AND演算の結果が0以外であれば、そのビットが立っている=権限を持っていることになります。

0010 AND
1101
----
0000 == 0 なので権限なし

0010 AND
1110
----
0010 != 0 なので権限あり

同じロジックは、チェック対象のビット位置を変えることで他の権限にも適用できます。最終的に、次のような定数とメソッド群が完成します。

VIEW_PERMISSION_MASK   = 0b0001
EDIT_PERMISSION_MASK   = 0b0010
DELETE_PERMISSION_MASK = 0b0100
INVITE_PERMISSION_MASK = 0b1000

さらに、権限を動的に定義することもできます。将来的に新しい権限を追加する場合でも、簡単なビットチェックで対応可能です。

たとえば先ほど、アシスタントは閲覧と編集のみが可能だとしました。このユーザーの権限値は0011です。この値をデータベースに保存しておけば、先ほど定義したメソッドを使って、アシスタントが特定のアクションを実行できるかどうかを簡単に判定できます。

ASSISTANT_MASK = VIEW_PERMISSION_MASK | EDIT_PERMISSION_MASK
# 結果は 0011

# おまけとして、このユーザーがアシスタントかどうかを判定するメソッドも用意できます。
# Userクラスの中に定義するとよいでしょう。
def is_assistant?(user)
  (user.permissions == ASSISTANT_MASK)
end

この話に聞き覚えがあるのは当然です。Unix系OSのファイルパーミッションで一般的に使われているのが、まさにこのアプローチだからです。

実践例2:チームのポジション管理

もう少しビット演算を楽しんでみましょう😉。

比較的一般的なもうひとつの応用例が、スポーツチームのポジションや企業の職種の管理です。ここでは話をシンプルにするために、バスケットボールチームを例にします。

バスケットボールには試合中に5つのポジションがあります。

  • ポイントガード
  • シューティングガード
  • スモールフォワード
  • パワーフォワード
  • センター

各ポジションに1組のビットを割り当てます。

00001 ポイントガード
00010 シューティングガード
00100 スモールフォワード
01000 パワーフォワード
10000 センター

Rubyで書くとこうなります。

POINT_GUARD_POSITION    = 0b00001
SHOOTING_GUARD_POSITION = 0b00010
SMALL_FORWARD_POSITION  = 0b00100
POWER_FORWARD_POSITION  = 0b01000
CENTER_POSITION         = 0b10000

POINT_GUARD_POSITION | SHOOTING_GUARD_POSITION | SMALL_FORWARD_POSITION | POWER_FORWARD_POSITION | CENTER_POSITION # = 31

これで興味深い操作ができるようになります。たとえば、チーム全員が揃っているかどうかを次のようにチェックできます。

# p1...p5 はそれぞれの選手のポジション
is_full_team_present = (p1 | p2 | p3 | p4 | p5 == 31)

なぜこうなるのでしょう? ビット単位のOR演算を行うと、すべてのポジションが揃っていれば結果は11111になります。

# OR演算
00001 |
00010 |
00100 |
01000 |
10000
-----
11111

そして11111は31です。2^0 + 2^1 + 2^2 + 2^3 + 2^4 = 31 となるからです。

これは厳密にはビット演算とは関係ありませんが、このようなデータモデリングをしておくと、2人の選手を入れ替えられるかどうかの判定も非常にシンプルになります。

def can_be_exchanged?(player1, player2)
  player1.position == player2.position
end

XOR(排他的論理和)

2組のビットに対して行えるもうひとつの演算がXORです。記号は^です。

XORは「排他的論理和」を意味し、以下のロジックに従います。

1 ^ 1 = 0
1 ^ 0 = 1
0 ^ 1 = 1
0 ^ 0 = 0

つまり、2つのビットのどちらか一方だけが1の場合に結果が1となり、両者が等しい場合は0になります。

XORは、ある数値をそれ自身と比較するアルゴリズムなどで使われます。x ^ x = 0 となる性質を利用するのです。

シフト演算

これは興味深いグループの演算です。ビットの集合の中で、ビットを左右に「移動」させます。具体的に説明しましょう。

ビットシフトでは、ビットを左または右のどちらかの方向へ移動(シフト)させます。

00010111 左シフト
<-------
00101110
10010111 右シフト
------->
11001011

ビットはn回移動できます。Rubyで数値5に左シフトを2回適用した例がこちらです。

5.to_s(2) # 101
(5 << 2).to_s(2) # 10100

ご覧のとおり、左シフトは<<で表されます。右シフトは>>を使います。

5.to_s(2) # 101
(5 >> 2).to_s(2) # 1

この場合、結果は1だけになります。101のうち右端の「0」と「1」が破棄されたためです。

右シフトによる2での除算

ビットシフトの興味深い点は、数学的な演算を代わりに行えることです。かつてはこの方法が高速だったのですが、現代ではゲーム開発など、リソースの制約が厳しい環境で働くプログラマーがほぼ専ら利用するテクニックになっています。

数値に右シフトを適用すると、2で割った結果が得られます。

10.to_s(2)        # 1010
(10 >> 1).to_s(2) # 101
10 >> 1           # 5

左シフトによる2倍の乗算

同様に、左シフトで2倍にすることもできます。

10.to_s(2)        # 1010
(10 << 1).to_s(2) # 10100
10 << 1           # 20

奇数・偶数の高速判定

ビット演算には、非常にシンプルかつ高速で分かりやすいテクニックもあります。

数値と1だけのAND演算を考えてみましょう。これは、環境に応じた桁数の0の並びと1とのAND演算です。まず2で試してみます。

2 = 00000010 &
    00000001
-------------
    00000000

次に4で試してみます。

4 = 00000100 &
    00000001
-------------
    00000000

では5はどうでしょう?

5 = 00000101 &
    00000001
-------------
    00000001

今度は1になりました。この意味がお分かりでしょうか?

1とのAND演算では、数値が偶数なら結果は0、奇数なら1になります。この性質を利用すれば、Rubyで簡単にメソッドを作成できます。

def is_odd?(number)
  number & 1
end
def is_even?(number)
  is_odd?(number) == 0
end
# または:
def is_even?(number)
  (number & 1) == 0
end

さらに深く知りたい方や、ビットの世界で驚きの体験をしてみたい方は、オンラインで公開されているビット演算ハックのコレクションをぜひチェックしてみてください。思わぬトリックが見つかるはずです。

まとめ

ビット演算は、初めて目にすると理解が難しく感じられます。しかし一度慣れてしまえば、既存のプロジェクトや将来のプロジェクトでビット演算に直面したときに、落ち着いて対応できるようになるでしょう。さらに、コードの問題に対するソリューションを設計する際の新たな武器にもなります。

  1. Windows10のヒントとコツ

    Windows 10は、最初の1年間はWindows8.1およびWindows7ユーザーへの無料アップグレードとして提供されており、Microsoftによって10年間サポートされます。新しいオペレーティングシステムには、いくつかの優れた新機能など、提供できるものがたくさんあります。 Windows10のヒントとコツを確認してみましょう それはあなたがそれを最大限に活用するのに役立ちます。 Windows10のヒントとコツ 初心者の方は、まずWindows10PCの基本的な使用方法のチュートリアルをお読みください。 1]Windows10を希望どおりに動作させる コントロールパネ

  2. Windows 10を使いこなすためのヒントとコツ53選

    Windows 10は、Windows 7およびWindows 8.1ユーザー向けに最初の1年間は無料アップグレードとして提供され、Microsoftによって10年間のサポートが約束された OS です。新しいオペレーティングシステムには、魅力的な新機能が数多く搭載されています。この記事では、Windows 10を最大限に活用するためのヒントとコツを53個ご紹介します。※一部の機能は、その後の大型アップデートで仕様が変更されたり提供が終了したりしている場合があります。あらかじめご了承ください。 Windows 10のヒントとコツ 初心者の方は、まずWindows 10パソコンの基本的な使い方に