RustのLLVMに関する問題
概要
この記事はRust Advent Calendar 2019の12日目の記事として書かれました。
遅刻してしまった...すいません(最悪)。
言わずもがなRustはLLVMをバックエンドに持つ言語です。 なのでバグがLLVMの最適化のせいだったりします。有名な例はnoaliasとか無限ループとかです。この辺りは定期的(2ヶ月一回くらい?)にTwitterとかで話題になりますね。またRustでの強い制約がLLVMにおける意味とのギャップで弱く表現せざるを得なかったりもします。そんな感じのこと少しだけ掘り下げて、RustのIssueとかLLVMのPhabricatorから幾つかまとめてみます。怪しいこと言ってたら教えて下さい。
dereferenceable_globally
LLVMにおいて現在のdereferenceable(n)というAttributeは壊れているという話があります。dereferenceable(n)は付けられたポインタが、指すアドレスから少なくともnバイトallocateされていると保証するものです。C++における参照だと思ってくれて大丈夫です。その定義を直そうとするパッチ上での会話が面白かったので紹介します。
⚙ D61652 [Attr] Introduce dereferenceable_globally
そもそものdereferenceableが壊れているというのはそのderef可能という性質がどの範囲で保証されているのか曖昧であるという問題が背景にあります。
struct A {int i, j; double k;}; int f(A& a){ ... } int g(A* a){ ... }
c++で上のような関数があったとき, 以下のようなIRが出力されます。
%struct.A = type { i32, i32, double } define i32 @_Z1fR1A(%struct.A* dereferenceable(16) %a) {..} define i32 @_Z1gP1A(%struct.A* %a) {}
マングルされているので分かりづらいですが上がfで下がgです。dereferenceable(16)があるのが分かると思います。当然16はstructのサイズです。さて次に
struct A {int i, j; double k;} int f(A& a){ delete &a; return a.i; }
は
define i32 @_Z1fR1A(%struct.A* dereferenceable(16) %a) { %a-p = bitcast %struct.A* %a to i8* tail call void @_ZdlPv(i8* nonnull %a-p) ; delete関数 %a-i = load i32, i32* %a, align 8 ret i32 %a-i }
のようにコンパイルされます。これは所謂use after freeですので実行時に落ちます。
このコードは平然とclangはコンパイルするので、dereferenceableというのはclang的にはそのプログラムポイントでderefすることが可能であることを指していることが分かります。
一方LLVMの最適化パスにおいてはdereferenceable(n)という性質が関数のスコープ内の任意のプログラムポイントで成立することを仮定しているかのように最適化を現状しています。ここでバグを生み出します。
int *obj; void foo(int& c, int * __restrict a, int * __restrict b) { delete obj; for (int i = 0; i < 3; ++i) { if (a[i] > 999) { a[i] = c * b[i]; } } }
ここで引数のcはc++の参照ですのでdereferenceable(4)です。
さてプログラマが(a[i] <= 999 ならばobj = &c)であると思いならがらコードを書いてるとします。さてここでaとcはaliasしないのでcの中身はループ内で値が変わりません。またLLVM的には関数内どこでもderefできると思ってます。よって
int *obj; void foo(int& c, int * __restrict a, int * __restrict b) { delete obj; int c_loaded = c; // ロードしてる for (int i = 0; i < 3; ++i) { if (a[i] > 999) { a[i] = c_loaded * b[i]; } } }
のほうが効率が良さそうです。さてa[i] <= 999の場合を考えます。最適化前はcはロードされないはずだったのですが最適化後はロードされてしまいます。ここでa[i] <= 999よりobj=&cなのでuse after freeで終わりです。
このように本来であれば引数%argがプログラムポイントPにおいてもdereferenceable(n)であるためにはP以前にdeallocateされ得ないことをLLVM側で確かめる必要があるわけです。これは多少保守的になってしまいますがCFGをSCCして上の方から伝搬するとかでうまくいきそうです。
ここで、まず定義から直そうとしているのが以下のパッチです。
⚙ D61652 [Attr] Introduce dereferenceable_globally
ここでは
dereferenceableは修飾されたプログラムポイントでのみ有効であるという意味に変更dereferenceable_globallyというそのポインタが定義されたときからプログラムの終了までallocate可能であることを保証するメタデータの導入
2点が提案されています。dereferenceable_globallyに関していうとグローバル変数くらいにしか付けられなさそうですが...
そこにRustの開発者の方(RustBeltのRalf Jungさんだった)が
- Rustにおいては「関数のスコープ全体で
dereferenceableが成立する」という意味で参照に無条件に付与されている(別スレッドから、違う参照からdeallocateされ得ないので) dereferenceable_globallyは強すぎてRustの参照にも付けれないので意味がない
という観点から"generally happy with its semantics"と主張しています。Rustが吐くコードに関してはuse afte freeなどの危険なコードは言語レベルで弾くことができるので、現状のLLVMのdereferenceableの定義を変える必要が全く無いわけです*1。むしろ変更に対応するコストと最適化がされづらくなるというRust側からするとマイナス面が大きいです。
折衷案として出てるのが関数の引数に対して「関数のスコープ全体でdereferenceableが成立する」をnofree,noalias,dereferecneableで表現するという考えです。確かに
nofreeはdeallocateする関数の引数/命令のオペランドにならないnoaliasはポインタのaliasが存在しない
の2つがdereferenceableに加えて成立するならば、dereferenceableは損なわれることが無いように思えます。
上のスレッドではRustにおいて、noaliasはimmutable refに対しては付与していいだろうが、mutalbe refに対してはnoaliasは危ないからやめたほうが良いという意見が出ています。
個人的にはもう一つ問題があると思っていてnoaliasをimmutable refに対して無条件につけるのはRust的にはmutateされないことが保証されているので、LLVMで最適化してもバグったりしないとは思いますが、
pub fn f(b:&i32, c:&i32) -> i32 { return b + c; } pub fn main() { let a = 42; println!("{}", f(&a,&a)); }
上のような場合,
かと言って全ての参照にb, cにnoaliasをつけるのは、b, cがaliasしうるので未定義なのでやっぱりダメだと思います。 noalias付けないようにすると先述した「関数のスコープ全体でdereferenceableが成立する」をimmutable refにすら付けらなくなってしまうので困ったなあという感じです。やっぱり専用のdereferenceable_scopeみたいなものを導入するのが丸い気がしますね。
(訂正) 上の議論は間違っていって現状noaliasはすでにimmutable refについているようです。それとb,cがaliasしたときに未定義であるというのはLangRefに定義されておらず、自分の思い違いでした。訂正します。
てかstoreされない参照にnoaliasつけるのはバグらないのかなあ....
NonZeroUXXのLower
Rustには正の数を表すような型NonZeroUXXのような型があります。現在のLLVMはこれを上手く表現することができません。
use std::num::NonZeroU64; pub fn foo(x: u64, y: NonZeroU64) -> u64 { x / y.get() }
をReleaseでコンパイルすると
define i64 @_ZN10playground3foo17h6f7b350790e360ceE(i64 %x, i64 %y) unnamed_addr #0 { start: %0 = icmp eq i64 %y, 0 ; これは消せるはず br i1 %0, label %panic, label %bb2, !prof !1 bb2: ; preds = %start %1 = udiv i64 %x, %y ret i64 %1 panic: ; 当然ここも消せる ... }
となり、現在は最適化できていないことが分かります、残念。 Rust Playground
さてこれは引数%yに(0, MAX]のような制約をつけられると良さそうです。
この問題のややこしいところはLLVMには範囲に制限するメタデータがあるという点です。
LLVM Language Reference Manual — LLVM 10 documentation にある例を引用すると
%a = load i8, i8* %x, align 1, !range !0 ; %a ∈ [0, 2) %b = load i8, i8* %y, align 1, !range !1 ; %b ∈ [-1, 2) %c = call i8 @foo(), !range !2 ; %c ∈ [0, 2) ∪ [3, 6) ... !0 = !{ i8 0, i8 2 } !1 = !{ i8 255, i8 2 } !2 = !{ i8 0, i8 2, i8 3, i8 6 } ; 0,1番目, 2,3番目でそれぞれ半開区間を表す。
つまり以下のようなLLVM IRをopt -O1にかける*2 と
define i64 @f(i64 %x, i64* %y) { start: %y-deref = load i64, i64* %y, !range !0 %0 = icmp eq i64 %y-deref, 0 ; br i1 %0, label %panic, label %bb2 bb2: ; preds = %start %1 = udiv i64 %x, %y-deref ret i64 %1 panic: unreachable } !0 = !{i64 1, i64 0}
こうなります。
define i64 @f(i64 %x, i64* %y) { start: %y-deref = load i64, i64* %y, !range !0 %0 = udiv i64 %x, %y-deref ret i64 %0 } !0 = !{i64 1, i64 0} ; 0はMAX+1なので[1, MAX)を表します
godbolt.org この場合は実際に最適化できていることが分かります。 違いは
%yを参照渡しにしているload命令に!rangeメタデータを付与している
です。じゃあどうして%yに!rangeをつけれないのかという疑問が出るんですけど
!rangeはload, call命令にのみ付けられる仕様に現在はなっています。よって関数の引数に!rangeを付与することができません。
解決方法としては
assertを挟む、つまり関数が始まってすぐに
cmpとllvm.assumeをぶっこむフロントエンド(Rust)側のみで対応でき、良い感じですが区間にあるという情報がそこそこ複雑な幾つかの比較命令のconjunction/disjunctionになってしまうという点で良くないです。
-
これは割と根本的な解決になっていそうで良さそうです。実際に以前に拡張しようとした形跡があります。ですが、文法、パーサー(bitcodeも!), llvm::Valueにメタデータを持たせる、Verifierの拡張、...とLLVM側をかなり変更する必要性があり実装されていません。またllvm::Metadataはllvm::Attributeに比べてコストが大きいと言われているので難しいかもしれません。
Attributeにrangeを追加する
Attributeなら関数の引数に情報を添付できます。dereferenceable(n), align nのように整数を持ったAttributeもあるのでrange(integer type, l, r)のようなAttributeがあると問題を解決できそうです。問題はAttributeの引数にはlistをもたせるのが恐らく不可能であるという点です。よって表現できるのは一つの半開区間のみです。Rustに関してはNonZero以外のユースケースが無いと思うので問題ないんですが...
となり一長一短な感じです。
さて最初のRustのコードをDebugでコンパイルしてみると
%3 = load i64, i64* %y, align 8, !range !30 ;これ ; call core::num::NonZeroU64::get %4 = call i64 @_ZN4core3num10NonZeroU643get17h15203f6a455e7f46E(i64 %3), !range !30;これ ... !30 = !{i64 1, i64 0} ; [0, MAX)
これを見ると!rangeがついています。
先述しましたがload, callには!rangeが付けれるのでDebugだと上手く付けることができます😜
まあrangeの伝搬が上手くできていないんで最適化はされないんですが...
空無限ループ
github.com これは有名ですね(またRalf Jungさんだった)。
pub fn f() { loop{} } pub fn main() { f(); println!("end"); }
のようなプログラムが終わってしまう(実際は"end"が表示されず落ちる)のが問題だということです。
上のissueに全て説明がありますが、軽く説明すると副作用が無い無限ループが定義されているRustと、定義されているが消しても良いとするっぽいLLVMとのギャップが原因です。「っぽい」と書いたのはLLVMにおいて定義か未定義かLangRefには書いてないからです。空ループそのものは未定義では無くて無限ループのbodyにmalloc(positive_number)みたいなことをすると未定義であると教わった記憶があります...よくわからん...
例として
define i32 @f1(){ br label %loop loop: br label %loop } define i32 @f2() { %ret = tail call i32 @f2() ret i32 0 } define i32 @f3() { %ret = tail call i32 @f1() ret i32 0 } define i32 @f4() { %ret = tail call i32 @f2() ret i32 0 }
というコードをLLVMで最適化することを考えます。opt -O1とかすると
define i32 @f1() local_unnamed_addr #0 { br label %loop loop: ; preds = %loop, %0 br label %loop } ; Function Attrs: nounwind readnone define i32 @f2() local_unnamed_addr #1 { ret i32 0 } ; Function Attrs: norecurse noreturn nounwind readnone define i32 @f3() local_unnamed_addr #0 { unreachable } ; Function Attrs: nounwind readnone define i32 @f4() local_unnamed_addr #1 { ret i32 0 }
とかになります。f1を見てくれると分かりますが空ループそのものはLLVMでも定義されています。Rustならば全て無限ループが残ってほしいというのに、 f2、f3、f4は関数呼び出しが消され、f3はunrechableというterminatorが追加されているという感じに変更されています。実はこれは無限ループ自体は全く関係なく「副作用を持たないかつ例外を投げない関数呼び出しは返り値が使われないならば消しても良い」という最適化です。
この最適化は(今回の場合は) 主にこの2つのパスがしています。
LLVM: lib/Transforms/IPO/PruneEH.cpp Source File
PruneEH はPrune (unused) exception handlersの略です。これはTransforms/IPO/FunctionAttrs.cpp, Attributor.cppのようにCallGraphの下からnounwind, noreturnをボトムアップに推論していくパスのようです*3。
また推論の後noreturnを持つ関数を呼んでいるときにterminatorをunrechableにするということも行っています。
LLVM: lib/Transforms/Scalar/EarlyCSE.cpp Source File
Common-subexpression eliminationのはずなんですが、むしろDead code eliminationをしています。isInstructionTriviallyDeadという関数が副作用のない無限ループをする関数呼び出しをDeadだと判定して関数呼び出しを消すようです。isInstructionTriviallyDeadはwouldInstructionBeTriviallyDeadを中で呼んでいて、
llvm-project/Local.cpp at 97572775d2fe088d8059b3a9423f6d8539fafe33 · llvm/llvm-project · GitHub
の
bool llvm::wouldInstructionBeTriviallyDead(Instruction *I, const TargetLibraryInfo *TLI) { .... if (!I->mayHaveSideEffects()) return true; /* mayHaveSideEffects { return I->mayWriteToMemory() || I-> mayThrow(); } */
にあるようにI->mayHaveSideEffects()で副作用が無いので、falseを返すので結局関数呼び出しが消えます。
さて今見たようにLLVMは副作用が無い無限ループを消すようです。それはしょうがないとして問題点はこれがLangRefに書いておらず文書化されていないことです。
本職の方々の間ですら解釈に違いがある様子(これの真ん中下くらいから)が見れます。文書化しようとする動き(これとかこれ)もありますが現状それには至っていません。 *4
一方C++には仕様で副作用がない無限ループが未定義であると決まっています(Forward progressについての言及)。 en.cppreference.com
つまりclangのバックエンドとしてのLLVMは副作用が無い無限ループを消してもよく(消したほうが一般に最適化としては優れている),
RustのバックエンドとしてのLLVMは副作用が無い無限ループを消さないという挙動をLLVMは求められているわけですが、現状は前者側にべったりです。
後者の言語実装者は
llvm.sideeffectというinaccessiblememonly nounwindというAttributeをもったダミーの命令を適宜ループに挿入することが一応LLVMから提供されている方法です。
しかしながらこの命令を入れることによって最適化が上手く行かなくなる現象が起こります😨 *5。実際Rustでも起こっています。
処理時間12倍なってるベンチマークは何があったんですかね...早く治ると良いですね
終わりに
本当はnoalias, LTOやConstant propについても扱いたかったのですが長くなってしまうので違う機会にしたいなと思います。来年こそは(研究とかで)Rustを使いこなせるように頑張りたいです✌
*1: 考えれるのはインラインして命令の順番を変えるみたいな状況であるが流石にそこまでヤバい最適化はしないだろうですし..
*2:正確にはopt -simplifycfg -early-cseで十分です
*3: 前回書いた記事でAttributorについて書いてみたので読んでみてください🙏
*4: Chandlerさんの返信を見て確かにと思ったのがRustからC++を呼ぶみたいなシュチュエーションはどうすればいいでしょうね
*5: 例えば関数内の命令数が多くなってinlineしなくなってしまうとか, カウントする際にハードコーディングして省けばよいですが.. 絶対漏れが出るし挙動がわかりづらくなる..流石にinlineとかloop unrollとかの有名所は上手くやってるでしょうが..
LLVMのAttributorというパス
この記事は
の6日目のために書かれました。22時くらいに投稿するのはギリギリセーフ?(すいません)
概要
4,5ヶ月前AttributorというパスがLLVMに追加されました(唐突)。自分はGSoCでこれの開発の手伝いをしたので紹介したいと思います。
上はメインに開発をしていたJohannesさん(メンターだった)がLLVM Devmtgで解説してる動画です。分かりやすいんで時間があれば見てみてください。もう一つチュートリアルがあるんですが難しめです。GSoCは全体の設計を彼がやって僕ともう一人の生徒で推論部分を実装するという感じでした。
Attributor
Attributorはもともと"Attribute"を推論するためのフレームワークでした *1。Attributeというのは最適化のパスにおいてて最適化が可能かどうかなどを判定するのに用いられるメタな情報です(llvm::Metadataとは違います *2 )。
例えば
int f(int *p, int u){ return *p + u; }
という適当な関数をclang -O2 -emit-llvm -S とかしてLLVM IRにすると
define i32 @f(i32* nocapture readonly %p, i32 %u) #0 { %p-deref = load i32, i32* %p, align 4 %res = add nsw i32 %p-deref, %u ret i32 %res } attributes #0 = { norecurse nounwind readonly }
のようなコードが出てきます*3。上のnocaputre, readonly, nounwind, norecurse, align 4がAttributeです。
nocapture readonly %p は
%pが指すアドレスにstoreされない(readonly)%pはcaptureされない(nocapture, 関数のスコープを超えてoutliveしない)
という情報を与えます。このように関数の引数につけられたAttributeをArgument Attributeと言います。
define i32 @f(i32* nocapture readonly %p, i32 %u) #0 {にある#0は関数を修飾しています。
つまりfに
- fは再帰をしない(
norecurse) - fは例外を投げない(
nounwind) - fはstoreしない(
readonly)
という性質があることを保証しています。このようなAttributeをFunction Attributeといます。
他にも関数の返り値につけることができます。またcontext sensitiveにCallSiteを区別してAttributeをつけることもできます。
全てのAttributeの定義はLangRefにあります。見てみてください。
まずデモとして上のコードをAttributorに通したときの結果を見てみましょう*4。 https://godbolt.org/z/nq9mQc
define i32 @f(i32* nocapture nofree nonnull readonly align 4 dereferenceable(4) %p, i32 %u) #0 { %p-deref = load i32, i32* %p, align 4 ... (i) %res = add nsw i32 %p-deref, %u ... (ii) ret i32 %res } attributes #0 = { nofree norecurse nosync nounwind readonly willreturn }
のようにAttributeが推論されます。%pを見てみるとnofree, nonnull, align 4, dereferenceable(4)が新たに推論され、fにはnofree, nosync, willreturnが推論されていることがわかります。
nonnullはそのポインタがnullでないalign 4はそのアドレスが4の倍数であるdereferenceable(4)はそのアドレスから最低4bytesはallocateされているnofreeは関数に対してはfree,deleteのようにメモリーをdeallocateする命令が関数内で呼ばれないこと、引数に対してはその値がfree,deleteされないことnosyncはスレッドの同期をするような命令が呼ばれないwillreturnは関数が必ず返る(無限ループ,exitとかが無い)
ことをそれぞれ保証します。このようなAttributeを推論していくのがAttributorがやっていることです。
軽く説明すると以下のような動きをします:
noXXXのようなFunction Attribute(ex.
nounwind,nofree)に関してはXXXを満たす命令が関数f内にないことを示したい。primitive な命令に関しては簡単に判定できる。 関数呼び出し(例えばgという関数を呼び出すとする)があったらgはnoXXXを満たすと仮定する。gの上でも推論を回しXXXを満たす命令があったらg,fともにXXXを満たす命令が実行されうるのでnoXXXは推論できない。逆にnoXXXでない証拠がでないならnoXXXと言って良い。%argがnofreeであることを示したい。もし関数がnofreeと仮定されているなら直ちに言える。nofreeが仮定されていなくても%argの全てのUseを見てそこでfreeされなければ良い。関数の引数に使われているならばそこの引数がnofreeであると仮定されてれば良い。%argがnonnullであることを示したい。もしも関数にload i32* %argのような命令があり、その命令が必ず実行されるなら%argがnullのときパニックするので%argがnonnullと言って良い。
推論の仕組みは大きく分けて2つに分かれます。
1つ目は最初のnoXXXに対する推論法を見るとわかりやすいですが、「とりあえずそのAttributeを持っていると仮定して、その仮定に反するならば諦める」というものです。
- そのAttributeを持っていると仮定する
- 仮定に反するかを確かめることを繰り返す、反するなら諦める。諦めたなら、諦めたという情報を依存関係がある所に伝搬する。
- 全体で変更がなくなったとき、その仮定は実際成立することが言える
という流れです。これについては後から詳しく説明します。
2つ目は「プログラムポイントPを通る任意の実行列においてそのAttributeを持つことが示されるならばPにおいてそのAttributeを持っているとして良い」というものです。
例えば関数の引数について推論したいときは、Pとしてentry point(最初のBBの最初の命令)をとってあればよいです。
int f1(int *p){ return *p; }
のような関数ではpは必ず4バイトderefされるので引数pはdereferenceable(4)を有します。
int f2(int *p){ if(rand()) { return p[0]; } return 0; }
のような関数ではrand()の値によってはpはderefされないのでダメです。
int f3(int *p){ maybe_infinite_loop(); return *p; }
上の関数の場合pがderefされないで無限ループする可能性があるので推論できません。この次に命令に必ず進むことが保証されるかどうか判定するのにもAttributeが使われています。willreturn,nounwindがあると十分です。
int f4(int *p){ return p[0] + p[1]; }
この場合8バイトderefされるのでdereferenceable(8)を言えます。
int f5(int *p){ if(rand()){ return p[0] + p[1]; }else{ return p[0]; } }
もっというとこの関数ではpはdereferenceable(4) or dereferenceable(8)が成立するのでdereferenceable(4)と言って良いです。
現在は「Pが実行されるならば必ず実行されるようなプログラムポイントの集合」を適当に推論して実装されています。上の例ですとf1,f4は推論されますが, f5を推論できません
(一応これで対応できるんですが色々と考えることがあって放置してしまったhttps://reviews.llvm.org/D65593)。
ここから実装の話に移ります。ぶっちゃけヘッダーにめちゃめちゃ詳しく書いてあるのでそっちを見たほうがいいかもしれません。 http://llvm.org/doxygen/Attributor_8h_source.html
http://llvm.org/doxygen/Attributor_8cpp_source.html
実装
- AbstractAttribute
AbstractAttributeは紐付けられているValue、推論の状態、IR上の場所などを保持するクラスです。
各Attributeに対して個別にAbstractAttributeを継承したクラスを作ります(ex. AANoFree, AADereferenceable...)。更にIR上の場所ごとに継承したクラスを作ります(ex. AANoFreeArgument, AANoFreeFunction..)。
例外はありますが基本的に状態は束の2つ組で表すことが多いです。この2つの値はそれぞれ見ている性質の元々知っている情報(known, lkと置く), 仮定(assumption, laと置く)です。もともとあった情報より悪くなることないのでlk ≦ laを保った上で状態を変化させていきます。lk=laとなったらこれ以上変化しないので推論をやめます。
例えばnofreeに関するAANoFreeの状態はboolの組で、(false, true)が初期状態です。もし最初からすでにnofreeが修飾されていたなら状態は(true, true)となりこれ以上変化しないです。推論の途中で仮定が破られたなら(false, false)となり終わります。(false, true)のまま終われば推論は成功です。
次の例を見てみます。
int f1(int *p) { return rand() ? f2(p) : 0; } int f2(int *p) { free(p); return rand() ? f1(p) : f3(p); } int f3(int *p) { return rand() ? f4(p) : f5(p); } int f4(int *p) nofree { return 0; } int f5(int *p) { return rand() ? f3(p) : 0; }
以下のcall graphを見てください。
これらのnofreeのFunction Attributeの推論の動きですが、まずf4はすでにnofreeなのでf4のknownがtrueになります。
次にf2にはfreeがあるのでf2のassumptionはfalseになります。
次にf1はf2のassumptionはfalseになったことからf1のassumptionもfalseになります。
これ以上変化することは無いのでf3,f4,f5がnofreeであることを言えます。
この伝搬をする関数がAbstractAttribute::updateImplです。これは仮定に対してmonotonicな状態遷移をする関数です*5。以下のようにAANoFreeFunction::updateImplが実装されています。
/// See AbstractAttribute::updateImpl(...). ChangeStatus updateImpl(Attributor &A) override { // CallSite を引数にとってCalleeが`nofree`を持つ, もしくは仮定されているならば // trueを返す関数。 auto CheckForNoFree = [&](Instruction &I) { ImmutableCallSite ICS(&I); if (ICS.hasFnAttr(Attribute::NoFree)) return true; // CallerのAANoFreeFunctionをとってくる const auto &NoFreeAA = A.getAAFor<AANoFree>(*this, IRPosition::callsite_function(ICS)); return NoFreeAA.isAssumedNoFree(); }; // 関数内の全てのCallSiteを回ってもしもfalseが見つかった場合 // この関数の`nofree`の仮定も破られる(pessimisticなfixpoint) if (!A.checkForAllCallLikeInstructions(CheckForNoFree, *this)) return indicatePessimisticFixpoint(); return ChangeStatus::UNCHANGED; }
他にはdereferenceableに関するAADereferenceableでは、自然数を台集合、∧ としてmin, ∨としてmaxを取った束の組を状態としてます。
初期状態では(0, ∞)に初期化されます(実装では∞にはかなり大きめの整数を入れます)。次の例で動きを見てみます。
define internal i32* @f1(i32* %0, i32* %1, i1 %2) { %ret1 = getelementptr inbounds i32, i32* %0, i64 1 %ret2 = getelementptr inbounds i32, i32* %1, i64 2 %ret = select i1 %2, i32* %ret1, i32* %ret2 ret i32* %ret } define i32* @f2(i32* dereferenceable(20) %0, i32* dereferenceable(32) %1, i1 %2) { %p = tail call i32* @f1(i32* %0, i32* %1, i1 %2) ret i32* %p } define i32* @f3(i32* dereferenceable(24) %0, i32* dereferenceable(36) %1, i1 %2) { %p = tail call i32* @f1(i32* %0, i32* %1, i1 %2) ret i32* %p }
イメージとしてはこのようなプログラムです:
int* f1(int *a1, int *a2, int c){ return c?a1+1:a2+2; } int* f2(int a1[5], int a2[8], int c){ return f1(a1,a2,c); } int* f3(int a1[6], int a2[9], int c){ return f1(a1, a2, c); }
これは以下のように推論されていきます。
初期化です。
関数の先頭についている"internal"とはその関数がmodule内からのみ見えるという意味です。そして今ここでf1が呼ばれる場所が静的に全て分かっている状態です*6。するとf1の1つ目の引数に関してはdereferenceable(min(20, 24))で有ることがわかります。よってf1の1つ目の引数の状態は(0, 20)となります。同様に2つ目の引数は(0, 32)となります。
(どうでもいいのでこっからf3は消します)
そしてポインタの足し算を適当にしています。
そして関数の引数に関してはderefereneceable(min(16,24))であることが言えます。結局これ以上変化しないのでこれが不動点です。
実際に実行すると他にも色々ついてhttps://godbolt.org/z/J8tU22
; Function Attrs: nofree nosync nounwind readnone willreturn define internal nonnull dereferenceable(16) i32* @f1(i32* nofree nonnull readnone dereferenceable(20) %0, i32* nofree nonnull readnone dereferenceable(32) %1, i1 %2) #0 { %ret1 = getelementptr inbounds i32, i32* %0, i64 1 %ret2 = getelementptr inbounds i32, i32* %1, i64 2 %ret = select i1 %2, i32* %ret1, i32* %ret2 ret i32* %ret } ; Function Attrs: nofree nosync nounwind readnone define nonnull dereferenceable(16) i32* @f2(i32* nofree nonnull readnone dereferenceable(20) %0, i32* nofree nonnull readnone dereferenceable(32) %1, i1 %2) #1 { %p = tail call nonnull dereferenceable(16) i32* @f1(i32* nofree nonnull dereferenceable(20) %0, i32* nofree nonnull dereferenceable(32) %1, i1 %2) #0 ret i32* %p } attributes #0 = { nofree nosync nounwind readnone willreturn } attributes #1 = { nofree nosync nounwind readnone }
となります。
より強い推論
現在はできませんが、もう少しAttributorを強くすると以下の例(Attributorのテストにあります)で面白い推論ができます。
; From test/Transforms/Attributor/nonnull.ll define internal i32* @f1(i32* %arg) { bb: %tmp = icmp eq i32* %arg, null br i1 %tmp, label %bb9, label %bb1 bb1: ; preds = %bb %tmp2 = load i32, i32* %arg, align 4 %tmp3 = icmp eq i32 %tmp2, 0 br i1 %tmp3, label %bb6, label %bb4 bb4: ; preds = %bb1 %tmp5 = getelementptr inbounds i32, i32* %arg, i64 1 %tmp5b = tail call i32* @f3(i32* %tmp5) %tmp5c = getelementptr inbounds i32, i32* %tmp5b, i64 -1 br label %bb9 bb6: ; preds = %bb1 %tmp7 = tail call i32* @f2(i32* %arg) ret i32* %tmp7 bb9: ; preds = %bb4, %bb %tmp10 = phi i32* [ %tmp5c, %bb4 ], [ inttoptr (i64 4 to i32*), %bb ] ret i32* %tmp10 } define internal i32* @f2(i32* %arg) { %tmp = tail call i32* @f1(i32* %arg) ret i32* %tmp } define internal noalias i32* @f3(i32* %arg) { %tmp = call i32* @f1(i32* %arg) ret i32* %tmp }
f1のCFGは以下なのでf1:bb6において%argはnonnullであることが言えます。
残念ながら現在はこのようなCFGを見るような推論はサポートされてません *7 。ここで%argがnonnullと言えるとf2のlinkageがinternalであることからf2の引数の%argもnonnullといえます。
この場合だとf2が呼ばれるのはf1:bb6だけです。f1:bb6において%argはnonnullで成立してるのでf2の関数定義側の引数にもnonnullをつけて良いことになります。
そうするとf3,f1もlinkageがinternalであることからそれぞれの引数の%argもnonnullといえます。そうするとf1:bbでの比較は無意味となり命令を減らすことができます。こんな感じです(汚くてすいません)。
終わり
たまたま選んだプロジェクトがすごく好みの分野の内容だったので非常にラッキーでした。その他いろいろな話題が最初の動画で紹介されているのでぜひ見てみてください。
*1: 今現在はAttributeだけではなくInter-procedual optimizationに向けのフレームワークとして進められています。
*2:Metadataはより多岐に渡る情報を添付できます。例えば!range(値の範囲を区間に制限する), !alias_scope(最近議論されているfull restrict support)などです。AttributeとMetadataの違いですがかなり謎な感じになってます。例えばMetadataは関数の引数に付与できません。つまり!rangeを直接引数に修飾できないので、RustのNonZeroのような型を上手く表現できないです。これちょっとおもしろくてload命令には修飾できるのでRustでOptのレベルを下げて出力してそれをLLVMで最適化するとちゃんとしてくれるという話があります
*3: readonlyやnorecurseはIPO/FunctionAttrs.cppで推論されているはずです(もしかしたらclangがannotateしているかもしれませんが)
*4: 現在デフォルトではオフになっています。--attributor-disable=false をすると動きます。clangだと-mllvm --atributor-disable=falseとすればよいはず
*5:実を言うとmonotonicでは無い挙動を示しうるのが現在の実装です。前述した必ず実行される命令からの推論も同時に処理しているため、(n, n) みたいな状態から(同一イテレーションで) nより大きいn'で(n', n') のように遷移する可能性があります。
*6: 関数アドレスがどこかにcaptureされたりするとわからなくなります
*7:書いてて思ったけどisKnownNonZeroがやってくれそうな気もするけどなんでなんだろう、この手の推論をflow sensitiveといえばよいのかcontext sensitiveといえばよいのかよく分かんなくなる
GSoC 2019 (LLVM) に採択された
概要
LLVMのGSoCに通ったので頑張りたい。
プロジェクト
プロポーザルは以下(個人情報はお気持ち程度に抜いている)
プロポーザルに大体書いてあるがプロジェクトを要約すると
- LLVM-IRにおいては関数(など)の性質(ex. "この関数はメモリを読み書きしない", "この関数は例外を投げない", "帰ってこない")を示すAttributeというものがある(https://llvm.org/docs/LangRef.html#function-attributes)。
- Attributeは関数の最適化に用いられている。これはフロントエンド(ClangとかRustとか)が関数に添付することができるがすべてフロントエンド側に投げるのはフロントエンド側のコストが高く非効率である(メモリの読み書きの有無とかならLLVM-IR上で分かる)。
- そこでLLVMミドルエンドで推論するのが自然な発想でありいくつかのAttributeに関しては実装されている(http://llvm.org/doxygen/FunctionAttrs_8cpp_source.html)。
- 推論の仕方としてはCalling Graphの SCCの下から伝搬させていくのが自然な実装である。既存実装を見ると分かるが各Attributeに対して別々にSCCをtraverseするようなコードが書かれておりカオスな感じになってる。
- そこで ⚙ D59918 [Attributor] Pass infrastructure and fixpoint framework で提唱されている"Attributor"というフレームワークを用いて再実装しようというのがプロジェクトの大筋である。
- Attributorは関数の性質を伝搬させるのを不動点に至るまでぶん回すみたいな物になっておりデータフロー解析に似た挙動をする。
Organization
LLVMを選んだ理由としてはCPU実験でそこそこLLVMを使ったのと産業で用いられているコンパイラのミドルエンドでの最適化に興味があったのであんま迷わなかった。ちょっと迷ったのはChapelとSwiftだけどLLVMで落ち着いた。CPU実験のおかげである程度フロントエンド、バックエンドをいじっていたので環境構築とかはそんな困んなかった。ビルドに時間がかかりすぎて困ってる(まあ流石にフルビルドをそんなしないやろと信じてる)。応募した感想
- Grammarlyは偉い
- プロポーザルにフィードバックは貰えなかったけどどうにかなる(メーリスに投げてコメント0だったのでちょっと寂しかった)
- Twitterとかで公開してる人のプロポーザルとか見るとn(>10)ページあってビビったりしてたがいうて要点つかめてたら大丈夫っぽい
- マージされたパッチ無かった()けど通ってよかった(流石にパッチは投げた)
ブログを書いてしまい引くに引けなくなったので完走できるように頑張りたい(なお演習3(kbys研...))(普通に完走できるか怪しい).
LLVM Backend と CPU実験
こんにちは。クリスマスいかがお過ごしでしょうか?

この記事は ISer Advent Calendar 2018 - Adventarの25日目の記事として書かれました*1。昨日はsatosさんの
でした。self-hosting面白そうですね(Cセルフホスティング勢に煽られたの面白い)。学期始まりにmincamlコンパイラを実装し始めた頃は「self-hostingもできればなあ」みたいな感じだったのですが片手間でできるようなものではなさそうですね。「フルスクラッチ勢として〜」と後書きにありますが、最近はフルスクラッチの対極にありそうなLLVMをずっとやっているので自作コンパイラをやんなきゃなあという感じです(自作バックエンドの方も頑張りたい😺)。
さて僕は今B3でちょうどCPU実験という実験をやっておりコンパイラ係というコンパイラを書く係をしています。そこで今回の記事ではCPU実験でLLVM Backendを書いてみた(書いている)話を感想をメインにしたいと思います。感想が多いポエムですご了承ください。
LLVMについて

The LLVM Compiler Infrastructure Project
この記事を読んでくださる層にはあまり説明が必要ないと思いますがLLVMとはclangやlldを筆頭に現代のコンパイラ基盤を支える超巨大なOSSです。以下はLLVMを説明するときによく使われる図です。①でLLVM-IRを出力しMiddle-endでLLVMに最適化をしてもらいそこから②で各アーキテクチャ向けにアセンブリやバイナリを出力します。したがって自作言語のコンパイラを実装したいとなったときは①を書くだけで一定以上のパフォーマンスを備えた言語を書くことができます。近年開発された言語でLLVMをBackendとする言語はたくさんありここ2,3年で大きく普及し始めているRust、Appleには欠かせないSwift*2やLLVMのJITによって高速化を図っているJuliaなどが代表例でしょう。また深層学習のためのコンパイラであるGlowなども最近話題になりましたね。

LLVM Backendを書いた話
さて②に相当するのがLLVM Backendです。
LLVM Backendは何をやってるいるのか?
以下の図のようにLLVM BackendはLLVM-IR -> Selection DAG(命令がDAGになってるやつ) -> MI(かなり低レイヤーな中間表現) -> MC(アセンブリとか)へと変換してくれるものです。
やろうと思えばBackendは本当にいろいろやってくれて(実装すれば) ELF生成とかJITとかまでやってくれます。これらの機能を得るためにどういう命令があるのか、どういった関数呼び出し規約か、保存されるレジスタはどれか、.....といったことを記述する必要があります。

本題
手元にLLVMが入ってるひとは
$ llc -version
とかで対応するTarget(及びSubtarget)が表示されると思います。ここに僕の班のISAであるCookieMonster(renameするのがめんどくさくて1stのELMOの名をまだ使っていますが)を入れてLLVM-IRをCookieMonsterのアセンブリに変換することが今回やったことです。全然リファクタリングできていないためかなり乱雑な感じになっていて申し訳ないですが以下が実装です。
雰囲気を掴むため、いくつかコンパイル結果を貼っておきます(ISAについての説明は省きます)(めっちゃアルファ変換されてるけど許して)。
ループに展開されていることが分かる
これはほとんど住井先生のmincamlと変わらない
もうここまで来るとよくわからんが実行したら早かった
分かりやすくするためにインライン展開はしていない。たぶん20億命令くらい。グローバル変数の静的な領域確保,インライン展開で13億くらいまでは行く。
とまあこんな感じでいい感じにアセンブリに変換してくれます。作り方は基本的に https://llvm.org/docs/WritingAnLLVMBackend.html にある手順に従えば問題ないのですがこれは抽象的な部分が多く、雰囲気をつかむことはできますが、これだけ見て実装出来るというわけではないと思います。なので以下の資料やターゲットが参考になると思います。
参考になる資料
- きつねさん
そこそこ昔のLLVMのサンプルなので多少勝手が違いますが日本語で書かれた数少ないBackendについて解説してる本です。分岐命令以外はCPU実験に必要な要素は解説されているので雰囲気を掴むためにも絶対買ったほうがよいです。
これなかったら本当にしんどかった(著者の方ありがとうございます:pray:)
- CPU0
Table of Contents — Tutorial: Creating an LLVM Backend for the Cpu0 Architecture
step-by-stepに実装が書かれたチュートリアルです。章ごとに別れているのでファイルと役割の関係がつかみやすいです。ただ分量が多くバイナリの生成などの必要ない部分も含まれるので Backend structure, Arithmetic and logic instructions, Control flow statements, Function callを実装するタイミングで見るのが良いと思います。
参考になるターゲット
- RISCV
最近追加されたアーキテクチャなので他とは違い比較的新しいC++の機能が使われています。またSubtargetが2つだけと少なくコメントも丁寧なので実装に困ったらまずこれを見るのが良いと思います。以下は2018 LLVM Developers’ MeetingでのlowRISCプロジェクトのFounderであるAlex Bradbury 氏のトークですが全体像をコンパクトに纏めてくれているので、実装しようと思ったときはまず最初に見ると良いと思います。
- Lanai
Lanaiは数年前にGoogleが内部のプロジェクトとして開発したプロセッサらしく研究用なのでFPUがないなどかなりミニマムな構成になっておりコードが読みやすいです。*3
実装して思ったこと
- 最適化は強い
Backendが動くとほとんど何もせずに強めの最適化が動かすことができます(すごい)。 ただminrtに関してはminrtにある程度特化する必要があるため先輩方の命令数(9億くらい?)に到達するにはただLLVMを動かすのみでは難しいと感じました(結構過激なことをする必要がありそうなので)*4。
- 自作CPUでCやRustのコードを走らせることが理論上可能だけど..
ライブラリ等をCで書いてそれをClangでLLVM-IRに変換してそれをリンクして最適化するみたいなことが可能です。例えばRustのmanderblotのコードを動かすことができます*5(コンパイル時に32bitアーキテクチャ向けにコンパイルすることが必要です)。
コンパイルのOpt-levelを1とかにするとするとデバッグ情報が入ってしまいその辺をLowerすることができないので僕のBackendはエラーを吐きます。Rustはメモリ管理周りのあれこれが渋いため現在はminrtは移植できていないです。 もし余興とかでOS等を書きたいのならmincamlくらいのものでもしんどかったのでclang,rustc等が吐くLLVM-IRに完全に対応するのは結構大変だと思います。 ちなみに現在は自分のコンパイラが吐くLLVM-IRは他のアーキテクチャで動かすことができない仕様になっています(LLVM-IRを吐く前にポインタの足し算とかをしているためWord単位のアドレッシングとなっているからバグる)。
- calling graph,cfg等の可視化がしやすい
これはbackendのメリットというよりはLLVM-IRに変換することのメリットかも知れません。まあ使う回数はかなり少ないですが...。参考

- TableGen .. なんだこれは....たまげたなあ
TableGenとはC++を生成するLLVMお手製のDSLです。DAGのパターンマッチの記述とかレジスタの定義とか命令の定義とかでこれをかなり使います。
def: Pat<(or ELMOGRRegs:$l, ELMOGRRegs:$r),
(XOR (XOR (AND ELMOGRRegs:$l, ELMOGRRegs:$r), $l),$r)>;
めっちゃ適当な例ですが上は簡単に言えば(l or r) がDAG上にあったら ( l & r) ^ l ^ rに変換してくれ〜という記述です*6。
これだけ見ると良さそうな感じですが、これマジで意味が分かんなくてしんどいんですよね(エラーメッセージがわかりづらい、てかパターンマッチやりたいなら全部ML likeな構文にしてくれという気持ちがある)。ちゃんと解説したリファレンスが欲しい。
- 実装するのが大変
これは俗に言うインクリメンタルな開発がしづらいという点にあると思います。自作コンパイラと違って全てのパスを(ある程度)適切に実装して初めてアセンブリが出力することができるので各クラスの依存関係を把握する必要があります。0からやりたくてやったけど,全体の設計の理解に加えてC++とTableGenとかの非本質的な部分でかなり辛くなってしまった。CPU0(の3章まで)とかをコピペしてスタートする選択肢を選んだほうが精神的にも効率も良いと思う。LLVMのコード自体はコメントがかなり丁寧で,ゴリゴリtemplateがたくさん!!みたいな感じでは無いので読みやすいと思います。
これはまた違う話だけど公式にメンテナンスされるミニマムなサンプルのターゲットが存在してほしいなあと思いました。
- フレームワークを運用するだけ感があり面白みに欠ける
コンパイラを書くというよりはLLVMとC++の勉強をしているという感覚が強いです。正直あまり楽しくない(悲しい)。自分で最適化のパスを実装すれば楽しいかもしれないが先に自作の方の完成度を高めたいという気持ちがある。
こっから下は具体的な話になってしまいhogeかもしれないです。
各実装の知見
RegisterInfo
RegisterInfo.tdは特に詰まるところはなくただCaller-saved,Callee-savedを記述するだけです。
InstrInfo
InstrInfo.tdにISAが持つ命令をつらつら書いていきます。
多くのチュートリアルではInstrFormatを定義して...これを継承して...みたいな流れが多いんですが結局何をしているのかがわかりづらいです。個人的には最初はFormatとかどうでも良いから直接Instructionを継承して書いてみるのが丸いとは思いました。この辺りの話は上に貼ったRISCVの動画のInstrInfo周りの話(7:00 - )で詳しいので聞いておくと良いと思う。
ISelLowering
かなり本質的な部分でLLVM-IRからSelectionDAGに変換する役割を持ちます。レイトレを動かすのに特に必要なのは関数呼び出し周り(これはきつねさんにだいたい書いてあります)と分岐命令のLower等です。
- 分岐命令のLower
比較命令と分岐命令が別れているならSparcが参考になります。そうでないならRISCVのやり方を参考にすれば良いと思う。
- 末尾呼び出し最適化
この実装はこれは実装されていないアーキテクチャもLanaiやSparcなどちらほらあります。実装例としてはRISCVの実装がわかりやすいです。
- Floatの即値
2ndアーキテクチャでは64bit長命令を採用しているためConstantPool(mincamlでいう浮動小数点テーブル)作らない方針でやったのですがどうやっても作られてしまいかなりハマってしまいました。ConstantFPをLegalにして上げれば作られないので命令を追加するだけで良いです。ConstantPoolを作るひとはConstantPoolを普通にLowerすれば良い。
Frontend
Frontendはそんなに煩雑ではないです。 Backendがメインなのであまり詳しくは説明しませんが, 現在フロントエンドは普通にOCamlで書かれており*7,型検査やクロージャ変換は普通に行いVirtual.tからLLVM-IRを出力しています(src/llvmCodegen.ml)。IRの生成については 素直にLLVMが提供しているbindingを使うのが一番楽だと思います。Kaleidoscopeの7章の実装や https://llvm.moe/ocaml-7.0/Llvm.html を見るとだいたい雰囲気がわかると思います。mincamlのVirtual.tくらいまで行くとほとんどギャップはないです。面倒なのはIf式をphi式に変換するところでこれはKaleidoscopeの実装を参考にすればよいです(非本質的な話ですがUnitの扱いがめんどかったです)。最初はフロントは適当にやっても命令数減るやろと思っていたのですがclang -emit-llvm -O3 と-O2の違いを見る限りループアンローリングなどはフロントエンドでやっている印象があった(ほんまか?)のでこっちを頑張る必要が出てきました。LLVM-IRを吐くことを考えてSSAでゴニョゴニョやるやつを実装していきたいなと思います(正月に)。
CPU実験特有の話
- 諸々がワード単位
バイト単位ではなくてワード単位でいろいろやることが多いと思います。RegisterInfo::eliminateFrameIndex,FrameLowering::emitPrologue辺りをいじってStackSizeを無理やり1/4にしています(正攻法かどうかは分からない)。
- sqrt,ftoi,itofとかを組み込みの命令にしたい
declare float @llvm.sqrt.f32(float %Val)
LLVM-IR上では上のような関数を呼んであげてfsqrtなどの命令をInstrInfo.tdに追加してISelLoweringでLegalにすると内部で勝手にやってくれます。
- out命令とかをinline asmしたい
print_charのような関数を呼ぶのではなく
out %r3
としたくなってくる。これをするにはISelLowering::getRegForInlineAsmConstraintを実装してあげれば
define void @print_char(i32 %x) {
call void asm sideeffect "out $0" "r"(i32 %x)
;これの"r"が整数レジスタを示すことをgetRegForInlineAsmConstraintで定義している
ret void
}
のようにprint_charを定義できる。これをインライン展開するとうまくいく。
雑多なトピック
開発のTips
- ninja,lldを使う
makeしか知らなかったので最初使ったときびっくりしました。現在使ってるのはopt,llcくらいなのでフルビルドする必要はないです。ninja,lldのおかげでビルド時間でストレス溜まることは少なかったです。
- -view-*-dags
llcにつけるとLowerされる途中過程をdotで見れる。linuxだとxdotが使いやすいと思う。
- printfデバッグ
WithColor::note() << "hoge"; とか dbgs() << "fuga";とかでできる。
試したツール
- Souper
Souper: A Synthesizing Superoptimizer – Google AI
SMTソルバを用いて論理的に等価な最適化をしてくれる。手元だとコンパイルを通すのが難しいので公式に配布されているDocker上でコンパイルするのが良いと思う。命令数はほとんど減らなかった(20万くらい)(冗長なコードに対しては強いだろうけどminrtは元から無駄がほとんどないから、まあそれはそうという感じ)。
感想
10月下旬に一応レイトレが実機で動いたので自作コンパイラでの最適化と並列にLLVM Backendを書いてみようというと思いBackendについて調べ始めました。当初2,3週間でできると思っていたのですが結局レイトレのコードをコンパイル出来るようになったのは12中旬になってしまいました(これのせいで自作コンパイラの方のバックエンドは簡易的な実装のままなのでなんとかしたい)。minrtを動かしてみると20億命令でインライン展開のパラメータをいじると13億命令とかになりました。正直10億付近にならないかなあと期待してたのでちょっと凹みました(比較命令と分岐命令を1命令にできると11億まで行けるからしてほしい😺)(まあ逆に考えると多少強めな最適化とレジスタ割当をするとこの辺りまで行けるということなんですかね)。外部のツールに頼りきって命令数減らすのアレな気がするのでLLVMに投げる前段階でもう少し頑張ってみたいなという感じです*8。残り2ヶ月頑張っていきたい。発表3月下旬にならないかなあ。
*1:誰も遅刻してないのウケるな
*2:swiftに関してはSILとかをフロントエンドの参考にするべきなんだよなあ
*3:Lanaiについて全く音沙汰ないんですけどどういった目的で作られたんでしょうね...
*4:CallingGraphのDAGの下からレジスタ割り当てするやつやってみたいんだよなあ
*5:これは半分嘘で生成されるLLVM-IRに含まれるrust_begin_unwindという無限ループする関数があるとBackendがEmitPrologueで死ぬ(なんで)
*6:これは実際にInstrInfo.tdにあってorは数十回しか呼ばれないので乗っているXOR,ANDに変えているやつです
*8:ループ周りの処理をすればもうちょっと上がると思ってる
LinuxでECCSのプリンターで印刷する方法
この記事はIS18er Advent Calendar 2017 8日目 adventar.org の記事として書かれました。
概要
概要もクソも無くタイトルままなのですが手元のLinuxでECCSのプリンターで印刷する方法があまりネットにまとまっていなかったので備忘録として書きます(Advent Calenderの記事としては違うのではと自分では思うのですが8日目誰も書かなそうだったのでハードルを下げる目的も兼ねて)。Linux初心者なので間違ったこと書いているかもしれません。というか 相談員ミーティング (2016年4月22日) | ECCS Tutor's pageの通りにやればできます(ただし下の4が必要です)。環境はX240,Archlinuxです。
方法
- cupsが入っていないなら入れます(yaourt -S cupsとか)。
- www.ecc.u-tokyo.ac.jp [FAQ: よくある質問] IPP印刷の設定方法 - Mac -の3にあるリンクを踏んでzipを落としてRICOHなんちゃらを適当な場所に置きます。
- cupsを起動します(systemctl start org.cups.cupsd.serviceとか)。
- www.ecc.u-tokyo.ac.jp [広報] IPP 印刷用パスワードの導入に沿ってパスワードを設定します(これに気付かずハマった)。
- プリンターを追加します(sudo lpadmin -p Main_Mono -E -v 'https://USERNAME:PASSWORD@printer.ecc.u-tokyo.ac.jp/Main_Mono' -P /PATH/TO/PPDみたいな感じ)。ここでのUSERNAMEは10桁のよく使うやつでPASSWORDは4で設定したものです。/PATH/TO/PPDは2で落としたファイルへのパスです。
- あとは印刷するだけです。http://localhost:631でcupsのwebインターフェイスが見れるのでジョブを消したりとかもguiでできて便利です。ただし印刷が終わってもPC側のジョブが消えないようなので自分で消す必要があります。
まとめ
これでわざわざ学校のiMacを起動しなくても印刷することできて結構便利です。WinとかMacだとECCSのページでやり方が丁寧に紹介されているのでそれを参照すると良いと思います。関係無いのですが18erでLinuxをメインに使っている人があまりいなくて質問とかあまりできなくて辛いので布教していきたいと思います。明日はgasinさん(@_gacin)の"TMPについて何か書こうとおもったらとんがりが既にやっていたのですがそれでも何か書きます"です。