はじめに
計算処理を特に高速化したいときや、コンピュータで遊んでみたいときに、CPUに対する命令を直接書いてみたくなるかもしれません。そのような時に、Crystalでは、インラインアセンブリを使うことができます。
CrystalはLLVMコンパイラ基盤の上に構築されたプログラミング言語ですので、LLVMの多くの機能を使うことができます。Intrinsic 関数を使ったり、asm 構文を記述することができます。
asm 構文
Crystal は asm キーワードを使ってインラインアセンブリを記述できます。
asm("template" : outputs : inputs : clobbers : flags)
-
template- LLVM統合アセンブラの構文に従ったアセンブリコード -
outputs- 出力オペランド -
inputs- 入力オペランド -
clobbers- 破壊されるレジスタ -
flags- オプションフラグ
asm() の内部をコロン : で区切る構文はCrystal言語の文法としては完全に異質ですが、これはGCCのインラインアセンブリ構文真似たものになっているようです。
実際の例を見ていきます。
NOP命令
asm("nop")
値を出力オペランドに設定
dst = uninitialized Int32
asm("mov $$10, $0" : "=r"(dst))
p dst # 10
紛らわしいですが $$10 は10の即値リテラルです。$0 はオペランドプレースホルダです。
uninitialized Int32 を使っていますが、dst = 0 と初期化しても動作します。
入力オペランドを使用する
src = 10
dst = 0
asm("mov $1, $0" : "=r"(dst) : "r"(src))
p dst # 10
複数の入力オペランドを使用する
a = 10
b = 20
c = uninitialized Int32
asm("add $2, $0" : "=r"(c) : "0"(a), "r"(b))
p c # 30
複数の出力オペランドを使用する
dst1 = uninitialized Int32
dst2 = uninitialized Int32
asm("
mov $$10, $0
mov $$20, $1" : "=r"(dst1), "=r"(dst2))
p dst1
p dst2
Intel構文を使用する
Intel構文を使うこともできます。
dst = uninitialized Int32
asm("mov dword ptr [$0], 10" :: "r"(pointerof(dst)) :: "intel")
p dst
Intrinsic
比較的単純な命令に関しては、LLVMがIntrinsicを提供しています。Intrinsicは最適化の対象になり、プラットフォームごとに異なる命令を書く必要がありません。また、インタープリターも対応しています。ただし、ほとんどの計算についてすでにCrystalの標準ライブラリのメソッドが用意されており、Intrinsicを直接使ってもパフォーマンスが向上するケースは少ないかもしれません。
Crystalで利用できるIntrinsic命令は、Intrinsics モジュールで定義されています。
利用可能なIntrinsic命令一覧
memcpy - メモリコピー
src = Slice(UInt8).new(10) { |i| i.to_u8 }
dest = Slice(UInt8).new(10, 0_u8)
Intrinsics.memcpy(dest, src, 10, is_volatile: false)
puts "Copied: #{dest}"
memmove - 重複可能なメモリ移動
buffer = Slice(UInt8).new(10) { |i| i.to_u8 }
Intrinsics.memmove(buffer.to_unsafe + 3, buffer.to_unsafe, 5, is_volatile: false)
puts "Moved: #{buffer}"
memset - メモリ初期化
buffer = Slice(UInt8).new(10, 0_u8)
Intrinsics.memset(buffer, 0xFF_u8, 10, is_volatile: false)
puts "Set: #{buffer}"
debugtrap - デバッガトラップ
Intrinsics.debugtrap
pause - CPUポーズ(x86/x64およびAArch64対応)
Intrinsics.pause
これらはCrystalで並列計算を制御する Mutex や SpinLock の実装に使われているらしい。
read_cycle_counter - CPUサイクルカウンタ読み取り
cycles = Intrinsics.read_cycle_counter
puts "Cycles: #{cycles}"
以下のようにループを使えば、CPUのサイクルカウンタが増えていく様子を見ることができます。
loop do
cycles = Intrinsics.read_cycle_counter
puts "Cycles: #{cycles}"
sleep 1.second
end
ビット反転(bitreverse
-
bitreverse8,bitreverse16,bitreverse32,bitreverse64,bitreverse128
value = 0b1101001_u8
result = Intrinsics.bitreverse8(value)
puts "Reversed: #{result.to_s(2)}" # 10010110
バイトスワップ(bswap)
-
bswap16,bswap32,bswap64,bswap128
value = 0x12345678_u32
result = Intrinsics.bswap32(value)
puts "Swapped: 0x#{result.to_s(16)}" # 0x78563412
ポップカウント(popcount)
-
popcount8,popcount16,popcount32,popcount64,popcount128
value = 0b11010110_i32
count = Intrinsics.popcount32(value)
puts "Bit count: #{count}" # 5
先頭ゼロカウント(countleading)
-
countleading8,countleading16,countleading32,countleading64,countleading128
value = 0b00001111_i32
count = Intrinsics.countleading32(value, false)
puts "Leading zeros: #{count}" # 4
末尾ゼロカウント(counttrailing)
-
counttrailing8,counttrailing16,counttrailing32,counttrailing64,counttrailing128
value = 0b11110000_i32
count = Intrinsics.counttrailing32(value, false)
puts "Trailing zeros: #{count}" # 4
終わりに
Crystalは日本語の情報が多くないですが、DeepWikiに聞けば大抵のことは教えてもらえます。上記の記事もDeepWikiに聞いた情報をもとに、コードが実際に動くことを検証しながら、記事に落とし込んでいます。おすすめです。
この記事は以上です。よいCrystalライフを!