qlang(quantum language) with LLVM
qlangというRISC-V 量子向けの独自のプログラミング言語をLLVM基盤上で開発していく。
前にgccで行った改造内容から次のことが分かった。完全に同一のC言語のソースコードについて、
- RISC-V向け量子向けGCCでコンパイルして実行できる。
- LLVM(clang)でフロントエンド=>バイトコードにコンパイル、RISC-V GCCで結合出来る。
つまりRISC-V 量子改造したgccを使ってコンパイルをしても、LLVMを経由してコンパイルをしても同一のソースコードで動作させる事ができている(当然といえば当然だけど)。もちろん、数あるLLVM基盤のプログラミング言語もビルドしてRISC-Vエミュレータ上で動作させる事が出来ることも確認している。例えばbrainf*ckのような言語でも、RISC-V量子向けエミュレータ上でコンパイル&動作することが出来ている。
GCCに特殊な言語的な表現やライブラリを読み込まなくても、RISC-V量子向けの実装をGCCに対して行ったことで、量子向けに自由度が高くコード表現を出来てGCCでもLLVMでコンパイル&実行できるようになった、という事になる。
■ qlang
量子コンピュータ向けのプログラミング環境は既に存在している物がある。Q#はMSが開発している環境になる。他にもQiskitはIBMというように、各社で量子向け言語の取り組みがある。
LLVMについては最近だとRust(Firefox)は有名だし、clangでコンパイルする物ではiPhone(swift/llvm)、Android(NDK toolchain)も非常に有名ですね。
C/C++/C#(.NET)のような特定言語向けの環境や独自言語で量子プログラミングを行うというのも良いんだけど、LLVMという基盤上で動作させる事ができると、言語の種類や独自言語であってもコンパイルを行って動作させる事ができるという高い自由度を持つことが出来る。またLLVMの強い最適化も使える(カモ)という予想も出来る。
このようにLLVMを使うと、言語的な表現を柔軟に出来ることと、モダンなC++のクラスを使える、強力な最適化を行える、言語開発に必要なパーツが揃っているといった多くの利点を享受して言語開発を行うことが出来る。
つまり、スクラッチでゼロから独自のプラットフォーム・言語を開発するより、LLVMという強力な基盤の上で開発を行った方が良い。例えるなら、MSが.NET上でQ#という事なら、こっちはLLVM上でqlangということになる。
という事で、ここからは量子向けのプログラミング言語 = qlang をLLVMを使って開発を行っていく。LLVM上で動作する量子向けのプログラミング言語は調べてみたところ、現在のところは全く無いため新しい取組になる。LLVMを使う利点は上記の通り。まぁ、この辺のアーキテクチャ・言語表現・プログラミングをやっている人にしかピンと来ないというのも若干泣けてくる所はあるけど。
■ qlang 基本構造
いまのところ次のような言語表現をしようとしている。
func main() { qint x, y; var z; x = 0; // initilize y = 1; x = y; // quantum teleportation z = 0; if x == 1 { write x; } while z <= 10 { write z; z = z + 1; } }
"qint : 量子向けの変数" ということで、基本的な初期化・量子テレポーテーションを簡単に行えるようにする。言語実装としてはナウで良い感じなgolang風にしている。2019年にエンジニアが学びたいプログラミング言語のNo.1らしいけど、自分は普通にgolangは昔からちょくちょく使っていて好きな言語の一つだったりする。
いまのところ、言語表現をLLVMで行ってコンパイル・RISC-V量子エミュレータ上で動作させる所まで行って、実際に"qlang test.q"のようにコンパイル&動作させる所まで出来ている。あとはフロントエンドとバックエンドのつなぎをすればOKになる(コードはまだprivate repositoryで進めていて、このプロジェクトが終わったら公開されると思われ)。
■ qlang のコード分離
普通に開発を行ってもLLVMでqlang言語は出来るのだろうけど、量子側のコードとの分離という面白い問題設定が出てきて、少し行ってみることにした。というのは、LLVMによるIR最適化と量子側の最適化を別々に行うことが出来る方が良いだろうという指摘があった。確かに既存の古典コードの資産を活かしつつ、量子側のコードを組込み&最適化できるような構造はおしゃれな感じがする。
ということで、単純にclangでシンプルな古典コードをビルドして、最適化有無の効果を見てみることにする。
■ LLVMの最適化の効果
# 基本サンプルコード : test.c#include <stdio.h> int main() { int i = 0; for (i = 0; i < 10; i++); printf("%d\n", i); return i; }
■ 未最適化コード
# 未最適化コード(LLVM IR) # clang -target riscv64-unknown-linux-gnu -emit-llvm -S -o test.ll test.cdefine dso_local signext i32 @main(i32 signext %0, i8** %1) #0 { %3 = alloca i32, align 4 %4 = alloca i32, align 4 %5 = alloca i8**, align 8 %6 = alloca i32, align 4 %7 = alloca i32, align 4 store i32 0, i32* %3, align 4 store i32 %0, i32* %4, align 4 store i8** %1, i8*** %5, align 8 store i32 0, i32* %6, align 4 store i32 0, i32* %7, align 4 store i32 0, i32* %7, align 4 br label %8 8: ; preds = %12, %2 %9 = load i32, i32* %7, align 4 %10 = icmp slt i32 %9, 10 br i1 %10, label %11, label %15 11: ; preds = %8 br label %12 12: ; preds = %11 %13 = load i32, i32* %7, align 4 %14 = add nsw i32 %13, 1 store i32 %14, i32* %7, align 4 br label %8 15: ; preds = %8 %16 = load i32, i32* %7, align 4 %17 = call signext i32 (i8*, ...) @printf(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i64 0, i64 0), i32 signext %16) %18 = load i32, i32* %7, align 4 ret i32 %18 } declare dso_local signext i32 @printf(i8*, ...) #1# 未最適化 RISC-V アセンブリコード
main: # @main # %bb.0: addi sp, sp, -48 sd ra, 40(sp) sd s0, 32(sp) addi s0, sp, 48 sw zero, -20(s0) sw a0, -24(s0) sd a1, -32(s0) sw zero, -36(s0) sw zero, -40(s0) j .LBB0_1 .LBB0_1: # =>This Inner Loop Header: Depth=1 lw a0, -40(s0) addi a1, zero, 9 blt a1, a0, .LBB0_4 j .LBB0_2 .LBB0_2: # in Loop: Header=BB0_1 Depth=1 j .LBB0_3 .LBB0_3: # in Loop: Header=BB0_1 Depth=1 lw a0, -40(s0) addi a0, a0, 1 sw a0, -40(s0) j .LBB0_1 .LBB0_4: lw a1, -40(s0) .LBB0_5: # Label of block must be emitted auipc a0, %pcrel_hi(.L.str) addi a0, a0, %pcrel_lo(.LBB0_5) call printf lw a0, -40(s0) ld s0, 32(sp) ld ra, 40(sp) addi sp, sp, 48 ret
■ 最適化コード
# 最適化コード(LLVM IR) # clang -target riscv64-unknown-linux-gnu -emit-llvm -S -O3 -o test.ll test.cdefine dso_local signext i32 @main(i32 signext %0, i8** nocapture readnone %1) local_unnamed_addr #0 { %3 = tail call signext i32 (i8*, ...) @printf(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i64 0, i64 0), i32 signext 10) ret i32 10 } declare dso_local signext i32 @printf(i8* nocapture readonly, ...) local_unnamed_addr #1# 最適化 RISC-V アセンブリコード
main: # @main # %bb.0: addi sp, sp, -16 sd ra, 8(sp) .LBB0_1: # Label of block must be emitted auipc a0, %pcrel_hi(.L.str) addi a0, a0, %pcrel_lo(.LBB0_1) addi a1, zero, 10 call printf addi a0, zero, 10 ld ra, 8(sp) addi sp, sp, 16 ret
最適化するといきなり10という結果だけになる。ループも回さずにいきなり結果になる。
LLVMフロントエンドIR、アセンブリコードのどちらも、見ての通り命令が非常に少なくシンプルになるため、見た目の可読性の向上・アプリの実行も高速化される。
この強力なフロントエンドLLVM IRの最適化を活かすと、通常のコード・量子系のコードでそれぞれ良い感じの最適化を行うことが出来るようになると考えられる。次にコードの分離を行って動作するか試してみることにする。
■ LLVMによる古典・量子のコード分離最適化と結合
# 古典側ソースコード test.c#include <stdio.h> #include <stdlib.h> int calc(); int main(int argc, char* argv[]) { int i = 0; for (i = 0; i < 10; i++); calc(); return i; }# 量子側ソースコード(RISC-V custom quantum ISA) test_q.c
#include <stdio.h> #include <stdlib.h> int calc() { int ret = 0; // hadamard asm volatile( "qooh.k qa0,qt1,qzero,1" ); // measure asm volatile( "qmeas.k %0,qt1,qzero,1" :"=r"(ret) : ); return ret; }
ぶっちゃけ関数で分離しただけのシンプルなコードになっている。次にclangで最適化を付けてIRを見てみる。ここでLLVMの最適化の効果が出るのは古典側のコードだけで、量子側コードについては"LLVM Pass Managerを使った最適化"を実装することで解決されるだろうという想定になる。
# 古典側コード(LLVM IR) clang -target riscv64-unknown-linux-gnu -emit-llvm -S -O3 -o test.ll test.cdefine dso_local signext i32 @main(i32 signext %0, i8** nocapture readnone %1) local_unnamed_addr #0 { %3 = tail call signext i32 bitcast (i32 (...)* @calc to i32 ()*)() #2 ret i32 10 } declare dso_local signext i32 @calc(...) local_unnamed_addr #1# 量子側コード(LLVM IR) clang -target riscv64-unknown-linux-gnu -emit-llvm -S -O3 -o test_q.ll test_q.c
define dso_local signext i32 @calc() local_unnamed_addr #0 { tail call void asm sideeffect "qooh.k qa0,qt1,qzero,1", ""() #1, !srcloc !2 %1 = tail call i32 asm sideeffect "qmeas.k $0,qt1,qzero,1", "=r"() #1, !srcloc !3 ret i32 %1 }# 古典・量子を結合したコード(LLVM IR) llvm-link test_q.ll test.ll -S -o link.ll
; Function Attrs: nounwind define dso_local signext i32 @main(i32 signext %0, i8** nocapture readnone %1) local_unnamed_addr #0 { tail call void asm sideeffect "qooh.k qa0,qt1,qzero,1", ""() #1, !srcloc !2 %3 = tail call i32 asm sideeffect "qmeas.k $0,qt1,qzero,1", "=r"() #1, !srcloc !3 ret i32 10 } ; Function Attrs: nounwind define dso_local signext i32 @calc() local_unnamed_addr #0 { tail call void asm sideeffect "qooh.k qa0,qt1,qzero,1", ""() #1, !srcloc !2 %1 = tail call i32 asm sideeffect "qmeas.k $0,qt1,qzero,1", "=r"() #1, !srcloc !3 ret i32 %1 }# 古典・量子を結合した RISC-V アセンブリコード
main: # @main # %bb.0: #APP qooh.k qa0,qt1,qzero,1 #NO_APP #APP qmeas.k a0,qt1,qzero,1 #NO_APP addi a0, zero, 10 ret
実行されるアセンブリコードが古典・量子の両方を結合された形になって、古典側コードはLLVMの最適化が効いたコードになっている。
量子側はLLVM Passを実装することで、連続した処理を消すなどの最適化が出来ればカッコいい感じになる。この辺は未実装でLLVM Pass Manager/Pass、opt あたりを参照しつつQuantumLogicalPassみたいなクラスを継承して作れば良いんだろうけど、まだよく分かっていない。
このようにそれぞれIRを出力して最適化を確認・修正することで、古典コード・量子コードで別々に最適化させた内容を容易に結合させることが出来るようになる。
この結果、通常/既存のコードや資産の最適化を活かしつつ、そのまま量子系の処理をソースコードに新しく含めて実装を進めることが出来るという流れ。
とりあえず今の所、qlangの文法表現の所までは出来たから、あとはRISC-V量子側とのIR解釈&つなぎこみを行うとqlangコンパイラが使えるようになる。最適化の分離はその次かな。