- Common LispのSBCLで
defconstantを使っても、C++のconstexprのような即値埋め込みは保証されません。 - 理由はCommon Lispが対話的な再定義を前提に設計されていて、コンパイラが安全策をとるため。
- 性能を引き出すには
defconstantよりdeclare (type ...)による型宣言とdeclaim (optimize ...)を使います。 fixnumやdouble-floatで型を明示するとボックス化を避けてレジスタ上で直接演算できます。
1. 他の言語での「定数」は何をしているか
C言語やC++では、不変な値は定数にすると計算効率を上がることがあります。
Common LispのSBCLでも同じような定数宣言を期待したのですが、どうもdefconstantは、コンパイラへの最適化ヒントとしてはちょっと弱い存在のようです。
1.1. C言語の #define マクロ
C言語では、定数として扱われる値を #define で定義します。
#define SCALE 1024
int process(int x) {
return x * SCALE;
}Code language: PHP (php)
#define は、コンパイル前にソースコードを機械的に置き換える単純な仕組みです。
そのため SCALE はコンパイル時にはすでに 1024 に展開されていて、
return x * 1024;Code language: JavaScript (javascript)
と同じコードとして扱われます。
この数値は、 即値(immediate) です。
これは、ヒープオブジェクトへのポインタではなく、値そのものがオブジェクト表現に直接埋め込まれる方式を指します。
つまり、変数としてメモリを確保する必要はなく、再代入や再定義の概念もありません。
ただし、構文的なチェックがないため、複雑な式は括弧で包んでおかないと、思わぬ誤動作が紛れ込むこともあります。
1.2. C++のconstexprとconst
C++では、constも似た動作をしますが、defineの代替なら C++11で導入された constexprのほうが適しています1。
constexprはコンパイル時に値が確定することをコンパイラに約束します2。
constexpr int scale = 1024;
int process(int x) {
return x * scale;
}Code language: JavaScript (javascript)
コンパイラはscaleを即値として埋め込んでよいと確信できます。
これも再定義の余地はなく、値は不変です。
2. Common Lispのdefconstant
一方、Common Lisp の defconstant。
これは、名前付きの値が変更されないことを宣言する定義形式です。
ただし、C++ の constexpr のように「必ずコンパイル時に即値として展開される」ことを強く保証するものではありません。
(defconstant +scale+ 1024)
(defun process (x)
(* x +scale+))
defconstantは「この名前を定数として使う意図がある」という宣言です3。
処理系によっては定数畳み込みが起きることがありますが、保証はありません。
理由は言語の設計思想に根差しています。
2.1. 対話的再定義との共存
Common Lispは、REPLで動かしながらコードを積み上げていく開発スタイルを前提にしています。
コンパイラが+scale+を即値として全面的に埋め込んでしまうと、あとからdefconstantを再評価したときに、すでにコンパイルされた関数だけが古い値を持ち続けます。
(defconstant +scale+ 1024)
(defun process (x) (* x +scale+))
;; あとでREPLで再評価しようとすると…
(defconstant +scale+ 2048) ; 処理系によっては警告またはエラー
同じ値での再評価は許容する処理系が多いですが、別の値への再定義は通常エラーになります4。
2.2. 値の不変性と名前の不変性は別
C++のconstexprは、名前も値も変わらないことをコンパイル時に確定します。
しかし、Common Lispでは「名前への再代入をしない」と「値そのものが不変である」は別の話です。
;; 配列を定数名に束縛した場合
(defconstant +buf+ (make-array 10 :initial-element 0))
;; 名前への再代入ではないので、これは防げない
(setf (aref +buf+ 0) 99)Code language: CSS (css)
数値や文字のような不変オブジェクトなら問題ありませんが、配列やハッシュテーブルのような可変オブジェクト、つまり中身を書き換えられるオブジェクトが来た場合、コンパイラは即値として扱いにくくなります。
2.3. コンパイル時と実行時の分離
Common Lispでは、ファイルをコンパイルして.faslファイルを作り、あとからロードする運用が普通です5。
コンパイル時に見えている定数定義と、実行時にロードされる環境が完全に一致するとは限りません。
コンパイル順やロード順によって前提が変わりうるため、コンパイラは安全策を取ります。
3. 最適化に効く型宣言
ちなみに、Common Lispのコンパイラが重視するのは、値が定数かどうかより、型が明確かどうかです。
(declaim (optimize (speed 3) (safety 0)))
(defun add (x y)
(declare (type fixnum x y))
(the fixnum (+ x y)))Code language: PHP (php)
fixnumはプラットフォーム依存の高速整数型で、多くの処理系でマシンのネイティブ整数幅に対応します6。declareで型を伝えると、コンパイラはボックス化を避けた整数演算を生成できます。theフォームは戻り値の型をコンパイラに伝え、戻り値のチェックも省きます7。
(defun add (x y)
(+ x y))
xとyの型が不明なので、コンパイラは汎用の加算ルーチンを使います。
整数か浮動小数点か、あるいはビッグナムかもしれない。
型宣言がなければ、Common Lispは浮動小数点演算でもオブジェクトをヒープに確保することがあります。これをボックス化といいます8。
型を明示すると、レジスタ上で直接演算できるようになります。
浮動小数点の場合は、
(declaim (optimize (speed 3) (safety 0)))
(defun scale-value (x factor)
(declare (type double-float x factor))
(the double-float (* x factor)))Code language: PHP (php)
3.1. defconstantと型宣言を組み合わせる
defconstant単体よりも、型情報を合わせて与えることで処理系が最適化しやすくなります9。
(defconstant +scale+ 1024)
(declaim (type fixnum +scale+))
(defun process (x)
(declare (type fixnum x))
(the fixnum (* x +scale+)))Code language: PHP (php)
数値リテラルの場合、処理系によってはここで定数畳み込みが走ります。
4. インライン展開との組み合わせ
inline宣言も同様に性能に効きます10。
関数呼び出しのオーバーヘッドがなくなるからです。
(declaim (inline square))
(defun square (x)
(declare (type fixnum x))
(the fixnum (* x x)))
(defun sum-of-squares (a b)
(declare (type fixnum a b))
(the fixnum (+ (square a) (square b))))Code language: PHP (php)
周囲の型情報と合わさることで、より強い最適化が可能になります。
5. まとめ
defconstantが最適化に弱いのは、Common Lispが「再定義できる対話的な環境」を前提に設計されているからです。
コンパイラが大胆な置換を安全に行えるだけの保証を、言語仕様が与えていません。
| 言語 | 定数の仕組み | コンパイラへの保証 | 最適化への効き |
|---|---|---|---|
| C | #define | 置き換え | 強い |
| C++ | const | 再代入不可(ポインタ経由は別) | やや強い |
| C++ | constexpr | コンパイル時確定、再定義不可 | 強い(即値埋め込み保証) |
| Common Lisp | defconstant | 処理系依存 | 限定的 |
| Common Lisp | declare (type ...) | コンパイラへの直接ヒント | やや強い |
性能を引き出したいなら、defconstantより型宣言と最適化宣言を中心に置くのが実態に合っています。defconstantは「この名前は変えない意図がある」という意味の明確化として使い、最適化はコンパイラに型情報を渡す形で行うのが、Common Lispらしいアプローチです。
constはポインタやリファレンス経由での変更を防げない場合があります。たとえばconst int* pはpの指す先の変更を禁じますが、int* const pはポインタ自体への再代入を禁じるだけで、指す先の値は変更できます。 – constexpr (C++) | Microsoft LearnconstexprはC++11で導入されたキーワードです。C++14以降は記述できる式の制約が緩和され、より複雑な処理をコンパイル時に評価できるようになりました。 – constexpr specifier (since C++11) – cppreference.com- Common Lispでは定数名を
+name+のようにプラス記号で囲む慣習があります。グローバル変数の*name*(アスタリスクで囲む「イヤーマフ」と呼ばれる記法)と区別するためのコーディング規約です。 – Common Lisp Style Guide - Common Lisp HyperSpec(CLHS)は「別の値での
defconstant再定義は結果が未定義(consequences are undefined)」と規定しています。SBCLではDEFCONSTANT-UNEQLコンディションが発生します。 – CLHS: Macro DEFCONSTANT .faslはFast Load(高速ロード)の略です。処理系によってファイル拡張子が異なり、SBCLでは.fasl、CLISPでは.fas、LispWorksでは.ofaslなどを使います。 – Common Lisp – Wikipedia- CLHSは
fixnumの範囲を「少なくとも-2^15から2^15 - 1の範囲を含む」と定めており、具体的な値は処理系に任されています。64ビット版SBCLではmost-positive-fixnumが4611686018427387903(62ビット符号付き整数の最大値)です。 – The Common Lisp Cookbook – Numbers theフォームは宣言した型と実際の値が一致しない場合、動作は未定義です。safetyを高く設定している環境では型チェックが行われますが、(optimize (safety 0))のもとでは検査なしにそのまま実行されます。 – CLHS: Special Operator THE- ボックス化とは、数値などのプリミティブな値をヒープ上のオブジェクトとしてラップする処理です。毎回メモリ確保とGCの対象になるため、ループ内での繰り返しが多いほどパフォーマンスへの影響が大きくなります。 – The Common Lisp Cookbook – Performance Tuning and Tips
declaimはファイルのトップレベルで使うグローバル宣言で、コンパイル時に効果を持ちます。一方declareは関数本体の先頭など、特定のスコープ内でのみ有効なローカル宣言です。 – CLHS: Declaration TYPE(optimize (speed 3) (safety 0))は最適化品質を指定します。speedは実行速度(0〜3)、safetyはエラーチェックの厳しさ(0〜3)です。safety 0は境界チェックや型チェックを省くため、バグの発見が難しくなります。本番コードではsafety 1以上を保ちながらホットパスだけ絞る運用が一般的です。 – SBCL User Manual – Efficiency