Go言語 ビット演算でのフラグ管理

2024/07/08に公開

近年のソフトウェア開発においては、ハードウェアリソースの増加に伴い、メモリの使用を厳密に意識する必要が減ってきました。

その結果、ビット演算を利用したフラグ管理はあまり一般的ではなくなりつつあります。

しかし、特定の状況においては、ビット演算によるフラグ管理が非常に有効であり、プログラムのパフォーマンスやメモリ効率を向上させることが可能です。

この記事では、ビット演算の基本構文と、ゲーム開発での実装例サンプルを紹介します。

基本構文

ビットAND(論理積)

対応するビットが両方とも1である場合に、1を返します。

これはフラグのチェックなどに使用されます。

a := 12 // 1100
b := 7  // 0111
result := a & b
fmt.Println(result)                    // 4
fmt.Printf("%04b in binary\n", result) // 0100 in binary

ビットOR(論理話)

対応するビットのどちらかが1である場合に、1を返します。

これはフラグを立てる際に使用されます。

a := 12 // 1100
b := 7  // 0111
result := a | b
fmt.Println(result)                    // 15
fmt.Printf("%04b in binary\n", result) // 1111 in binary

ビットXOR(排他的論理和)

対応するビットが異なる場合に、1を返します。

これはフラグを反転させたい際に使用します。

a := 12 // 1100
b := 2  // 0010
result := a ^ b
fmt.Println(result)                    // 14
fmt.Printf("%04b in binary\n", result) // 1110 in binary

ビットNOT(論理否定)

ビットNOT演算子(^)は、数値の各ビットを反転させます。

特定のビット幅内での反転を行うためにビットマスクを使用します。

a := 3       // 0011
mask := 0x0F // 1111 (4-bit mask)
var result = ^a & mask

fmt.Println("result:", result)         // 12
fmt.Printf("%04b in binary\n", result) // 1100 in binary

ビットシフト

左方向にずらす場合

指定した数、ビットを左方向にずらし、右端に0を追加します。

2のべき乗で乗算するのと同義です。

a := 3 // 0011
result := a << 2
fmt.Println(result)                    // 12
fmt.Printf("%04b in binary\n", result) // 1100 in binary

計算のイメージ

result = a * (2 * 2)

右方向にずらす場合

指定した数、ビットを右方向にずらします。

2のべき乗で徐算するのと同義です。

もし、端数が発生する場合には、小数点は切り捨てになります。
(仮にaが20の場合、2.5なので、2となります。)

a := 24 // 11000
result := a >> 3
fmt.Println(result)                    // 3
fmt.Printf("%04b in binary\n", result) // 0011 in binary

計算のイメージ

result = a / (2 * 3)

実装例サンプル

ゲームでのスキル解放を例にサンプルを作成しました。

package main

import "fmt"

// スキルの定義
const (
	SkillThunder = 1 << iota // 1 (0001 in binary)
	SkillFire                // 2 (0010 in binary)
	SkillIce                 // 4 (0100 in binary)
	SkillWind                // 8 (1000 in binary)
)

type Skills int

func (s *Skills) Unlock(skill int) {
	*s |= Skills(skill)
}

func (s Skills) IsUnlocked(skill int) bool {
	return s&Skills(skill) != 0
}

func main() {
	var s Skills

	// サンダーとアイスを解放
	s.Unlock(SkillThunder)
	s.Unlock(SkillIce)

	// サンダーとアイスが解放状態であることを確認
	fmt.Println(s)                    // 5
	fmt.Printf("%04b in binary\n", s) // 0101 in binary

	// ファイアーを解放
	s.Unlock(SkillFire)

	// サンダーが解放されているか確認
	fmt.Println(s.IsUnlocked(SkillThunder)) // true

	// ウィンドが解放されているか確認
	fmt.Println(s.IsUnlocked(SkillWind)) // false
}

まとめ

今回はゲームのスキルを例にビット演算によるフラグ管理のサンプルを作成しましたが、ビット演算は他にも多くのユースケースがあります。
特にUNIX系のファイルシステムの権限などは、より身近でイメージしやすいかもしれません。

下記にビット演算でフラグ管理を行うことのメリット、デメリットをまとめてみました。

メリット

  • メモリ効率が高い。
    • 複数のフラグを1つの整数値で管理するため、メモリ効率が高くなる。
  • シンプルに管理できる。
    • switch caseでステータスを判定するような冗長的な処理を含まずに済む。

デメリット

  • 可読性の低下
    • ビット演算に不慣れなメンバーにとっては、可読性が低下する。
  • フラグの上限数がビット数に依存する
    • uintの場合、システムに依存するが、32個または64個の制限がある。
  • 複雑な状態管理には不向き
    • true/falseで管理できる単純な状態管理には適しているが、(例えば、未完了、進行中、完了のような)複数のステータスを持つような場合は、不適切である。

採用情報

e-dashエンジニアチームは現在一緒にはたらく仲間を募集中です!
同じ夢について語り合える仲間と一緒に、環境問題を解決するプロダクトを作りませんか?

Discussion