【Common Lisp】
svrefの役割とarefで十分な理由
(歴史と効率性)

  • Common Lispにはベクタアクセスにarefとsvrefの2つがあるが、svrefはsimple-vector専用でarefのサブセットにすぎない。
  • svrefを使ってもアクセス速度は上がらず、コンパイル時の型チェックとコードの意図表明がおもな用途になっている。
  • 現代のSBCLでは、arefに型宣言を組み合わせるのが柔軟性と性能の両面で実用的な選択になる。

関連記事

1. svrefはarefに含まれる

Common Lisp には、配列要素にアクセスするための関数として、汎用の arefsimple-vector 専用の svref があります。

しかし、svref を使ったからといって配列へのアクセス速度が上がるわけではありません。
では、なぜ svref は存在するのか、気になったので整理します。

まずは、aref は、配列要素を参照するための特殊形式です(array element reference)。

;; aref はどんな配列にも使える
(let ((v (vector 1 2 3)))
  (aref v 0))         ; => 1

(let ((v2d (make-array '(2 3) :initial-contents '((1 2 3) (4 5 6)))))
  (aref v2d 1 2))     ; => 6

(let ((v (make-array 3 :adjustable t :initial-contents '(1 2 3))))
  (aref v 0))         ; => 1  adjustable でも問題なしCode language: Lisp (lisp)

Common Lispの配列には、要素型が特化した配列だけでなく、多次元配列や可変長配列などさまざまな配列があり、aref はどれでも使えます。

一方、svref は、 simple-vector にしか使えません(simple vector reference)。
simple-vector とは、任意の要素型を保持できる1次元配列ベクタで、サイズ調整や fill pointer は持ちません1

(let ((v (vector 1 2 3)))       ; #(1 2 3) は simple-vector
  (svref v 0))                  
;=> 1Code language: Lisp (lisp)

逆に、simple-vector 以外に svref を使うと、SBCLではエラーになります。

;; adjustable vector
(let ((v (make-array 3 
                     :adjustable t 
                     :initial-contents '(1 2 3))))
  (svref v 0))
; WARNING: Derived type of V is (AND (VECTOR T) (NOT SIMPLE-ARRAY)),
;          conflicting with its asserted type SIMPLE-VECTOR.
; ERROR: Value of V in (THE SIMPLE-VECTOR V) is #(1 2 3), not a SIMPLE-VECTOR.
Code language: Lisp (lisp)
1. svrefはarefに含まれる

1.1. svrefを使うメリットはあるのか

つまり、svref の適用範囲は aref のサブセットで、svref が使える場面では aref も必ず使えますが、逆は成り立ちません。

一つのメリットがあるとすれば、それはコンパイル時のチェックです。

svref(aref (the simple-vector v) i) と等価と定義されており、the による型表明が根拠になっています2

; 2次元配列 v = #2A((0 0 0) (0 0 0))
(let ((v (make-array '(2 3))))
  (svref v 0))
; ERROR: not a SIMPLE-VECTOR.

(let ((v (make-array '(2 3))))
  (svref v 0 0))
; ERROR: wrong arguments.
Code language: Lisp (lisp)

SBCLが型を推論できる文脈では、simple-vector でない値を渡すと、実行前に WARNING が出て気づくことができます。
ただし、型が不明な文脈では警告も出ません3

;; let で作った配列は型が確定するので WARNING が出る
(let ((v (make-array 3 :adjustable t :initial-contents '(1 2 3))))
  (svref v 0))
; WARNING: Derived type of V is (AND (VECTOR T) (NOT SIMPLE-ARRAY)),
;          conflicting with its asserted type SIMPLE-VECTOR.

;; 関数の引数は型が不明なので WARNING は出ない
(defun maybe-bad (v)
  (svref v 0))Code language: Lisp (lisp)

もう一つは、読み手への意図表明です。
svref と書くだけで「ここは simple-vector 前提だ」と伝わります。
ただしこれはコンパイラ向けではなく、コードを読む人間向けの情報です。

2. 最適化なら型宣言

コンパイラ向けに変数が simple-vector であることを伝えるには、svref を使うことより、型宣言を与えることのほうが最適化に効きます。
型宣言なしの aref は、コンパイラが配列の種類を判断できないため汎用的なコードを生成します。

;; 型宣言なし: SBCLは配列の種類を知らないので汎用パスになる
(defun sum-generic (v n)
  (let ((acc 0))
    (dotimes (i n acc)
      (incf acc (aref v i)))))Code language: Lisp (lisp)

しかし、simple-vector だとわかっていれば、declare で伝えることができます。

;; simple-vector と宣言: svref を使ったときと同等に近い最適化がかかる
(defun sum-sv (v n)
  (declare (type simple-vector v)
           (type fixnum n))
  (let ((acc 0))
    (dotimes (i n acc)
      (incf acc (aref v i)))))Code language: Lisp (lisp)

2.1. 要素型を宣言すると simple-vectorではなくなる

要素型を任意に取らず、整数配列に絞れば、さらに最適化が進みます。

;; 要素型まで宣言: ボクシングが省かれ、より速くなる
;; ボクシングとは、整数などのプリミティブ値をヒープ上のオブジェクトに包む処理です
(defun sum-fixnum (v n)
  (declare (type (simple-array fixnum (*)) v)
           (type fixnum n)
           (optimize (speed 3) (safety 0)))
  (let ((acc 0))
    (declare (type fixnum acc))
    (dotimes (i n acc)
      (incf acc (aref v i)))))Code language: Lisp (lisp)

safety 0 では配列境界チェックも省かれるため、最大限の性能が必要な場合は宣言と最適化設定の組み合わせが効きます4
svrefaref かという選択より、この宣言の有無の方が実行速度への影響がずっと大きいです。

SBCLのリリースノートでは、非 simple な配列でも要素型が判明していれば aref の速度が改善されたと記載されており、aref 自体が十分最適化の対象であることがわかります。

ただし、このような要素型を指定した配列には、かえって svrefは使えません。
simple-vectorではなく、 simple-array だからです。

;; 要素型を指定した simple-array
(let ((v (make-array 3 
                     :element-type 'fixnum 
                     :initial-contents '(1 2 3))))
  (svref v 0))
; ERROR: not a SIMPLE-VECTOR.Code language: PHP (php)

2.2. 他の処理系でも同じ傾向か

大筋では同じです。

SBCL、CCL、LispWorks、ECL のようなネイティブコード生成系では、型宣言と optimize 宣言が最適化の中心です。
LispWorks は speedsafety の設定に応じてアクセサのインライン化や型チェックの方針を変えると明記しており、ECL は「宣言、とくに型宣言はコンパイル済みコードの効率を上げる」としています。

JVM上で動作する ABCL は少し事情が違い、機械語レベルの配列最適化という文脈では SBCL 等とは異なります。
ただし svref を選ぶ意味が薄い、という結論は変わりません。

3. svrefの歴史的背景と後方互換性

実は、もともと Common Lisp には、「simple な対象には専用アクセサを置く」という設計方針が見られました。

歴史的背景と後方互換性 初期Lisp 処理系間の差 型推論が限定的 設計方針 simple専用アクセサを配置 名前で型を宣言し効率化 現代SBCL 型宣言で十分 aref が最適化対象 専用アクセサの対応表 svref ← simple-vector cf. aref schar / char ← simple-string cf. aref

これは、初期の Lisp 実装では、処理系間の差や型推論能力の限界がより顕著で、「これは単純なベクタだ」と名前で宣言することで、効率化できる場面があったと考えられます。

アクセサ対象
arefsvref配列一般とsimple-vector
charschar文字列と simple-string
bitsbitビットベクタと simple-bit-vector

ただし、現代の SBCL では aref に型宣言を組み合わせれば十分最適化されることが多く、svref の役割は後方互換性と意図の伝達が中心になっています。

CLHSでは char についても (char s j) == (aref (the string s) j) と定義されており、専用アクセサと汎用アクセサの関係は svrefaref のそれと同じ構造になっています5

なお、X3J13ではAREF-1Dという議論が行われ、多次元配列への1次元アクセスを効率よく扱うための関数として row-major-aref が提案・採用されており、配列アクセスの効率化への問題意識は仕様策定時から存在していたことがわかります6

4. まとめ

現代において、svrefを知っておく主な意義は、古いコードを読むためでしょう。
あえて svref を使うとすれば、「これは simple-vector 前提で書いている」とコード上で明示する、人間向けの意図表明としては機能します。
ただ、SBCLなどにおけるコンパイラへの効果は、型宣言と比べると限定的です。

実務的な使い分けはこうなります。

  • 普段は aref で書く
  • 性能を詰めるなら svref より (declare (type (simple-array ...) v)) を先に検討する
  • 古いコードで svref を見たときは「ここは simple-vector 前提だ」と読む
  • 自分で svref を書くなら、型の意図を明示したい場面に限定してよい

svref は今も仕様上正しく機能しますが、SBCLで新たに書くコードでは aref に型宣言を組み合わせる方が、柔軟性と最適化の両面で扱いやすいです。

  1. CLHSでは simple-vector を「他の配列にdisplaceされておらず、fill pointerを持たず、明示的に調整可能でなく、任意の型の要素を保持できるベクタ」と定義しています。型としては (simple-array t (size)) と同一です。 – CLHS: Type SIMPLE-VECTOR
  2. CLHSの svref の項には (svref v i) == (aref (the simple-vector v) i) と明記されています。the は型を表明するspecial formで、違反時の挙動は処理系依存ですが、SBCLでは実行時エラーになります。 – CLHS: Accessor SVREF
  3. SBCLのマニュアルでは、型宣言はデフォルトでassertionとして扱われ、実行時に精密な型チェックが行われると説明されています。型の不一致が検出できるかどうかはコンパイラの型推論能力と文脈に依存します。 – SBCL User Manual: Declarations as Assertions
  4. SBCLマニュアルでは、safety が0のとき「すべての宣言がチェックなしで信用され、引数の個数チェックと配列境界チェックも無効化される」と明記されています。ヒープが破損するリスクがあるため、デバッグ完了後の最終チューニング時にのみ使うことが推奨されています。 – SBCL User Manual: Compiler Policy
  5. CLHSの char/schar の項には (char s j) == (aref (the string s) j) と記載されています。svrefcharscharbitsbit はいずれも同じ設計パターンに従っています。 – CLHS: Accessor CHAR, SCHAR
  6. X3J13のAREF-1D議論では、異なるrankを持つ配列を効率的に扱うために row-major-aref の導入が提案されました。displaced arrayを一時的に作る必要をなくし、不要なconsを避けることが目的でした。 – CLHS: Issue AREF-1D Writeup