1. float だと精度が足りないことがある
プログラミングの問題で分数や無理数を出力するとき、小数表示が必要になります。
このとき、誤答の原因として精度の不足があります。
AtCoder の 問題で float を使ったら 不正解(WA) になり、double-float に変えることで 正解(AC) になりました。
(float 1/3)
; => 0.33333334
(float (/ 1 3) 1.0d0)
; => 0.3333333333333333d0
(coerce 1/3 'double-float)
; => 0.3333333333333333d0Code language: Lisp (lisp)
(float x) は、引数ひとつで呼ぶと、*read-default-float-format* の初期値が single-float なので単精度(有効桁数がおよそ 7 桁)に変換されます1。
問題は「絶対誤差または相対誤差が 10⁻⁹ 以下」を要求していたので、 7 桁では足りませでした。
倍精度の 64 ビット形式なら有効桁数が約 16 桁になるので、この要件を満たせます。
1.1. 浮動小数点数の型と精度
Common Lisp の浮動小数点型は 4 種類あります。
| 型 | 精度の目安 | リテラル例 |
|---|---|---|
short-float | 約 7 桁 | 1.0s0 |
single-float | 約 7 桁 | 1.0f0 / 1.0 |
double-float | 約 16 桁 | 1.0d0 |
long-float | 実装依存 | 1.0l0 |
単精度は IEEE 754 の 32 ビット形式で、有効桁数がおよそ 7 桁しかありません2。
2. 倍精度小数のリテラル(d0)
リテラルとして書くときは指数部に d を付けます3。
0.0d0 ; double-float のゼロ
1.0d0 ; double-float の 1Code language: Lisp (lisp)
2.1. double-float への変換方法(coerce)
;; 分数 → single-float(NG)
(float (/ 3 7)) ;=> 0.42857143 (7桁程度)
;; 分数 → double-float(OK)
(float (/ 3 7) 1.0d0) ;=> 0.42857142857142855d0
(coerce (/ 3 7) 'double-float) ;=> 0.42857142857142855d0Code language: Lisp (lisp)
(float x 1.0d0) という書き方があります。
第 2 引数にプロトタイプを渡すと、その型に合わせて変換してくれます。
あるいは、分数から変換するには coerce を使います4。
3. 出力で d0 を消す方法(format “~f”)
double-float を ~a や princ で出力すると、末尾に d0 が付いてしまいます。
(princ 0.42857142857142855d0)
;=> 0.42857142857142855d0 ← ジャッジが読めないCode language: Lisp (lisp)
format の ~f ディレクティブを使うと、d0 なしの十進小数として出力できます5。
(format t "~f" 0.42857142857142855d0)
;=> 0.42857142857142855Code language: Lisp (lisp)
競プロのコードでは (format t "~f" result) が定番です。
桁数を明示したい場合は ~,15f のように書くと小数点以下 15 桁で出力できます。
問題の要求精度がシビアなときに使えます。
3.1. 【補足】デフォルトは単精度
このデフォルト値が single-float のままになっている背景には歴史的な事情があります。
Common Lisp の標準が策定された時代、Symbolics 社のハードウェアが単精度を直接即値として扱えたのに対し、倍精度はそうではありませんでした。
その速度差が仕様に影響を残しています6。
現在の SBCL では速度差はほぼなく、標準仕様として固定されているためデフォルトは変わっていません。
4. まとめ
小数の精度が求められる問題では、次の 3 点を押さえておけば対処できます。
- 変換には
(coerce x 'double-float)または(float x 1.0d0)を使う - リテラルは
1.0d0のようにdを付けて書く - 出力は
(format t "~f" x)でd0サフィックスを取り除く
Common Lisp は分数を正確に保持できるので、計算の最後だけ coerce する書き方が自然です。(/ 1 3) は計算の途中でずっと分数 1/3 のまま扱われ、coerce を呼んだ瞬間にはじめて浮動小数点数に変換されます7。
他の言語で 1.0 / 3 と書いた時点で精度が落ちるのとは異なる動きです。
*read-default-float-format*の初期値はsingle-float。指数マーカーがeまたはEのリテラルもこの設定に従います。 – CLHS: Variable READ-DEFAULT-FLOAT-FORMAT- 単精度浮動小数点は有効桁数が約 7 桁、倍精度は約 16 桁。定数
single-float-epsilonとdouble-float-epsilonで各精度の限界を確認できます。 – The Common Lisp Cookbook – Numbers - 指数マーカー
s/Sが short-float、f/Fが single-float、d/Dが double-float、l/Lが long-float に対応します。マーカーを省略した場合やe/Eを使った場合は*read-default-float-format*の設定に従います。 – CLHS: 2.3.2.2 Syntax of a Float coerceは第 1 引数のオブジェクトを第 2 引数の型に変換します。result-typeがdouble-floatの場合、実数を符号と大きさを保ったまま倍精度に変換します。変換が不可能な場合はtype-errorを発生させます。 – CLHS: Function COERCE~fは Fixed-Format Floating-Point ディレクティブ。完全な書式は~w,d,k,overflowchar,padcharFで、wが全体幅、dが小数点以下の桁数です。パラメータをすべて省略すると可変幅で出力され、d0などの型サフィックスは付きません。 – CLHS: 22.3.3.1 Tilde F: Fixed-Format Floating-Point- Symbolics 社のコンパイラは単精度を即値として扱えたが倍精度は不可だったため、単精度演算の方が大幅に高速だった。この経緯が
*read-default-float-format*のデフォルトがsingle-floatのまま固定された背景のひとつとされています。 – Two understandable deficiencies in Common Lisp - Common Lisp の ratio 型は分子と分母の整数ペアとして正確に保持されます。
coerceやfloatを呼ぶまで浮動小数点への変換は起きないため、途中の計算で精度が落ちません。 – The Common Lisp Cookbook – Numbers