「cblas_sscalがない」と言われたとき

先日Nimでneoというパッケージを使ったプログラムを動かそうとしたとき、コンパイルまでは通ったのだが、実際に実行しようとしたら以下のエラーが出てハマった。

$ cat src/ai2
import neo

let m = randomMatrix(3, 7)
echo m
$ nimble build
$ ./ai2
could not import: cblass_sscal  # ←エラー
$ 

調べてみると、cblas_sscalとは線形代数演算ライブラリBLASの一部らしい。さらに調べてみるとBLASにはいろんな種類があり、どの種類をインストールしたらよいか迷ったが今回はatlasというライブラリをyumでインストールした。実は他の種類blaslapackもインストールしてみたのだが、これらにはcblas_sscalが入っていなかった。

解決策

atlasとlapackyumでインストール
$ sudo yum install atlas atlas-devel
$ sudo yum install lapack
defineオプションを追加して再ビルド
$ nimble build --define:blas=tatlas

この--define:blas=tatlasミソ。このオプションはnimblasというパッケージで使用されている。cblass_sscalは私のCentos7.5には/usr/lib/atlas/libtatlas.so.3.10の中に入っていることが分かったため、このような形になった。

結果

今度はうまく動作した。

$ ./ai2
[ [ 0.2326961497609559  0.9894961846897405      0.5936563635008223      0.8244128746481869      0.04526972974926768     0.2417286976201536      0.2114615988047588 ]
  [ 0.8228467011541094  0.4437264946689483      0.195534693152861       0.6844019291752474      0.9574039031918617      0.4352774127669565      0.438158530187601 ]
  [ 0.3333379470197706  0.07581602342631522     0.5258890850057507      0.4899364645980169      0.1510872370471941      0.6537294148212911      0.8833919949505109 ] ]
$ 

参考

追記(2018-06-18)

別のLinuxで今回のブログを検証した結果、lapackというライブラリも必要なことがわかったので、lapackyumでインストールする部分を追加した。

パッケージ内のシンボルを全表示する

たとえば簡単なmyapp2プロジェクトに含まれる関数をすべて表示させたいとき、do-all-symbolsを使う。しかし簡単にはいかない。(do-all-symbols :myapp2)などとやってもエラーとなり、期待した出力は出ない。
myapp2内のパッケージ内すなわちmyapp2/src/myapp2.lispには、次の関数fibがあるとする。

(defun fib (n)
  (cond
    ((= n 0) 0)
    ((= n 1) 1)
    (t (+ (fib (- n 1)) (fib (- n 2))))))

quicklispがmyapp2パッケージを認識可能な状況で(例えば「ln -s ~/work/sbcl/myapp2 ~/.roswell/local-projects」を実行して、プロジェクトディレクトリのシンボリックリンク~/.roswell/local-projectsに置く)、次のような一連のプログラムをREPLで入力してみる。

(ql:quickload :myapp2)
(let ((lst ()))                                                     
  (do-all-symbols (s lst)
    (when (eq (find-package 'myapp2) (symbol-package s)) (push s lst)))
  lst)
;; (MYAPP2:FIB MYAPP2::N)  ;; ←結果

これだとfib内の引数nまで表示されてしまうがしかたないか。
この関数をいちいち手入力で作成するのは面倒なので、どこかに格納しておきたい。

Common Lispファイルでプロファイリングする(SBCL)

プログラムを作っていると、どこの部分が時間がかかっているか調べたいときがある。そういうとき使うツールがプロファイラというもので、速度が遅い関数を特定できれば、そこをチューンアップしてプログラム全体の性能を改善するための手助けになる。
ここではCommon LispSBCL(Steel Bank Common Lisp)に標準装備されている「sb-sprof」というプロファイラで速度を測ってみた。

なお、SBCLのプロファイラにはもうひとつsb-profileというのもあるらしい。自分が調べたい呼び出しを狙い撃ちで指定できるようだが、使い方がよくわからなかった。

プロファイラを使う

自分の作ったプロジェクトで、csvファイルを読み込んで加工して表示する一連のプログラムがある。ひどいプログラムなので公開は差し控えるが、このプログラムは45個の関数を持ち、src/apro.lispに入っている。今回、遅い関数はどれか調べてみた。
プロジェクト名はaproという。

$ cd ~/work/sbcl
$ ls
myapp  myapp2  apro
$ cd apro
$ ls
README.markdown  db  apro-test.asd  apro.asd  run.ros  src  tests
$ ros run
(ここからはSBCL内。先頭の*はプロンプト。)
* (ql:quickload :apro)
To load "apro":
  Load 1 ASDF system:
    apro
; Loading "apro"
.
(:APRO)
* (require :sb-sprof)  ; プロファイラの読み込み

("SB-SPROF")
* (sb-sprof:start-profiling)  ; プロファイラによる計測開始

* (progn (apro:prepare-tables) (apro:rebuild))  ; 時間計測するプログラム

APRO::DONE
* (sb-sprof:stop-profiling)  ; 計測終了

* (sb-sprof:report)  ; レポート出力


Number of samples:   62
Sample interval:     0.01 seconds
Total sampling time: 0.62 seconds
Number of cycles:    0
Sampled threads:
 #<SB-THREAD:THREAD "main thread" RUNNING {9D5E329}>

                               Callers
                 Total.     Function
 Count     %  Count     %      Callees
------------------------------------------------------------------------
    22  35.5                   SB-KERNEL:%CONCATENATE-TO-STRING [5]
    22  35.5     22  35.5   SB-KERNEL:UB32-BASH-COPY [1]
------------------------------------------------------------------------
     7  11.3                   (LAMBDA (APRO::Y) :IN APRO::WRITE-TO-DAILY) [192]
     2   3.2                   APRO::MATCH-PAIRS [171]
     9  14.5      9  14.5   (FLET SB-IMPL::FAST-NTHCDR :IN NTHCDR) [2]
------------------------------------------------------------------------
     1   1.6                   SB-UNIX:UNIX-STAT [169]
     7  11.3                   "foreign function interrupt_handle_pending" [79]
     8  12.9      8  12.9   "foreign function __kernel_vsyscall" [3]
------------------------------------------------------------------------
(略)
------------------------------------------------------------------------
     1   1.6                   SB-INT:SIMPLE-EVAL-IN-LEXENV [33]
     0   0.0      1   1.6   APRO::WRITE-TO-MONTHLY [193]
     1   1.6                   REMOVE-IF-NOT [19]
------------------------------------------------------------------------


           Self        Total        Cumul
  Nr  Count     %  Count     %  Count     %    Calls  Function
------------------------------------------------------------------------
   1     22  35.5     22  35.5     22  35.5        -  SB-KERNEL:UB32-BASH-COPY
   2      9  14.5      9  14.5     31  50.0        -  (FLET SB-IMPL::FAST-NTHCDR :IN NTHCDR)
   3      8  12.9      8  12.9     39  62.9        -  "foreign function __kernel_vsyscall"
   4      3   4.8     25  40.3     42  67.7        -  SB-KERNEL:%CONCATENATE-TO-STRING
   5      3   4.8      3   4.8     45  72.6        -  (FLET "CLEANUP-FUN-7" :IN SB-IMPL::REFILL-INPUT-BUFFER)
   6      1   1.6     10  16.1     46  74.2        -  REMOVE-IF-NOT
   7      1   1.6      2   3.2     47  75.8        -  EQL
   8      1   1.6      1   1.6     48  77.4        -  SB-KERNEL::%TYPE-INTERSECTION
(略)
 192      0   0.0      1   1.6     62 100.0        -  APRO::RANGE-YMD-STRING
 193      0   0.0      1   1.6     62 100.0        -  APRO::WRITE-TO-MONTHLY
------------------------------------------------------------------------
          0   0.0                                     elsewhere
#<SB-SPROF::CALL-GRAPH 62 samples {AAACE21}>
*

プロファイラの使い方は以下の通り。

  • (require :sb-sprof)でプロファイラを読み込む。
  • (sb-sprof:start-profiling)を実行してプロファイラを起動する。
  • なにか実行時間を計測したいプログラムを実行させる。
  • 計測が終了したら、(sb-sprof:stop-profiling)を実行する。
  • (sb-prof:report)でレポート出力する。

レポートは前半と後半の2部に分かれており、読み取り方はこれから勉強する。

Common Lispファイルをコンパイルする(Roswell, SBCL)

Roswellでプロジェクトを作ったはいいが、コンパイルするにはどうしたらいいのだろう?
答え:ros initコマンドで作成したスクリプトは、ros buildコマンドでコンパイルできる。 前回前々回で作成したmyapp2プロジェクトを使う。

rosファイルをコンパイルする

コンパイルするには「ros build <rosファイル>」とすればよい。ここで拡張子は「.ros」である必要がある。拡張子が存在しないと何もしないし、ros以外の拡張子では受け付けてくれない。しかも、たぶん、ros initで作成した内容に則した形式である必要がある。例えば第1行が#!で始まるとか。
とにかく「ros build main.ros」を実行してみたのが以下の例だ。なにやらいろんな警告が出た後、拡張子無しの「main」という実行可能ファイルが作成される。

$ cd ~/work/sbcl/myapp2
$ tree
.
|-- README.markdown
|-- README.org
|-- main.ros
|-- myapp2-test.asd
|-- myapp2.asd
|-- src
|   `-- myapp2.lisp
`-- tests
    `-- myapp2.lisp

2 directories, 7 files
$ ros build main.ros

; file: /usr/local/etc/roswell/dump-sbcl.lisp
; (略。警告が結構出力される。)
; compilation unit finished
;   caught 1 STYLE-WARNING condition
compressed 4096 bytes into 1206 at level -1
compressed 4096 bytes into 663 at level -1
compressed 29323264 bytes into 10476494 at level -1
$ 

実行可能ファイルのサイズは約11MB(!)もある。フィボナッチ数列ごときを計算するだけなのにどうしてこんなに大きいのかわからないが、上の表示をみるとこれでも圧縮したのだそうだ。

どのくらい速くなったか確認する

私のPCはノートのCeleron 1.9GHzという骨とう品レベルなので遅い。最近の爆速PCなら一瞬で終わるだろう。

$ time ./main.ros  # スクリプトファイル実行1回目
fib(8)=21

real    0m5.013s
user    0m3.389s
sys     0m1.490s
$ time ./main  # コンパイル済ファイル実行1回目
fib(8)=21

real    0m0.967s
user    0m0.584s
sys     0m0.332s
$ time ./main.ros  # スクリプトファイル実行2回目
fib(8)=21

real    0m1.804s
user    0m1.213s
sys     0m0.550s
$ time ./main  # コンパイル済ファイル実行2回目
fib(8)=21

real    0m0.381s
user    0m0.231s
sys     0m0.146s
$ 

最初の1回目はいつも遅いので、どちらも2回実行し、2回目の結果を比較した。すろと、スクリプトでは1.80秒かかったものがコンパイルすると0.38秒まで早くなった。率にすると約80%の速度アップ!
もっと頻繁にデータベースなど外部にアクセスすればシステム待ちが増え、こんな劇的な改善には至らないだろうと思う。

今回使用したサンプルプロジェクト

An example of Common Lisp Project
https://github.com/kitemw/myapp2

続:Common Lispって何?

前回のエントリーでは、Common Lispのプロジェクトを開発するための前半として、cl-projectを使ってmyapp2という名前のプロジェクトの骨組みを作り、それを呼ぶためのメインファイルmain.rosを作成する方法を説明した。
今回はその続き。このプロジェクトではフィボナッチ数列を作る。

サブルーチン部分(コア)を作成する

src/myapp2.lispファイルにフィボナッチ数列を作る関数を書く。ポイントは、defpackageマクロ(たぶんマクロ)で外部アクセスを許可したい関数を指定するところ。

src/myapp2.lisp変更前:

$ cd ~/work/sbcl/myapp2
$ cat src/myapp2.lisp
(defpackage myapp2
  (:use :cl))  ; (1)ここを変更
(in-package :myapp2)

;; blah blah blah.  ; (2)これを削除
;; (3)フィボナッチ関数をここに記述

src/myapp2.lisp変更後:

$ vi src/myapp2.lisp
$ cat src/myapp2.lisp
(defpackage myapp2
  (:use :cl)
  (:export :fib))  ; 外部へアクセス可能な関数
(in-package :myapp2)

(defun fib (n)
  (cond
    ((= n 0) 0)
    ((= n 1) 1)
    (t (+ (fib (- n 1)) (fib (- n 2))))))

外部から呼ばれる関数を集めたファイルは「モジュール」と呼ばれ、このようにdefpackageで定義し、in-pacakgeでこれ以降がパッケージの中身だと宣言しているようだ。
パッケージの定義で:exportで指定されたシンボルだけが外部から参照可能なので、:exportは必須と言える。

main関数から別ファイルのfib関数を呼ぶ

fib関数を書いたsrc/myapp2.lispを読むため、main.rosファイルを書き換える。「;;#+quicklisp」のセミコロン2個消し忘れに注意。

main.ros変更前:

$ cat main.ros
#!/bin/sh
#|-*- mode:lisp -*-|#
#|
exec ros -Q -- $0 "$@"
|#
(progn ;;init forms
  (ros:ensure-asdf)
  ;;#+quicklisp (ql:quickload '() :silent t)
      ;; (1)アンコメント、(2)カッコ内に:myapp2を追加
  )

(defpackage :ros.script.main.3735336286  ; たぶん数字には意味はない
  (:use :cl))
(in-package :ros.script.main.3735336286)

(defun main (&rest argv)
  (declare (ignorable argv)))  ; (3)main関数からmyapp2:fibを呼ぶ
;;; vim: set ft=lisp lisp:

main.ros変更後:

$ vi main.ros
$ cat main.ros
#!/bin/sh
#|-*- mode:lisp -*-|#
#|
exec ros -Q -- $0 "$@"
|#
(progn ;;init forms
  (ros:ensure-asdf)
  #+quicklisp (ql:quickload '(:myapp2) :silent t)
  )

(defpackage :ros.script.main.3735336286
  (:use :cl))
(in-package :ros.script.main.3735336286)

(defun main (&rest argv)
  (declare (ignorable argv))
  (format t "fib(8)=~d~%" (myapp2:fib 8)))
;;; vim: set ft=lisp lisp:

fib関数を呼ぶときには、「myapp2:fib」というふうに呼ぶ。myapp2:をつけないとエラーとなる。

プロジェクトを実行する

スクリプトとして普通にmain.rosを実行させる。下のようになったら正常に実行できたことになる。

$ ./main.ros
fib(8)=21
$ 

まとめ

以上、なるべくREPL(Read-Eval-Print Loop)を使わずに複数のファイルでCommon Lispを開発する方法を紹介した。たかがフィボナッチ数列を計算するためにここまでたくさんのファイルを作成しなくても、と思われたかもしれないが、ひとつの例なのでご容赦願いたい。 今回のような複数のlispファイル(main.rosもひとつのlispファイル)を使った開発でのポイントは、

  • 呼ぶ側のファイルであらかじめ#+quicklisp (ql:quickload '(:pack1 :pack2) :silent t)のように指定すること。
  • 呼ぶ側は、外の関数を(pack1:fib x)のようにパッケージ名+コロン+関数名と指定すること。
  • 呼ばれる側のファイルであらかじめdefpackage内に(:export :sym1 :sym2)のように宣言すること。

なお、ここまで作成したmyapp2プロジェクトの一連のファイルは以下のレポジトリにアップロードしたので、自分の環境で動作を確認できる。

An example of Common Lisp Project
https://github.com/kitemw/myapp2

Common Lispのプロジェクトって何?

以前のエントリーでsqlite3を取り上げる予定だと書いたが、まだそこまでのレベルに到達していない。もっと基本的なことを書く。
まず、Common Lispのプロジェクトを作成するためにはcl-projectを使うのが便利。Ruby on Railsのscaffoldと同じ。

しかし待ってほしい。そもそもプロジェクトって何だろう?

とにかくプロジェクトを作成してみる

まずは作る。基本的には以前のエントリーで作成したCaveman2用のプロジェクト作成と同じだが、以下に例示しておく。
プロジェクトの名前を「myapp2」とする。

$ cd ~/work/sbcl  # 場所はお好みで。
$ ros run
(ここからはSBCL内。先頭の*はプロンプト。)
* (ql:quickload :cl-project)
* (cl-project:make-project #P"myapp2" :author "kitemw")  ; "myapp2"と"kitemw"はお好みで。
(CTRL+DでSBCLを終了)

これで、以下のようなディレクトリの中にファイルが作成される。

$ tree myapp2
myapp2
|-- README.markdown
|-- README.org
|-- myapp2-test.asd
|-- myapp2.asd
|-- src
|   `-- myapp2.lisp
`-- tests
    `-- myapp2.lisp

2 directories, 6 files

作成したけどそのあとは?

cl-projectで作成したファイルだけでは、実は何もできない。ただ骨組みを作っただけ。
そもそもプロジェクトでできることと言ったら?やろうと思えば何でもできるのだが、

  • スタンドアロンで、ファイルを読み、計算し、ファイルや画面に結果を出力するプログラム … 可能。メインルーチンを含むエントリーファイルを追加する。
  • ライブラリとして動作するプログラム。すなわち他のプログラムから値をもらい、計算し、元のプログラムに結果を返すプログラム … 可能。

さて何をするか。当面の目的としてフィボナッチ数列を計算するプログラムでも作ってみますか?
では今回はスタンドアロンで動作するプログラムを書いてみる。ライブラリ作成は今回やらない。
上で作成したmyapp2プロジェクトを使うことを考えてみる。これのどこをどうすればよいかについては、だいたい次のような感じで作成していく。

  1. メインルーチン … 新しくエントリーファイルを作成する。フィボナッチ数列を作る関数を呼ぶ。
  2. サブルーチン … src/myapp2.lispの中に、フィボナッチ数列を作る関数を格納する。

プログラムのコアと言える部分(フィボナッチ数列を計算する部分)はsrc/myapp2.lispの中に入る。そしてあとはそれを呼ぶための一連の手続きを書くだけなのだが、そちらのほうが割と目立つので難しく見え、Common Lisp開発をhesitateさせる原因になっている気がする。

実行可能のエントリー(main)ファイルを作成する

cl-projectで作成したmyapp2を起動させるためにはmainを含むファイルが必要で、作成する方法がちゃんと用意されている(ros init)。エントリーファイルの名前は何でもよく、ファイルの場所もどこに作成してもよいが、一番わかりやすい場所はやはりmyapp2ディレクトリだろう。

$ cd ~/work/sbcl/myapp2
$ ros init main
Successfully generated: main.ros

main.rosというファイルが作成された。中身を見る。

$ cat main.ros
#!/bin/sh
#|-*- mode:lisp -*-|#
#|
exec ros -Q -- $0 "$@"
|#
(progn ;;init forms
  (ros:ensure-asdf)
  ;;#+quicklisp (ql:quickload '() :silent t)
  )

(defpackage :ros.script.main.3735336286
  (:use :cl))
(in-package :ros.script.main.3735336286)

(defun main (&rest argv)
  (declare (ignorable argv)))
;;; vim: set ft=lisp lisp:

ファイルの中身はほとんど「呪文」のようなもので中身はよくわかっていないが全部理解する必要もなさそうだ。
ちなみにこのファイルを何も書き換えずにそのまま実行すると、もちろん何もしないで終わる。

$ ./main.ros
$   (←何も出力せず終了した。)

サブルーチン(まだ作成していない)とのやりとりを行うため書き換える箇所は2箇所ある。;;#+quicklispの行と、main関数だ。ほかのところはそのままでよい。むしろいじらないほうがよいと思う。
実際の方法は次回以降に。