微妙に便利なgccのオプション
普段アセンブリコードを眺めるときは、最初に覚えた方法であることと、たいてい同時に実行して確認しているのもあって、objdumpを使っている。
が、コンパイル結果のアセンブリコードを見たいだけなら-Sで十分である。
$ cat "double add(double x, double y) {return x + y;}" > add.c $ gcc -S -O2 -c add.c $ ls add.c add.s $ cat add.s ; ... add: .LFB0: .cfi_startproc addsd %xmm1, %xmm0 ret .cfi_endproc ; ...
そんなことは皆知っているのだが、GCCは-を渡すと標準入力から入力を受け取ることもできる。
$ cat "double add(double x, double y) {return x + y;}" | gcc -S -O2 -c - -o add.s
このとき、出力ファイル名を指定しないと-.oみたいなファイルができて面倒なことになるので注意だ(消そうとしてrm -.oとかするとそういうオプションだと思われてエラーになるが、rm ./-.oとすると消せる)。
普段GCCは拡張子で言語を判断しているようなのだが、標準入力からコードを渡すと言語が判定できないと怒られる。
その場合 -x LANGUAGEとして言語を指定できる。LANGUAGEには、c、c++、objective-c、objective-c++、go、ada、f77、などなどが指定できる。
ところで、gccで何らかのオプションをつけた時にどのようなマクロが定義されるかなどに興味のある人は多いだろう[要出典]。GCCにはプリプロセッサをデバッグするための機能があり、-dMとするとdefineされているものを全てダンプしてくれる。
これを使えば、例えばSIMD命令が使えるはずのCPUで__AVX2__のようなマクロが指定されるかどうか調べることができる。
$ gcc -O2 -march=native -mtune=native -dM gcc: fatal error: no input files compilation terminated.
おっと、そもそも入力ファイルがないと怒られてしまった。
こういう時の-だ。プリプロセッサだけ見たいのでファイルの内容なんぞどうでもよいのだから、/dev/nullをリダイレクトすればよいだろう。gccが混乱しないように-xcだけつけて……
$ gcc -O2 -march=native -mtune=native -dM -xc - < /dev/null (.text+0x20): undefined reference to `main' collect2: error: ld returned 1 exit status
おっと、mainがなかったのでリンクエラーになってしまった。どうやら存在しないファイルをちゃんとコンパイルしようとしてしまっている。
こういうときはプリプロセッサだけしか実行しないオプション、-Eの出番だ。普段はプリプロセッサマクロのデバッグに使うが、こういう時にも使える。
$ gcc -O2 -march=native -mtune=native -dM -E -xc - < /dev/null # ...
あとは、想定している命令セットが検出されたかどうか、grepでもすればよい。
$ gcc -O2 -march=native -mtune=native -dM -E -xc - < /dev/null | grep AVX #define __AVX__ 1 #define __AVX2__ 1
reluを分岐無しで実装する
ReLUとは、ご存知の通り、xが正の値ならxを返し、そうでなければ0になるという非常に単純な関数である。だがこれが深層学習において重要な役割を演じたのだから面白い。バックプロパゲーションをする時に微分係数が1より小さいよくある活性化関数だと指数的に係数が弱まって深層にすると学習できなくなるとか何とか。
同僚とReLUの話題になった時、私の頭には分岐無しで abs を実装するという話(『ハッカーのたのしみ』参照)が浮かんだ。現代的なプロセッサでは、分岐予測が働くので、予測を外した場合分岐は時間のかかる命令になる。なのでよく呼ばれる簡単な関数では、分岐を減らすことには十分な意味がある。 abs では何が行われていたかというと、bit演算をうまく組み合わせて一度も分岐をすることなく abs 関数を実装していたのだ。負の値だと最上位ビットが1になるがそうでなければ0であるという事実をうまく使って。
それが脳裏にあったので、ReLUも同様にbit演算を使えば分岐無しで実装できるのではないかと思った。
という訳でチャレンジしよう。まず、IEEE754を思い出すと、浮動小数点数は最上位ビットが符号ビットだ。なので負なら最上位が1、正なら0になっている。これは使える。というのも、int32_tにして算術右シフトを31回行えば、正なら 0 、負なら 0xFFFF.... の値になるからだ。
float x = 1.0; std::cout << std::hex << (*reinterpret_cast<std::int32_t*>(x) >> 31) << std::endl; x = -1.0; std::cout << std::hex << (*reinterpret_cast<std::int32_t*>(x) >> 31) << std::endl;
続いて、これをうまく使って正負どちらの場合でも望みの結果が出るようなbit演算を考えたい。まず、負なら0になってほしいが、ゼロは指数部、仮数部共に0であるので、要するに全てのビットが0である(負の0は考えないことにする)。これを作るのは簡単そうだ。なにせ全てのビットが1の値を作ることができるので、例えば算術右シフトを31回行った結果とのbitwise orを考えよう。これだと、片方が1なら(負の場合)もう片方に関係なく1になる。片方が0なら(正の場合)もう片方と同じ値が出てくる。
筆算で書くとこうなる(便宜的に8bitとする。sは符号、eはexponent、fはfractional)。
正の数 | 負の数
seeeffff | seeeffff
00101001 | 10101001
bor 00000000 | 11111111
------------ | --------
00101001 | 11111111
負の数が全て1になり、正の数は保たれている。いい感じだ。
だが、負の数の場合に欲しい値は0で、そのbit表現は全てのbitが0というものだ。なのでbitを反転しなければならない。ただし、正の数の場合は反転してはいけない。要するに、次はbit二項演算の中から、0と行うと何も起きないが、1と行うとbitが反転するような操作を探さなければならない。
そしてそれは存在する。xorだ。片方が1だと(今回は負の数の場合)、もう片方が1なら0、0なら1とビットが反転する。片方が0だと(今回は正の数の場合)、もう片方が1なら1、0なら0と何も起きない。なのでこれが探していた演算だ。
という訳で以下のようになる。
正の数 | 負の数
seeeffff | seeeffff
00101001 | 10101001
bor 00000000 | 11111111
------------ | --------
00101001 | 11111111
xor 00000000 | 11111111
------------ | --------
00101001 | 00000000
ReLUができたではないか。ここまで来ると、notしてandでもいいということに気づくが、まあそれは何でもいい。実装は以下のようになる。reinterpret_cast が長いので、float と std::int32_t を変関する ftoi と itof を実装したことにする。それはreinterpret_castでもできるし、 union を使って読み替えても良い。
float ReLU(float x) { const auto y = ftoi(x) >> 31; return itof((ftoi(x) | y) ^ y); }
通常の ReLU はこうだ。
float ReLU_normal(float x) { return (x > 0.0) ? x : 0.0; }
ではどうなるか比べてみよう。コンパイルしたのちobjdumpで逆アセンブルする。
ReLU: movd %xmm0, %eax movd %xmm0, %edx sarl $31, %eax notl %eax andl %edx, %eax movd %eax, %xmm0 retq ReLU_normal: maxss (%rip), %xmm0 retq
はい。max って1命令でしたね! これは実際に逆アセンブルした時に気づいた。そして書いている間は ReLU が max(x, 0.0) であることも頭になかった。
確かに今回書いたコードには分岐はない。だが、それ以上に命令が多い。知っている限りmaxのレイテンシは1か2なので、シフトやビット命令をしているこちらの方が時間がかかってしまうだろう。
物事に熱中している間は根本的なところに気づかない、という良い例だった。皆さんもたまには立ち止まって出発地点を確かめましょう。
tr1とboost/tr1でis_permutationがつらい話
なんとも重箱の隅をつつくような話だ。
私が開発に参加しているプロジェクトがあり、そこでは(古くから開発されているので)C++98が使われている。だが先進的なものも積極的に使おうとするプロジェクトでもあるので、Boostやtr1を使ってもいた。CMakeでtr1があるかどうかを確認し、存在していたらそちらをusingもしくはtypedefするという方針である。例えば、
#ifdef HAVE_CXX11_ARRAY #include <array> namespace hoge { template<typename T, std::size_t N> struct array_getter{typedef std::array<T, N> type;}; } #else #include <boost/array.hpp> namespace hoge { template<typename T, std::size_t N> struct array_getter{typedef boost::array<T, N> type;}; } #endif
のような感じだ。C++98ではテンプレートエイリアスが存在しないため、構造体を使っている。まあ、この例ではstd::arrayとboost::arrayは名前が同じなのでusing std::array;でもいいと思う。
ところで、ある日私はテストを書いており、std::is_permutationが使いたくなった。そのテストでは配列持っている値は保証できるが順番は保証できないたぐいのものだったので、正解配列のpermutationであればOKだろうという判断である。今思えばstd::setに内容をコピーして正解集合と比較すればよかったのだが、その時はstd::setの存在を完全に忘れていた。
で、最初にお話しした通りそのプロジェクトではC++98が使われている。よってC++11以降で追加されたstd::is_permutationは使えない。だがC++erの強い味方Boostは使っていいことになっているので、Boost.Algorithmのboost::algorithm::is_permutationが使える。
はずだった。
手元で動くことを確認した後、Travisからfailedメールが来た。調べてみると、異なる2つのstd::tr1::tupleが定義されているようだ。しかし、何故だ?
主な変更箇所はis_permutationだったので、少し覗いてみることにした。TravisではBoost 1.54が使われていたので(!)、boost.orgからダウンロードし、boost/algorithm/cxx11/is_permutation.hppを開く。
#include <boost/tr1/tr1/tuple> // for tie
とある。どうやらstd::pairに対してtieを使いたかったようだ。そのためにboost/tr1/tr1/tupleをインクルードしている。しかし、ご存知の通りboost/tr1の実装とGCCのtr1実装は異なるものである。さらに、boost/tr1の内容はstd::tr1名前空間に定義される。前述した通りそのプロジェクトはtr1の機能も使っている。よって衝突が起きる。この問題の行は1.56.0時点でなくなっている。
仕方がない、と思い、その時std::setのことを思い出せば良かったのだが、何故か「is_permutationくらい自分で実装するか」と思ってしまった。本来は値の同一判定をする関数オブジェクトなどを取れるようにしてかなりジェネリックに書かないといけないのだが、今回は単にテストに使うためだけのものなので、最も簡単なものでよかろうと思い、それらの機能は無視することにした。
そして実装した。テストコードは単一の.cppファイルなので、別段名前空間を分ける必要もなかろうと思い、以下のような関数を定義した。
template<typename Iterator1, typename Iterator2> bool is_permutation(const Iterator1 first1, const Iterator1 last1, const Iterator2 first2, const Iterator2 last2);
ところが今度は手元でコンパイルが通らなかった。オーバーロード解決がambiguousだというのだ。明らかに解決できるのは私が定義した関数だけだろう、と思ったのだが、数秒経って気付いた。手元で使っているGCCは7.3で、-std=c++98などを付けなければ自動的にC++14が選択される。そして-std=c++98は使っていなかった。なのでこれと同じ形をした関数定義がstd名前空間内に存在する。
さらに、C++にはArgument Dependent Lookup (ADL)がある。これは関数の引数が何かの名前空間(例えばstd)で定義されているものなら、関数の候補をその名前空間(例えばstd)から"も"探すというものだ。この利便性と危険性に関しては星の数ほど記事があるので適宜googleしていただきたい。
で、何が起きたかというと、このis_permutationに渡していたIteratorクラスは、どちらもstd::vector<T>::iteratorだった。これは当たり前だがstd名前空間で定義されている。なので、std名前空間にある関数が全てオーバーロード解決の候補になる。GCC7.3ではデフォルトC++14なので、引数がマッチするstd::is_permutationが存在する。ところで私が定義したis_permutation関数もマッチする。よってambiguousになる。
気付くかこんなもん!!!
解決策は簡単で、自作is_permutation関数を適当な名前空間で囲み、使うときに名前空間を指定して呼べばよい。
namespace hoge { template<typename Iterator1, typename Iterator2> bool is_permutation(const Iterator1 first1, const Iterator1 last1, const Iterator2 first2, const Iterator2 last2); } hoge::is_permutation(v1.begin(), v1.end(), v2.begin(), v2.end());
あるいは、もっと単純な解決策がある。is_permutationという名前にしないことだ。そうすれば標準ライブラリと名前が被って困ることはない。
ところで、何も関係ない話だが、この前留学生に「重箱の隅をつつくってどういう意味?」と尋ねられ、「重要でない小さなことばかり議論することだ」と答えた。重箱のことは知っていたようなので、感覚もわかってもらえたようだ。すると次に、「では、豆腐の角に頭をぶつけるとは?」と聞かれ、一体どこで知ったのか非常に気になったのだが、「それは罵倒語で、馬鹿にしつつgo to hellのようなことを言っているのだ」と答えた。彼がどこでこれらの言葉を知ったのかというと、どうやら「角」と「隅」の違いについて調べていたらしい。両方vertex周辺の領域を指すが、概ね、角は出っ張っているところを言い、隅は凹んでいるところを言う、ということについて調べていた時にわからないセンテンスが出てきたので聞いてみたらしい。
最後に、「でも、”豆腐の角に頭をぶつけて死んでしまえ”は冗談だと思われるかも知れないから、罵倒語としてはあまり有用ではない。もっと端的に表現するべきだ」とも伝えておいた。これで、日本人と喧嘩になった時も、彼の怒りが正しく伝わるだろう。
一番簡単な画像フォーマット
世の中には画像フォーマットが沢山ある。データを圧縮してサイズが小さくなるフォーマットや、様々なメタデータを持てるフォーマットなど色々だが、自分で作るとなると何が一番楽だろうか。 ある意味ではsvgだろうが、今回はビットマップということにする。svgでrectを置きまくる? いやそういう趣旨ではない。
なんだろうかとは言ったが、これには確実な答えがある。PNMだ。
このフォーマットは冗談抜きに中身をエディタで表示しても画像が見える。上記リンクのWikipediaから引用すると、一番単純な白黒画像(PBM)は以下のようになる。
P1 # This is an example bitmap of the letter "J" 6 10 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0 1 0 0 0 1 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0
カラーのPPM形式は以下だ。 アスキー形式だとコメントを入れられるところも人間が扱うことを考慮している雰囲気がする。
P3 # The P3 means colors are in ASCII, then 3 columns and 2 rows, then 255 for max color, then RGB triplets 3 2 255 255 0 0 0 255 0 0 0 255 255 255 0 255 255 255 0 0 0
というわけでこの説明を書き始めたのだが、たらたら思いつくままに書いているとわかりにくい文章になったので系統立てて書くことにする。 まず、PNMフォーマットというのは総称で、実際には大きく分けて3つの形式があり、それぞれが二値画像、グレースケール、RGBカラーに対応している。 さらに、それらのそれぞれがアスキーとバイナリの2つのフォーマットで保存することができる。持っている情報は同じだ。
二値画像、PBM
最初に紹介した形式だ。ファイルの最初はP1\nで始まる必要がある。その後画像のサイズが横 縦の順で書かれ、その後0または1が続く。
注意すべきことは、ビットが立っているところが黒で、それ以外が白ということだ。通常255が一番明るい色なのでこれは少し混乱を生むかも知れない。
P1 # This is an example bitmap of the letter "J" 6 10 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0 1 0 0 0 1 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0
ところで、これは全てのPNMアスキー形式に共通することだが、改行はフォーマット識別子以外のどこで行われても許される。これはWikipediaを読んでも書いていなかったので適当なPNM画像を作って改行を入れながらビューワで見ることにした。大抵どこに入れても見ることができた。規格を参照していないのでそうあるべきかはわからないが、そうなっている方が便利なのは間違いない。このせいでファイル読み込み関数が少し面倒になってしまうのだが。
PBMのバイナリ形式はこれもまた厄介で、ビットごとにピクセルが対応している。だが、画像の一列ごとにパディングが入り、LSBは放置される。要するに上の画像をバイナリにすると以下のようになるだろうということだ。
50 31 0a 36 20 31 30 0a 08 08 08 08 08 08 88 70 00 00 P 1 \n 6 1 0 \n 以下参照 0 0 0 0 1 0 0 0 -> 0x08 0 0 0 0 1 0 0 0 -> 0x08 0 0 0 0 1 0 0 0 -> 0x08 0 0 0 0 1 0 0 0 -> 0x08 0 0 0 0 1 0 0 0 -> 0x08 0 0 0 0 1 0 0 0 -> 0x08 1 0 0 0 1 0 0 0 -> 0x88 0 1 1 1 0 0 0 0 -> 0x70 0 0 0 0 0 0 0 0 -> 0x00 0 0 0 0 0 0 0 0 -> 0x00
それぞれの行の後ろに、8の倍数になるまで関係のないビットが追加される。一行の終わりでそのような処理がされるが、今回は行ごとのピクセルが6つしかないので、全てのバイトがパディング入りになる。
ところでご覧の通り、バイナリ形式であっても画像のサイズはASCIIで保存されている。これはおそらく、整数値をバイナリで保存するときにそのサイズや符号の有無を定義するのが難しいとかそういう理由だろう。ちなみにコメントもバイナリ形式に含めることができて(今知った)、バイナリであっても#に相当するASCIIコードから\nまでは飛ばされるらしい。
グレイスケール、PGM
グレイスケールも基本は同じだ。ただ、最大値を記しておける点が異なっている。この最大値と同じ値のとき、そのピクセルは白くなる。
Wikipediaから例を引用すると、
P2 # Shows the word "FEEP" (example from Netpbm man page on PGM) 24 7 15 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3 3 3 3 0 0 7 7 7 7 0 0 11 11 11 11 0 0 15 15 15 15 0 0 3 0 0 0 0 0 7 0 0 0 0 0 11 0 0 0 0 0 15 0 0 15 0 0 3 3 3 0 0 0 7 7 7 0 0 0 11 11 11 0 0 0 15 15 15 15 0 0 3 0 0 0 0 0 7 0 0 0 0 0 11 0 0 0 0 0 15 0 0 0 0 0 3 0 0 0 0 0 7 7 7 7 0 0 11 11 11 11 0 0 15 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
まあ、これだけだ。今回はバイナリの時にパディングが入ることはなく、書くピクセルが1バイトで保存される。要するに255階調しか色調は存在しないのだ。一応、2バイトへの拡張は存在するが、エンディアン関係の問題があって微妙な扱いになっているように書かれている。
カラー、PPM
同じだ。Wikipediaの例ではピクセルごとに改行されているが、そうしないといけないというわけではない。
P3 # The P3 means colors are in ASCII, then 3 columns and 2 rows, then 255 for max color, then RGB triplets 3 2 255 255 0 0 0 255 0 0 0 255 255 255 0 255 255 255 0 0 0
pnm++
というわけでこれは簡単なので自分で画像を作るときはいっちょ使ってみるかと思ったのだがC++用ライブラリがさほど充実していないように見える。ちゃんと検索していないだけかもしれない。恐らくみんなその場でざーっと書いてしまっているのだろう。それでいいと思う。ただ、私もそれを一応したので、ここに設計含めて説明を載せておこうとも思う。
まず、こんなに単純なフォーマットなのだから、コードはそんなに長くならないだろうと思った。1000行行かないのではないかと(これは後で盛大に超えてしまったので正確な推測ではなかった)。なので単一ファイルのヘッダーオンリーライブラリにすることにした。これならインストールなどの問題は限りなくなくなる。コピーして置いてインクルードするだけだ。こんなに簡単なことはない。
次に、C++なのだから、速度は最低限は出て欲しい。なので、データの局所性のために画像データは1次元配列で持つことにした。それをラップして二次元配列のように振る舞わせる。at(size_t, size_t)は導入するとして、operator[]が一行分のrangeをラップしたプロキシクラスを返すことにする。
そして、それぞれの画像フォーマットから出てくるデータは、pixelクラスをtemplateとして受け取るクラスにする。writeはそれでオーバーロードして、readはpixel型をtemplateとして受け取ることにしよう。
ということで作った。ライセンスはMIT、要求はC++11、STL以外の依存はなし。適当にマンデルブロ集合などを描いて遊んでいる。
GitHub - ToruNiina/pnm: pnm format(pbm, pgm, ppm) IO for modern C++ (single header only library)
shellのfor文について
最近技術系のことをしっかりまとめる時間が減ってきた。とりあえず小粒なものを書いておこうと思う。
2ヶ月前くらいに、AからJまで10個のアルファベットを順に取り出したいことがあった。つまりこういうことがしたい。
grep 'A' something.dat | some_command | ... grep 'B' something.dat | some_command | ... grep 'C' something.dat | some_command | ... # ... grep 'J' something.dat | some_command | ...
これを行コピーして書き換える者は「怠惰」ではない。
一応、bashには配列があるが、文法が微妙に複雑で、使おうと思った時は大抵忘れてしまっている。結局毎回「bash 配列」などで検索する羽目になる。
declare -a alphabets=("a" "b" "c")
これはわかりにくくないか?
ところで、bashのforは意外と柔軟だ。
for a in A B C D E F G H I J; do
grep ${a} something.dat | some_command | ...
done
これが動く。
まあA B C D E F G H I Jを打つのは面倒なことに変わりはない。個人的には、普段ループに使っているseqで動けば一番いいのだが、と思っていた。
例えばこれは以下のようにすれば何とかなるだろう……と思ったが動かなかった。
for i in `seq 65 74`; do
grep $(printf "%c" ${i}) something.dat | some_command | ...
done
どうやらこうすると最初の1文字目が出力されるだけでASCIIコードに対応する文字が出るわけではないようだ。
他にはhexにしてから0x4cなどのようにして変換させるという方法があるが、どんどん可読性が落ちていってしまう。
そして最近、以下が動くことを知った。
for a in {A..J}; do
grep ${a} something.dat | some_command | ...
done
最初の苦労は何だったんだ?
その上、以下のようなことが起きる。
$ for a in {A..C}{1..3}; do
echo ${a}
done
A1
A2
A3
B1
B2
B3
C1
C2
C3
今までの苦労を返して欲しい。
ところでfishでこれをする方法を探したが、どうもなさそうに見える。かなしい。
Better document differences from bash · Issue #2382 · fish-shell/fish-shell · GitHub
自作プロダクトにIssueが立ったのでManjaroを触った話
背景
私が作った個人プロダクトの中で、特に宣伝していない割にはそれなりに(見ず知らずの人に)スターを貰って使ってもらえているものがあるのだが、そこにIssueが立った。
internal compiler error with gcc-7.3.1 · Issue #9 · ToruNiina/toml11 · GitHub
ちなみにこれはTOMLという設定ファイルのパーサである。
このIssueは、Arch系でリリースされているgcc-7.3.1だけで(本家では7.3.0までしかリリースされていない)生じるInternal Compiler Errorに関するものだ。それまでは出ていなかったICEが非公式リリース7.3.1で突然発生しているのだからコンパイラのバグかと思ったのだが、微妙な処理系依存動作に乗っかっていたのかもしれない。どちらにせよworkaroundできた方がいい。
それで報告されたエラーメッセージを見た所、おそらくSFINAE関係だろうと推測できた。他のライブラリが問題なくコンパイルできているのだとしたら、その箇所でやっていた妙なことといえば、constexpr関数をtemplateのデフォルト引数に使っていたことだ。
constexpr int func() {} template<typename T, int I = func()> class X{};
というような感じだ。 おそらくここでおかしくなっているのだろうと何の根拠もなく直感した。
そしてboostなどがコンパイルできるのなら、structを使った普通のメタ関数は普通にコンパイルできるのだろう。そう思ってconstexpr関数を使っているところを無くした。リファクタリングも兼ねてメタ関数を整理もした。とりあえずTravis.CIとappvayorのテストは通った。
そして、予想があっているかどうかを確認するため、VMを立てた。
本題: Manjaro on VirtualBox
Arch系で簡単に使うとなると、Archそのものは厳しいので、Manjaroだろう。使ったことはないが記事で見たことがある。そしてIssueの報告者もManjaro使いだったのでこれ以外の選択肢はないだろう。
というわけでVMを立ち上げてManjaroをインストールした。gccのバージョンを確認する。
[manjaro@manjaro ~]$ g++ --version g++ (GCC) 7.3.1 20180312
よし、ということで件のプロダクトをcloneしてcmake……と思ったが、cmakeがない。確かArchのパッケージマネージャはpacmanとか言ったな、と思いググってインストールしようとしてみる。
[manjaro@manjaro ~]$ pacman -S cmake
と思ったが何やらエラーが出ている。「database file for "core" does not exist」というようなエラーだ。
よくわからなかったまま十分ほどgoogleし、どうやらManjaroのインストール後は、最初にpacmanのデータベースファイルを更新しなければならないらしいとわかった。
$ pacman -Syy
を実行すると、どうやらどこかのサーバからパッケージのリストのようなものがダウンロードされている感じがして、インストールできるようになった。
こういうので時間を食ってしまうのは仕方がないが少しつらい。
結果
で、Issueはどうなったのかというと、直っていた。workaround成功だ。というわけでArch使いの方も安心して我がプロダクトを使っていただきたい。
プログラミングパラダイムとは制約をかけることだという見方
ふと感じたのだが、プログラミングパラダイムというのは、プログラムに制約をかけることなのではないか。
基本的に、現代で書かれるプログラムはどれも非常に長く複雑で、人間の脳みそに収まりきらない。実行パスをトレースできない規模のものはザラにあるし、長さがそもそも覚えられないものもある。
このようなプログラムを頭から完全に理解するのは不可能だ。大規模なプログラムを頭から完全に理解するというのは単に文字列として覚えるという意味ではなく(そもそもそれが不可能だと思うが)、様々な入力や内部状態に応じてどの実行パスを通りどの処理が実行されて、それに伴ってどの程度のメモリが消費され、どのスレッドがどの領域のMutexを取得して……。プログラムを文章として読むだけではなく、頭の中でその挙動をエミュレーションできる必要があり、さらに爆発的に増えていく状態や入力の組み合わせの全てに対して実行してみる必要があるのだ(頭の中で)。そもそもそのようなことをしようとすることが時間の無駄のように思える。だいたい、活発に開発されているプログラムは同時並行で異なる部分が編集されていったりする。すると知ったそばから内容が変わっていたりする。こうなると理解するのはもう不可能だ。
なのにどうやってプログラムは開発されているのだろう。理解できていないものを適当に変更したらたいていの場合壊れるのだが、壊れずに動いているプログラムが多いのは何故だろう。それは、主だったプログラミングパラダイムや積み上げられてきた知見がどれも、組み合わせ爆発を抑えていくようなものになっているからだ、とふと思ったのが今回のこの記事だ。
オブジェクト指向的な考え方が何かというと、大雑把には、単一の仕事に対して責任を持つクラスを作り、それに仕事をさせるためのインターフェースを設計し、そのクラスはその仕事を完璧にこなし、外に内輪の情報は何も漏らさないというものだろう。これは、単一の仕事についてそれをこなすために必要な処理を全てブラックボックスに閉じ込めることで、プログラム全体の状態数を減らすという戦略だと言えなくもない。
関数型はもっとわかりやすく、状態が作られることがないので、その部分の複雑さは一切ない。その瞬間にその関数に渡ってきた入力が全てなので、見るべきものが非常に小さくなっていることになる。
このような観点から見ると上の2つは同じことだ。単一の仕事について、その処理をまとめ、それに付随する内部状態が全体に波及しないようにする。簡単な話じゃあないか。
特定の部分について考えているときに他の部分については考えずに済むなら、頭から爪先まで理解している必要はない。読んでいる部分に集中すればよい。
そう考えてみると、goto-lessプログラミングもそのような見方で捉えられなくもない気がする。というのも、gotoがあるプログラムは今読んでいる行に突然全く別の内部状態で処理が遷移してくるかもしれないと怯えながら読まなければならないので、これはつまりプログラムの全状態の組み合わせを全ての行で考慮しなければならないことを意味する(これは若干誇張しているが)。なので、この強すぎる武器gotoを使わないようにする、または使う場面を非常に強く制限することで、考慮すべき状態数を減らしたのがgoto-lessプログラミングだと言えるだろう。
これらは人間の小さな脳みそが組み合わせ爆発に対抗する手段だ。そういう発想があれば、どのパラダイムも納得がいくようになるのではないか。