Delphi Programming / Object Pascal
サイトのトップページへ リンクのページへ ロゴマーク
[掲載 2007年09月02日] [更新 2018年11月24日] Delphi サンプルプログラム集
   
890_計算誤差 ( 数値計算の誤差 ) と多倍長演算
動作確認等 Windows 7 U64(SP1) + Delphi XE(UP1) Pro, 一部は他のバージョンの Delphi
890_CalcError.zip [10,589 KB] 2018年11月24日版 (EXE 同梱)







計算誤差 ( 数値計算の誤差 ) と多倍長演算


図の下のコードを見てください.
このコードでは,実数型の変数 R1 に 0.1 を代入しています.この R1 の値と 0.1 という数値が同じであるかを確認するサンプルです.結果は下図の通り「等しくない」となります.



図1
下のコードの実行結果

リスト1
procedure TForm1.Button1Click(Sender: TObject);
var
  R1 : Single;
begin
  R1 := 0.1;

  if R1 = 0.1 then begin
    MessageBox(Handle, '同じ値です', '結果', MB_ICONINFORMATION);
  end else begin
    MessageBox(Handle, '同じ値ではありません', '結果', MB_ICONWARNING);
  end;
end;


コミュニティの場で,たまに,計算結果が正しくない,という質問があります.例えば
  •  "0.56" となるハズなのに "0.5600000000000001" になってしまいます
  •  "0.56" となるハズなのに "0.5599999999999917" になってしまいます
  •  異なる変数に同じ値が入っているはずなのに,= で比較しても True になりません

というものです.例をあげたらきりがありません.対象の数値や結果は,数値の型や,どんな計算をしたか,結果をどのように取得したかにもよります.

コンピュータは昔「電算機」とか「電子計算機」と呼ばれていました.科学技術計算でコンピュータを使用する者にとって,上記のような現象は,必要不可欠な知識であり,常識といっても過言ではないでしょう.しかし,そうでない方にとっては,数学的に正確な値が計算できないということが不思議なのかも知れません.そこで,この質問に対する回答となるような記事を書いてみました.

結論から言うと,これらは全て,ごく普通の現象です.

コンピュータによる計算では,計算誤差と必要な有効桁数を考慮する必要があるということです.
上の最初の質問の場合,計算結果の最後の 1 が問題となるのであれば,誤差のない計算方法を考えなければなりません.そうでなければ必要な桁数だけの値で十分です.

例えば,最初の 2 つの質問の場合,小数点以下 5 桁の精度でよければ,次のようにすれば,どちらの結果も 0.56000, あるいは 0.56 と表示されます.表示桁数の範囲では誤差がないことになります.
実数の値を比較する場合も範囲で比較します.等号たけでは,前述のように,等しくないという結果となることがあります.



図2
数値を四捨五入して表示する
  • FormatFloat 関数で指定桁数だけ表示すると,四捨五入されてどちらも 0.56000 となる
  • 四捨五入して FloatToStr 関数で表示すると,どちらも 0.56 となる

リスト2
//=============================================================================
//  実数値を四捨五入して表示する例
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  E, F : Double;
begin
  E := 0.5600000000000001;
  F := 0.5599999999999917;

  Memo1.Lines.Add(' E: ' + FormatFloat('##0.00000', E));
  Memo1.Lines.Add(' F: ' + FormatFloat('##0.00000', F));

  // 数値を小数点以下の指定桁で四捨五入する
  // SimpleRoundTo の使用には uses に Math が必要
  Memo1.Lines.Add(' E: ' + FloatToStr(SimpleRoundTo(E, -5)));
  Memo1.Lines.Add(' F: ' + FloatToStr(SimpleRoundTo(F, -5)));
end;

//=============================================================================
//  実数値が指定範囲内にあるかを判定する例
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  LValue  : Single;
  LRefVal : Single;
  LIncVal : Single;
  LCount  : Integer;
begin
  Memo1.Lines.Clear;

  // 数学的には LValue と LRefVal は同じ値になる
  LValue  := 0.0;
  for LCount := 1 to 1000 do begin
    LValue := LValue + 0.0001;
  end;
  LRefVal := 0.1;

  // Double 型であれば 1.0E-13 らいまで可能
  LIncVal := Abs(LRefVal * 1.0E-4);
  if (LValue >= (LRefVal - LIncVal)) and (LValue <= (LRefVal + LIncVal)) then begin
    Memo1.Lines.Add('範囲内');
  end else begin
    Memo1.Lines.Add('範囲外');
  end;
end;


科学技術計算では,長い有効桁数が必要な分野もありますが,多くの場合数桁で十分です.物理的な計測で得られる値が 3 桁の場合,その値を元にした計算結果の精度は,理論的に,3 桁を越えることはできません.
一方,金額計算では,たとえ大きな金額であっても,1 円でも誤差があると困る場合があります.

「数学は自然が語りかける言葉」,そしてコンピュータはその数学を実際に計算する道具です.したがって,科学技術計算における数値計算の誤差は重要な課題です.
科学技術の分野では,測定の結果,計測値,あるいは計算結果を表わす時,以下のように,精度がわかるように,桁数を明確に示すことが一般的に行われています.


 2.0     精度が小数点以下 1 桁までの場合
 2.00    精度が小数点以下 2 桁までの場合
 2.000   精度が小数点以下 3 桁までの場合





01_実数値の表現  -  コンピュータは有限桁の数値しか扱えない


コンピュータでは,有限桁の数値しか扱えません.
コンピュータの数値には,数学でいう「無限大」や「無限小」という値はありません.したがって,数学の定義通りの積分や微分の計算はできません.コンピュータによるこれらの計算は,一般的に「数値計算」と呼ばれています.数値計算」における演算誤差は,主に実数を使用する計算で発生します.

Delphi における実数の内部表現は,IEEE 754 仕様の浮動小数点数形式となっています.4 バイトの実数型 Single の仮数部は 23 ビットで,10 進数では高々 6 桁です.8 バイトの実数型 Double の仮数部は 52 ビットで,10 進数では高々 15 桁です.



図3
実数型の仮数部の最大値
  • 4 バイトの実数型は 10 進数で 7 桁まで
    10 進数としての最大有効桁は 6 桁まで
  • 8 バイトの実数型は 10 進数で 16 桁まで
    10 進数としての最大有効桁は 15 桁まで

リスト3
procedure TForm1.Button1Click(Sender: TObject);
var
  LSingle : UInt64;
  LDouble : UInt64;
begin
  LSingle := $7FFFFF;        // 23 ビット
  LDouble := $FFFFFFFFFFFFF; // 52 ビット

  Memo1.Lines.Add(UIntToStr(LSingle));
  Memo1.Lines.Add(UIntToStr(LDouble));
end;


[備考]
実数値を指数形式で変数に代入するには,以下のようにします.
      LValue := 1.23456E-10;
指数部の E は,大文字でも小文字でも構いません.E は底が 10, つまり 10 のベキ乗の意味です. 一般のベキ乗は Math ユニットの Power 関数または IntPower 関数を使用します.





02_2 進数による数値演算の注意


コンピュータの内部では、数値は 2 進数で記憶されます.2 進数は,数値を 0 または 1 で表すものです.この 2 進数の 1 桁を「ビット」と言います.2 進数の 0 と 1 の羅列では冗長でわかりにくいので,通常は,2 進数の 4 桁 (4 ビット) を 1 桁とする 16 進数による表記を使用します.
どんな数値も 2 進数の有限桁で処理します.このため,10 進法の有限小数の大部分は無限小数となり,コンピュータの内部では正確に表現できません.
これが,実数計算で誤差が生じる原因です.これを一般に「丸め誤差」と呼んでいます.
当然ですが,この他にも,コンピュータ内部で扱える数値の範囲外の値を扱うと,桁落ち,情報落ち,積み残し等による誤差が発生します.

コンピュータでは,数学でいう分数形式 (有理数) も扱えません.1 / 3 のような分数は,除算の結果として変数に代入することになります.この値は無限小数です.数値の 0.2 も,2 進数,16 進数では無限小数となります.しかし,コンピュータは有限桁しか扱えません.その結果,コンピュータ内部では 0.2 という数値は正確に表現できません.したがって,変数に代入した時点で丸め誤差が発生することになります.



10 進法 2 進法 16 進法
0.1 0.00011001100110011... 0.19999999999...
0.2 0.00110011001100110... 0.33333333333...
0.3 0.01001100110010010... 0.4CCCCCCCCCC...


実数を浮動小数点形数式 ではなく,小数点以下の桁数を限定した 固定小数点数形式 で処理すれば,その桁数だけは正確になります.固定小数点数形式というのは,コンピュータ内部で,整数部の桁数と小数点以下の桁数を固定して処理する実数型のことです.

Delphi では Currency 型が固定小数点数形式となっています.Currency 型では小数点以下 4 桁までを正確に計算することができますが,科学技術計算等で必要な大きな,あるいは小さな数値は扱えません.整数部は約 15 桁までに限られます.
数値を BCD (Binary Code Decimal・2 進化 10 進数) 形式で扱えば計算誤差を抑えられます.BCD は,内部の計算処理を 10 進法として処理します.Delphi で BCD 演算の機能を使用するには,uses に FMTBcd (Delphi XE2 以降では Data.FMTBcd) が必要です.



数値計算では,誤差を少なくするためのアルゴリズムがいろいろ考え出されています.
例えば,2 次方程式の根は,最初に大きな根の値を求め,次に小さな根の値を計算します.また,浮動小数点の実数値に同じ値を加算していくような場合,乗算を使用します.このような数値計算のアルゴリズム,定石は,市販の参考書の方が,より詳しい情報が得られます.





03_演算誤差のテスト


演算誤差を確認するためのテストプログラムです.この記事は計算機の誤差の解析が目的ではありませんので詳しい説明は省略します.実際に体験してみて下さい.

もしかしたら,このサンプルプログラムの例は極端だと思うかも知れません.
しかし,残念ながら,実際には,決してそうでもないのです.コンピュータによる数値計算の初心者が犯しやすい演算なのです.

ついでですが,科学技術計算で犯しやすいミスに,乱数を使用した計算があります.
コンピュータで扱う乱数は「疑似乱数」と言われています.開発言語に標準で用意されている乱数発生の関数類は,簡単なテスト用です,本格的な乱数としては利用できません.そのために,多くの乱数発生のアルゴリズムが研究されています.使用した乱数発生のアルゴリズムによって,計算の結果が違ってくることがあります.
そのため,科学技術の研究では,使用した乱数のアルゴリズムを明記するのが一般的です.


図4
実行結果
  • 数値の表示には TPanel を使用
  • Button1 0.1 に 10 を掛けた
  • Button2 0.1 を 10 回加算
  • Button3 0.1 を 10 回加算して 50 を掛ける
  • Button4 Currency 型で Button3 と同じ計算
  • Button5 BCD 演算で Button3 と同じ計算
  • Button6 有効桁数が減る例
    72366.573 - 72366.550 = 0.023
    となるはずであるが,0.023438 となってしまう
    このような演算を続けると誤差拡大の可能性がある
  • Button7 積み残しの例(加算しても増加しない)
    72366.573 + 0.00001 = 72366.5731
    なので,本当なら 72366.5731 となる

リスト4
演算誤差のテスト
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Button3: TButton;
    Button4: TButton;
    Button5: TButton;
    Button6: TButton;
    Button7: TButton;
    Panel1: TPanel;
    Panel2: TPanel;
    Panel3: TPanel;
    Panel4: TPanel;
    Panel5: TPanel;
    Panel6: TPanel;
    Panel71: TPanel;
    Panel72: TPanel;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
    procedure Button4Click(Sender: TObject);
    procedure Button5Click(Sender: TObject);
    procedure Button6Click(Sender: TObject);
    procedure Button7Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

uses FMTBcd;

{$R *.dfm}

//=============================================================================
//  0.1に10を掛ける
//  Button2Clickのコードと違い,コンパイラが誤差なく計算してくれる.これは言語
//  の仕様によって異なるので,他のコンパイラでも同じ結果となるとは限らない
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  A : Single;
begin
  A:=0.1*10;
  Panel1.Caption := FormatFloat('000.0000000000', A);
end;

//=============================================================================
//  0.1を10回加算しても数学的に意味の1.0とはならない例
//  例えば帳票類で等間隔に罫線を描画する時には注意が必要
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  A : Single;
  i : Integer;
begin
  A := 0.0;
  for i:=0 to 9 do begin
    A := A + 0.1;
  end;
  Panel2.Caption := FormatFloat('000.0000000000', A);
end;

//=============================================================================
//  ある値(実数値)を整数倍しても必ずしも数学的な整数倍にならない例
//  例えばButton2Clickの結果1.0000001192を50倍しても  50.000005960にはならない
//=============================================================================
procedure TForm1.Button3Click(Sender: TObject);
var
  A : Single;
  i : Integer;
begin
  A := 0.0;
  for i:=0 to 9 do begin
    A := A + 0.1;
  end;
  A := A * 50;
  Panel3.Caption := FormatFloat('000.0000000000', A);
end;

//=============================================================================
//  Currency 型
//  DelphiのCurrency型は小数点以下の4桁を整数として格納する固定小数点のデータ型
//  で,金額計算などでの丸め誤差が少なくなる
//  代入文や式で他の実数型と混在させた場合はCurrency型の値に対し自動的に10000で
//  の除算または乗算が行われる
//=============================================================================
procedure TForm1.Button4Click(Sender: TObject);
var
  A : Currency;
  i : Integer;
begin
  A := 0.0;
  for i:=0 to 9 do begin
    A := A + 0.1;
  end;
  A := A * 50;
  Panel4.Caption := FormatFloat('000.0000000000', A);
end;

//=============================================================================
//  FMTBcd.pasのBCD(2進化10進数表記 Binary Coded Decimal)ルーチンを使用すれば
//  誤差なく計算できるが非常に面倒
//  FMTBcdでは4ビットに10進の各桁を保持することにより10進演算を行っている.そ
//  のため長い桁も少なくとも加減算は誤差なしに扱える
//  このコードの実行にはusesにFMTBcdが必要
//=============================================================================
procedure TForm1.Button5Click(Sender: TObject);
var
  A : TBcd;
  B : TBcd;
  i : Integer;
begin
  A := NullBcd;
  B := StrToBcd('0.1');
  for i:=0 to 9 do begin
    BcdAdd(A, B, A);
  end;
  BcdMultiply(A, 50, A);
  Panel5.Caption := FormatBcd('000.0000000000', A);
end;

//=============================================================================
//  有効桁数の激減
//  Singleの有効桁数は7~8桁なので以下の計算では有効桁数が激減してしまう.
//=============================================================================
procedure TForm1.Button6Click(Sender: TObject);
var
  A : Single;
  B : Single;
  C : Single;
begin
  A := 72366.573;
  B := 72366.550;
  C := A - B;
  Panel6.Caption := FormatFloat('00000.000000', C);
end;

//=============================================================================
//  積み残し
//  Singleの有効桁数は7~8桁なので以下の計算では0.00001は事実上,加算されない
//  元のAと加算した結果のCの値は全く同じとなってしまう
//=============================================================================
procedure TForm1.Button7Click(Sender: TObject);
var
  A : Single;
  B : Single;
  C : Single;
begin
  A := 72366.573;
  B :=     0.00001;
  C := A + B;
  Panel71.Caption := FormatFloat('00000.000000', A);
  Panel72.Caption := FormatFloat('00000.000000', C);
end;

end.




04_実数型による違い


上のサンプルとは別の値で,実数型による違いを確認するためのコードです.
Currency 型の精度確認用のコードも追加しています.バージョン等の環境による違いはあるかも知れませんが,結果については省略します.「えっ?」と思うか,「ふ~ん」と思うかは貴方の感性にお任せいたします.


図5
設計時画面
  • 数学的に正しい値は以下の通り
  • Button1  0.1
  • Button2  0.1
  • Button3  0.1
  • Button4  0.1
  • Button5  0.001
  • Button6  0.0001

リスト5
実数型の違いを確認する
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Button3: TButton;
    Button4: TButton;
    Button5: TButton;
    Button6: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
    procedure Button4Click(Sender: TObject);
    procedure Button5Click(Sender: TObject);
    procedure Button6Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

//=============================================================================
//  Single(4バイトの単精度)型の場合
//  数値範囲 1.5 x 10^-45 .. 3.4 x 10^38  有効桁数 7~8
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  s : Single;
  i : Integer;
begin
  s := 0;
  for i := 0 to 9 do s := s + 0.01;
  ShowMessage(FloatToStr(s));
end;

//=============================================================================
//  Double型(8バイト)の場合
//  数値範囲 5.0 x 10^-324 .. 1.7 x 10^308  有効桁数 15~16
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  s : Double;
  i : Integer;
begin
  s := 0;
  for i := 0 to 9 do s := s + 0.01;
  ShowMessage(FloatToStr(s));
end;

//=============================================================================
//  Extended型(10バイト)の場合
//  数値範囲 3.6 x 10^-4951 .. 1.1 x 10^4932  有効桁数 19~20
//=============================================================================
procedure TForm1.Button3Click(Sender: TObject);
var

  s : Extended;
  i : Integer;
begin
  s := 0;
  for i := 0 to 9 do s := s + 0.01;
  ShowMessage(FloatToStr(s));
end;

//=============================================================================
//  Currency型(8バイト)の場合
//  数値範囲 -922337203685477.5808.. 922337203685477.5807 有効桁数 19~20
//  金額を扱うには十分な桁だろう
//=============================================================================
procedure TForm1.Button4Click(Sender: TObject);
var
  s : Currency;
  i : Integer;
begin
  s := 0;
  for i := 0 to 9 do s := s + 0.01;
  ShowMessage(CurrToStr(s));
end;

//=============================================================================
//  Currency型(8バイト)の場合
//  数値範囲 -922337203685477.5808.. 922337203685477.5807 有効桁数 19~20
//  Currency型が扱える小数点以下4桁目の値を加算
//=============================================================================
procedure TForm1.Button5Click(Sender: TObject);
var
  s : Currency;
  i : Integer;
begin
  s := 0;
  for i := 0 to 9 do s := s + 0.0001;
  ShowMessage(CurrToStr(s));
end;

//=============================================================================
//  Currency型(8バイト)の場合
//  数値範囲 -922337203685477.5808.. 922337203685477.5807 有効桁数 19~20
//  Currency型が扱える範囲を越えた小数点以下5桁目の値を加算
//=============================================================================
procedure TForm1.Button6Click(Sender: TObject);
var
  s : Currency;
  i : Integer;
begin
  s := 0;
  for i := 0 to 9 do s := s + 0.00001;
  ShowMessage(CurrToStr(s));
end;

end.




05_桁落ちの別例 整数型の有効桁数に注意


整数型の有効桁数による桁落ちの例です.このような単純な計算であれば,すぐ気づきますが,複雑で計算コードが大量にある場合,擬似乱数もそうですが,このようなことに注意しないと,もしかしたら物理学上の新発見をしてしまうかも知れません.


図6
設計時画面
図7
Button1クリック
  • 上の Edit の値を Extended 型の変数に代入後,整数型の Integer 型で整数にする
  • 桁落ちして,とんでもない値となる
図8
Button2クリック
  • 上の Edit の値を Extended 型の変数に代入後,整数型の Int64 型で整数にする
  • 桁落ちなしで,正常な結果となる

リスト6
整数型の有効桁数の違いによる桁落ちの例
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Edit1: TEdit;
    Edit2: TEdit;
    Button2: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure FormShow(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

//=============================================================================
//  変換元の値をセット
//=============================================================================
procedure TForm1.FormShow(Sender: TObject);
begin
  Edit1.Text := '1234567890123.56';
end;

//=============================================================================
//  桁落ちの例
//  Extended型の数値をTrunc関数で整数にしてみる
//  Extended型の扱える桁数よりも,Integer(32bit)が扱える有効桁数は少ない
//  したがって結果はとんでもない値となる
//
//  整数型の説明はヘルプの[整数型]で
//  実数型の説明はヘルプの[実数型]で
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  Ret   : Integer;
  Value : Extended;
begin
  //文字列を実数に変換
  Value := StrToFloat(Edit1.Text);

  //整数に変換
  Ret := Trunc(Value);
  Edit2.text := IntToStr(Ret);
end;

//=============================================================================
//  桁落ちしないように整数を64bitにして有効桁数を確保する
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  Ret   : Int64;
  Value : Extended;
begin
  //文字列を実数に変換
  Value := StrToFloat(Edit1.Text);

  //整数に変換
  Ret := Trunc(Value);
  Edit2.text := IntToStr(Ret);
end;

end.




06_数値の四捨五入等と数値を文字列にする関数類


浮動小数点演算の誤差は避けられないことを説明しました.実際のコードでは,できるだけ誤差を避けるため,例えば途中の計算結果を,一定の桁数で丸めて,精度を変えてしまうことも有効な手段ではないかと思います.
Delphi には,以下のような,そのための関数が用意されています.これらの関数の中には,精度等を指定して,文字列として取得する関数もあります.計算結果の表示等の目的に利用できます.


ユニット名.関数名 機能
Math.SimpleRoundTo 浮動小数点値を,指定桁で算術的な丸めを行う (四捨五入する)
SimpleRoundTo(V, -n) は,V の値の小数点 n+1 桁目を四捨五入して,小数点以下 n 桁までの浮動小数点型の実数値を返す
Math.RoundTo 浮動小数点値を,指定桁で丸める
銀行型丸め (Banker's Rounding) と呼ばれる「最近接偶数への丸め」
戻り値は浮動小数点型の実数値
System.Round 実数型の数値を整数値に変換する.戻り値は Int64 型
銀行型丸め (Banker's Rounding) と呼ばれる「最近接偶数への丸め」
SysUtils.FloatToStr 浮動小数点値を文字列に変換する.丸めは四捨五入方式
戻り値の有効桁数は 15.四捨五入した数値を文字列にしたもの
SysUtils.FloatToStrF 浮動小数点値を,指定の精度と桁数を指定して文字列に変換する
丸めは四捨五入方式
SysUtils.FloatToText Extended 型または Currency 型の数値を,精度と桁数を指定して文字列に変換する.結果は引数で受け取る
戻り値は文字列の長さで Integer 型
SysUtils.FloatToDecimal Extended 型または Currency 型の数値を,精度と桁数を指定して文字列に変換する.結果は TFloatRec レコード型の引数で受け取る
指定可能な有効桁数は 1 から 18
TFloatRec.Exponent が指数部の桁数
仮数部の値は,Char(TFloatRec.Digits[桁番号]) で取得する
Math.Ceil 浮動小数点値の,小数点以下の値を切り上げた整数値を取得する関数
戻り値は Integer 型
Math.Floor 浮動小数点値の,小数点以下の値を切り捨てた整数値を取得する関数
戻り値は Integer 型
System.Trunc 実数値を,0 方向に小数部を丸めた Int64 型の整数値を返す関数
つまり,小数点以下の値を切り捨てた実数値を返す関数
例えば,Trund(0.5), Trunc(-0.5) は 共に 0 となる.
System.Int System.Trunc と同じだが,戻り値は Extended 型
System.Frac 実数値の小数部分を返す関数.戻り値は Extended 型
SysUtils.IntToStr 符号付き整数値を文字列に変換する関数
SysUtils.UIntToStr 符号なし整数値を文字列に変換する関数
SysUtils.IntToStrDef 整数値を文字列に変換する関数
エラー時のデフォルト出力の指定付き
SysUtils.CurrToStr Currency 型 (通貨型) の値を文字列に変換する関数
SysUtils.CurrToStrF Currency 型 (通貨型) の値を書式付きで文字列に変換する関数
System.Str 整数値または実数値を,結果の文字列の長さと,小数点以下の桁数を指定して文字列に変換する
戻り値はない.結果は引数の文字列で受け取る
結果の文字列以外の引数は : (コロン) で区切るので注意
SysUtils.IntToHex 整数値を,16 進数の文字列に変換する関数
SysUtils.FloatToTextFmt Extended 型または Currency 型の値を,指定した書式で文字列に変換する関数.設定書式は FormatFloat 関数と同じ
結果は引数で受け取る.戻り値は Integer 型で,結果の文字列の長さ
SysUtils.Format 複数の文字列や数値を,指定した書式で文字列に変換する関数
単一の文字列,数値でも使用可能
SysUtils.FormatFloat 浮動小数点の値を,書式を指定して文字列に変換する関数
実際には整数値でも使用可能なので,数値を文字列にする汎用の関数として利用可能
SysUtils.FormatCurr Currency 型の数値を書式を指定して文字列に変換する関数
小数点以下に 5 桁以上指定しても, 5 桁目以降には 0 が出力される
整数値でも使用可能


SimpleRoundTo は,指定桁数で,いわゆる算術型丸め,四捨五入した値を返します.
一方,Round 関数,RoundTo 関数は,通称「銀行型丸め」(Banker's Rounding) という「最近接偶数への丸め」を行う関数です.数学的な四捨五入をする関数ではありません.
いずれも,数値の精度も,その指定した桁数にすることになります.

浮動小数点値を文字列にする関数類には,指定精度,桁数で,あるいは書式を整えて文字列にする関数もあります.例えば,FloatToStr 関数は,数値を 15 桁で丸めて,有効桁数 15 の汎用数値形式で文字列に変換します.汎用数値形式では,有効桁内の末尾の 0 は出力しません.
FloatToStrF 関数は,数値を指定精度の桁で四捨五入して,出力する桁数を指定できます.

FormatFloat 関数は,単一の数値を,書式を指定して文字列にします.Format 関数は,複数の数値や文字列も含めた値を,書式設定して文字列にする汎用の文字列作成用関数です.もちろん,単一の数値の場合でも利用可能です.
数値計算,すなわち,科学技術における計算結果は,有効桁数を明示するのが慣習となっています.したがって,FormatFloat や Format 関数を用いるのが一般的です.



32 ビットアプリケーションの場合,Extended 型は,10 進数で 18 桁以上 (約 20 桁) の値を格納できますが,ForamtFloat 関数や Str 関数を使用して文字列にしても,四捨五入した 18 桁までの値しか取得できません.桁の精度を指定する関数も,Double 型で 15 桁まで,Extended 型では 18 桁までとなっています.
64 ビットアプリケーションの場合,Extended 型は Double 型と同じ精度です.
参考リンクの [浮動小数点の丸めの問題] の記事にあるように,"見えているものが実態ではない" ということになります.

下の図とコードは,それを確認したものです.
元の値から 1.0 を引いてみると,有効桁数を超えた値には誤差はありますが,四捨五入すれば,確かに 61 という値がメモリ上には格納されていることが分かります.
FloatToStr 関数の有効桁は 15 で,汎用数値形式ですから,末尾の 0 は文字列として取得できません.科学技術計算の結果としての表示には向いていません.



図9
Extended 型の数値を文字列にする
  • 18 桁までしか取得できない
  • 1 は FloatToStr での結果

リスト7
procedure TForm1.Button1Click(Sender: TObject);
var
  E : Extended;
  StrText : String;
begin
  Memo1.Lines.Clear;
  Memo1.Font.Name := 'MS ゴシック';
  Memo1.Font.Size := 10;
  Memo1.Lines.Add('1 2345678901234567890');
  Memo1.Lines.Add('-----------------------------------------');

  E := 1.00000000000000000612;
  Memo1.Lines.Add('1.00000000000000000612');
  Memo1.Lines.Add(FormatFloat('0.00000000000000000000000E+000' , E));

  System.Str(E: 22: 20, StrText);
  Memo1.Lines.Add(StrText);
  Memo1.Lines.Add(FloatToStr(E));
  E := E - 1.0;
  Memo1.Lines.Add(FormatFloat('0.000E+000', E));
end;


Trunc 関数を使用して,小数点以下を四捨五入した実数値を取得するには工夫が必要です.
Trunc 関数は小数点以下の値を切り捨てます.Trunc 関数を使用する場合,例えば以下のような関数を作成しておくと便利です.


リスト8
function Roundoff(X: Extended): Integer;
begin
  if X >= 0 then Result := Trunc(X + 0.5)
            else Result := Trunc(X - 0.5);
end;


SimpleRoundTo 関数を使用すれば,以下のように処理できます.SimpleRoundTo 関数は内部で,上のコードと同様の処理を行っています.


function Roundoff(X: Extended): Integer;
begin
  Result := SimpleRoundTo(X, 0);
end;


任意の桁で四捨五入,切り捨てや切り上げする方法は,以下の記事を参考にしてください.



Delphi XE3 で実装された TStringHelper には数値を文字列に変換する Parse というメソッドがあります.下のコードはその使用例です.TStringHelper には Format というメソッドがありますが,ここでは FormatFloat 関係を使用して書式を設定しています.



図10
TStringHelper.Parse メソッドを使用して数値を文字列に変換
図11
書式が必要な場合は FormatFloat 等の関数を使用する

リスト9
//=============================================================================
//  数値を文字列に変換
//  書式が必要なければTStringHelperが使用できる(Delphi XE3 以降専用
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LValue : Double;
  LText  : string;
begin
  LValue := 123.456;
  LText := string.Parse(LValue);

  Memo1.Lines.Clear;
  Memo1.Lines.Add(LText);
end;

//=============================================================================
//  数値を文字列に変換
//  書式が必要な場合
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  LValue : Double;
  LText  : string;
begin
  LValue := 123.456;
  LText := FormatFloat('#0.00000', LValue);

  Memo1.Lines.Clear;
  Memo1.Lines.Add(LText);
end;


[備考]
実数型の除算 (割り算) の演算子は / です.
整数型の除算の演算子は div です.割り切れない場合は切り捨てられ,5 div 2 は 2 となります.剰余は mod 演算子で求めることができます.
乗算 (掛け算) の演算子は,整数型,実数型ともに * です.





07_文字列を数値にする関数類


数値を文字列に変換するのではなく,文字列を数値に変換するには以下のような関数があります.



Delphi XE3 で実装れた TStringHelper には,以下のオンラインヘルプの記事のように,文字列を各種の数値型の値に変換するメソッドがあります.
下に示すコードは不動小数点形式である Double 型に変換する例です.



図12
TStringHelper のメソッドを使用して,文字列を数値に変換する例

リスト10
//=============================================================================
//  TStringHelperのメソッドを使用して文字列を数値に変換
//  TStringHelperには文字列各種の型の数値に変換するメソッド類がある
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LValue : Double;
  LText  : string;
begin
  LText  := '123.456';
  LValue := LText.ToDouble(LText);

  Memo1.Lines.Clear;
  Memo1.Lines.Add(FormatFloat('#0.000', LValue));
end;




08_関数の引数が,数値を使用した式の時に発生する現象


関数の引数には変数を使用するのが基本です.引数に具体的な数値が存在する計算式を使用すると期待した結果にならないことがあります.
以下は Trunc 関数を使用したテストコードです.Turnc 関数の引数を実数型 1 だけつにすると期待する結果を得ることができます.Trunc 関数の引数として,100.0 のように小数点以下の値も含めて,明示的に実数型の値として与えるか,具体的な数値ではなく,変数を使用すればこの現象が発生しないことが確認できます.

この現象は主に,引数が算術式を含む「式」の場合に,その演算がどのように処理されるかの問題です.したがって,このような現象は Trunc 関数以外でも発生することがあります.



図13
Trunc 関数の引数による計算結果の違い
  • 数学的に正しい値は 998

リスト11
//=============================================================================
//  Trunc関数(小数点以下を切り捨て)の挙動
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LInteger : Integer;
  LDouble  : Double;
begin
  Memo1.Lines.Clear;

  LInteger := Trunc(9.98 * 100);
  Memo1.Lines.Add(IntToStr(LInteger));

  // 数値を明示的に実数型とする場合
  LInteger := Trunc(9.98 * 100.0);
  Memo1.Lines.Add(IntToStr(LInteger));

  // 計算結果の値を実数型の変数に代入してからTrunc関数の引数とする場合
  LDouble  := 9.98 * 100;
  LInteger := Trunc(LDouble);
  Memo1.Lines.Add(IntToStr(LInteger));

  // 各々の数値をそれぞれの型の変数に代入してからTrunc関数の引数とする場合
  LDouble  := 9.98;
  LInteger := 100;
  LInteger := Trunc(LDouble * LInteger);
  Memo1.Lines.Add(IntToStr(LInteger));

  // 末尾の桁の値が 6 の場合
  LDouble  := 9.9799999999999996;
  LInteger := Trunc(LDouble * 100);
  Memo1.Lines.Add(IntToStr(LInteger));

  // 末尾の桁の値が 5 の場合
  LDouble  := 9.9799999999999995;
  LInteger := Trunc(LDouble * 100);
  Memo1.Lines.Add(IntToStr(LInteger));
end;


次も Trunc 関数の例ですが,上のサンプルと数値が異なる例です.上のサンプルと同じ現象が発生することが確認できます.



図14
Trunc 関数の引数による計算結果の違い
  • 数学的に正しい値は 5

リスト12
//=============================================================================
//  Trunc関数(小数点以下を切り捨て)の挙動
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LInteger  : Integer;
  LIntegerB : Integer;
  LDouble   : Double;
begin
  Memo1.Lines.Clear;

  // この結果は 4.これ以降の計算結果は全て 5
  LInteger := Trunc(5 * 0.01 * 100);
  Memo1.Lines.Add(IntToStr(LInteger));

  // 数値を明示的に実数型とする場合
  LInteger := Trunc(5.0 * 0.01  * 100.0);
  Memo1.Lines.Add(IntToStr(LInteger));

  // 数値の大きい値の順に計算する場合
  LInteger := Trunc(100 * 5 * 0.01);
  Memo1.Lines.Add(IntToStr(LInteger));

  // 計算結果の値を実数型の変数に代入してからTrunc関数の引数とする場合
  LDouble  := 5 * 0.01 * 100;
  LInteger := Trunc(LDouble);
  Memo1.Lines.Add(IntToStr(LInteger));

  // 各々の数値をそれぞれの型の変数に代入してからTrunc関数の引数とする場合
  LInteger  := 5;
  LIntegerB := 100;
  LDouble   := 0.01;
  LInteger := Trunc(LInteger * LDouble * LIntegerB);
  Memo1.Lines.Add(IntToStr(LInteger));
end;


上のサンプルのような現象は,関数の引数である数値の型が具体的に指定されていないことと,コンピュータが有限の桁数の数値しか扱えないために発生します.
このことから,以下のように SimpleRoundTo 関数を使用して,引数の値を小数点以下の適当な桁数で四捨五入してしまうことでも回避できます



図15
Trunc 関数の引数の値を,小数点以下の適当な桁数で四捨五入した結果

リスト13
//=============================================================================
//  Trunc関数(小数点以下を切り捨て)の挙動
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LInteger : Integer;
begin
  Memo1.Lines.Clear;

  // 引数の値を小数点以下 19 桁目で四捨五入
  // この計算式の場合は小数点以下 19 桁目は実際にはない
  LInteger := Trunc(SimpleRoundTo((5 * 0.01 * 100), -19));
  Memo1.Lines.Add(IntToStr(LInteger));

  // 引数の値を小数点以下 10 桁目で四捨五入
  LInteger := Trunc(SimpleRoundTo((5 * 0.01 * 100), -10));
  Memo1.Lines.Add(IntToStr(LInteger));
end;


数値計算では有効桁数が長いほど,より数学的な計算結果に近い値が得られます.このことは,これまでのテストの結果でも確認しています.
CPU のストリーミング SIMD 拡張命令 (SSE) を使用すると制度の高い演算が期待できます.64 ビットのモードを持つ CPU は SSE を実装しています.Delphi XE7 以降では浮動小数点演算に SSE が使用されます.そこで 64 ビットの EXE を作成してテストした結果が下図です.

もちろんですが,関数の引数に具体的な数値を使用することを勧めているわけではありせん.関数の引数には,型を指定した変数を使用するのが基本です.



図16
Delphi XE7 以降の 64 ビット EXE による計算結果
  • 精度の高い演算結果となる

リスト14
//=============================================================================
//  Trunc関数(小数点以下を切り捨て)の挙動
//  64 ビットの EXE の場合
//
//  Delphi XE 5(UP2) Pro    : 997 と 4 になる
//  Delphi XE 7 Pro         : 998 と 5 になる
//  Delphi 10.2.3 Community : 998 と 5 になる
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LInteger : Integer;
begin
  Memo1.Lines.Clear;

  LInteger := Trunc(9.98 * 100);
  Memo1.Lines.Add(IntToStr(LInteger));

  LInteger := Trunc(5 * 0.01 * 100);
  Memo1.Lines.Add(IntToStr(LInteger));
end;


SSE 等の機能は,それらの機能を実装している CPU であれば,32 ビットの EXE でも利用できます.以下はその例です.作成した MyTrunc という名前の関数の引数に,上のテストで使用した 2 つの計算式を書いて実行しています.どちらも期待した値となっています.

このサンプルコードは Delphi XE2 以降用です.インラインアセンブラで SSE, SSE2 関係の CPU 命令が使用可能になったのは Delphi XE2 からです.



図17
SSE を使用して小数点以下を切り捨てた結果

リスト15
//-----------------------------------------------------------------------------
//  ストリーミング SIMD 拡張命令 (SSE) を使用した Trunc 関数
//  SSE, SSE2,... 関係等の命令セットは Delphi XE2 以降で使用可能
//
//  CVTTSD2SI : Double 値を 64ビット整数値へ変換
//  EAX       : 演算結果の格納用レジスタ (80386以降用)
//-----------------------------------------------------------------------------
function MyTrunc(const X: Double): Integer;
asm
  CVTTSD2SI  EAX, [X]
end;

//=============================================================================
//  Trunc関数(小数点以下を切り捨て)の挙動
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LInteger : Integer;
begin
  Memo1.Lines.Clear;

  LInteger := MyTrunc(9.98 * 100);
  Memo1.Lines.Add(IntToStr(LInteger));

  LInteger := MyTrunc(5 * 0.01 * 100);
  Memo1.Lines.Add(IntToStr(LInteger));
end;


現在のシステムで使用可能な SSE 関係の機能は,TestSSE 関数で調べることができます.下図は筆者の環境における結果です.



図18
現在のシステムで使用可能な SSE 関係の機能

リスト16
implementation

uses
  Math;

{$R *.dfm}

//=============================================================================
//  使用中のコンピュータの SSE(ストリーミング SIMD 拡張命令)機能の確認
//  usesにMathが必要
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LRes : Cardinal;
begin
  LRes := TestSSE;

  Memo1.Lines.Clear;

  if (LRes and seSSE)       = seSSE       then Memo1.Lines.Add('SSE');
  if (LRes and seSSE2)      = seSSE2      then Memo1.Lines.Add('SSE2');
  if (LRes and seSSE3)      = seSSE3      then Memo1.Lines.Add('SSE3');
  if (LRes and seSSSE3)     = seSSSE3     then Memo1.Lines.Add('Supplemental SSE3');
  if (LRes and seSSE41)     = seSSE41     then Memo1.Lines.Add('SSE4.1');
  if (LRes and seSSE42)     = seSSE42     then Memo1.Lines.Add('SSE4.2');
  if (LRes and sePOPCNT)    = sePOPCNT    then Memo1.Lines.Add('POPCNT');
  if (LRes and seAESNI)     = seAESNI     then Memo1.Lines.Add('AES');
  if (LRes and sePCLMULQDQ) = sePCLMULQDQ then Memo1.Lines.Add('PCLMULQDQ');
end;




09_FPU のコントロールワード ( 制御ワード ) と SafeLoadLibrary


浮動小数点の演算の結果は FPU のコントロールワード (制御ワード) によっても異なることがあります.以下のその 1 つの例です.
Delphi では下に示すコードのように,Set8087CW で FPU の制御ワードの値が設定できます.現在の制御ワードの値は Get8087CW で取得できます.



図19
FPU の制御ワードの設定によって値が異なる例
  • 変数に代入している値は全て 0.1


Delphi XE7 以降でコンパイルした 64 ビットの EXE は SSE 関係のレジスタを使用します.FPU のレジスタは使用しません.したがって,Set8087CW で FPU の制御ワードを変更しても演算の精度が変わることはありません.



図20
Delphi XE7 で作成した 64 ビットの EXE の場合
  • 浮動小数点の演算に FPU のレジスタを使用しないので制御ワードの設定は無視される
  • SSE では 128 ビットのレジスタが使われる
  • Double 型の 0.1 が,表示桁数の範囲内では全て 0 となっている

リスト17
procedure TForm1.Button1Click(Sender: TObject);
var
  R1 : Double;
  R2 : Single;
begin
  Memo1.Lines.Clear;

  Set8087CW(Default8087CW);
  R1 := 0.1;
  R2 := 0.1;

  Memo1.Lines.Add(FormatFloat('##0.00000000000000000000', R1));
  Memo1.Lines.Add(FormatFloat('##0.00000000000000000000', R2));

  Set8087CW($1072);
  R1 := 0.1;
  Memo1.Lines.Add(FormatFloat('##0.00000000000000000000', R1));

  Set8087CW($1272);
  R1 := 0.1;
  Memo1.Lines.Add(FormatFloat('##0.00000000000000000000', R1));
end;


FPU の精度制御モード,現在の浮動小数点丸めモード,例外処理の値は,浮動小数点数制御ルーチンの関数類を使用して個別に取得または設定できます.以下は取得の例です.
このコードは Delphi XE2 以降用です.



[備考]
FPU の制御ワードは,FPU の制御レジスタの下位ワードの値です.Delphi 3 から Delphi 5 までのデフォルト値は $1332 です.Delphi 6 からは $1373 がデフォルトの値となっています.この両者の値に実質的な違いはありません.Delphi 6 以降で $1332 に設定しても $1373 になります.
この 16 進数の 2 桁目にある 5, 6 ビット目は例外フラグですが,7, 8 ビット目は,FPU 自身が使用するフラグとステータス情報です.演算精度,丸めモード,例外フラグとして外部から設定するフラグ用のビットは 1 ~ 6 ビット目と 9 ~ 12 ビット目です.



図21
FPU の制御モードの取得

リスト18
implementation

uses
  Math;

{$R *.dfm}

//=============================================================================
//  FPU の現在の制御モードを取得
//  このコードは Delphi XE ではコンパイルできない
//
//  usesにMathが必要
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LFPUPrecis  : TFPUPrecisionMode;
  LRoundMode  : TRoundingMode;
  LExceptions : TFPUExceptionMask;
begin
  Memo1.Lines.Clear;

  Memo1.Lines.Add('$' + IntToHex(Get8087CW, 4));

  LFPUPrecis := GetPrecisionMode;
  if LFPUPrecis = pmSingle   then Memo1.Lines.Add('単精度 - 24 ビット');
  if LFPUPrecis = pmDouble   then Memo1.Lines.Add('倍精度 - 53 ビット');
  if LFPUPrecis = pmExtended then Memo1.Lines.Add('拡張倍精度 - 64 ビット');

  LRoundMode := GetRoundMode;
  if LRoundMode = rmNearest  then Memo1.Lines.Add('もっとも近い整数値への丸め');
  if LRoundMode = rmDown     then Memo1.Lines.Add('切り下げ');
  if LRoundMode = rmUp       then Memo1.Lines.Add('切り上げ');
  if LRoundMode = rmTruncate then Memo1.Lines.Add('切り捨て');

  LExceptions := GetFPUExceptionMask;
  if exInvalidOp in LExceptions    then Memo1.Lines.Add('無効な演算を無視');
  if exDenormalized in LExceptions then Memo1.Lines.Add('非 0 の小さい値を許可');
  if exZeroDivide in LExceptions   then Memo1.Lines.Add('0 による除算許可');
  if exOverflow in LExceptions     then Memo1.Lines.Add('オーバーフロー許可');
  if exUnderflow in LExceptions    then Memo1.Lines.Add('アンダーフロー許可');
  if exPrecision in LExceptions    then Memo1.Lines.Add('有効桁数を超える演算許可');
end;


上のコードに次のコードを追加すれば,Delphi 6 以降でコンパイルできます.
Delphi 5 以前には Get8087CW が実装されていないのでコンパイルできません.


リスト19
implementation

{$R *.dfm}

type
  TFPUPrecisionMode = (pmSingle, pmReserved, pmDouble, pmExtended);
  TRoundingMode = (rmNearest, rmDown, rmUp, rmTruncate);
  TFPUException = (exInvalidOp, exDenormalized, exZeroDivide,
                   exOverflow, exUnderflow, exPrecision);
  TFPUExceptionMask = set of TFPUException;

//----------------------------------------------------------------------------- 
// 精度制御の値は9,10ビット目にあるのでシフトして 3(2進数の11)でANDして取得 
//-----------------------------------------------------------------------------  
function GetPrecisionMode: TFPUPrecisionMode;
begin
  Result := TFPUPrecisionMode((Get8087CW shr 8) and 3);
end;

//----------------------------------------------------------------------------- 
// 丸め制御の値は11,12ビット目にあるのでシフトして 3(2進数の11)でANDして取得 
//-----------------------------------------------------------------------------  
function GetRoundMode: TRoundingMode;
begin
  Result := TRoundingMode((Get8087CW shr 10) and 3);
end;

//----------------------------------------------------------------------------- 
// 例外フラグの値は1から6ビット目にあるので3F(2進数の111111)でANDして取得 
// ビットが立っている(1である)と該当例外がマスクされている 
//-----------------------------------------------------------------------------  
function GetFPUExceptionMask: TFPUExceptionMask;
begin
  Byte(Result) := Get8087CW and $3F;
end;


サードパーティが提供しているライブラリ等では,この FPU の制御ワードを変更する場合があります.例えば,オンラインヘルプにも記述があるように, OpenGL では変更しています.
SafeLoadLibrary 関数を使用して DLL をロードすると,FPU の制御ワードを維持した状態で DLL を読み込むことができます.

以下はエクセルクリエータ 3.0 (Excel Creator 3.0) の DLL をロードする例です.
LoadLibrary 関数で DLL をロードすると,ロード直前に設定した FPU の制御ワードの設定は無視されて,エクセルクリエータの DLL が設定した値に変更されています.一方,SafeLoadLibrary 関数で DLL をロードした場合は,直前に設定した制御ワードの値が有効になっていることが確認できます.



図22
LoadLibrary 関数で DLL をロード
  • FPU の制御ワードの値が変更されている
  • $1372 は Delphi のデフォルトの値
図23
SafeLoadLibrary 関数で DLL をロード
  • FPU の制御ワードの値が,DLL をロードする前に設定した値のままとなっている
  • この図は Button1 をクリックした後に Button2 をクリックした結果

リスト20
LoadLibrary と SafeLoadLibrary の動作の違いを確認
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Memo1: TMemo;
    Button2: TButton;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
    FDllName : string;
  end;

var
  Form1: TForm1;

implementation

uses
  Math;

{$R *.dfm}

//=============================================================================
//  フォーム生成時の処理
//  ロードする DLL のファイル名を設定
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  FDllName := 'XlsCrt2.DLL';
end;

//=============================================================================
//  LoadLibrary 関数で DLL をロードする場合
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LDllHandle   : THandle;
  LOldCtrlWord : Word;
  LNewCtrlWord : Word;
  LRValue      : Double;
begin
  LOldCtrlWord := Get8087CW;

  // これが現在の設定値になる
  Set8087CW($1072);

  LDllHandle := LoadLibrary(PChar(FDllName));
  try
    // 現在の設定値を取得
    // LoadLibrary で現在の設定値が変わる
    LNewCtrlWord := Get8087CW;

    Memo1.Lines.Clear;
    Memo1.Lines.Add('$' + IntToHex(LOldCtrlWord, 4));
    Memo1.Lines.Add('$' + IntToHex(LNewCtrlWord, 4));

    LRValue := 0.1;
    Memo1.Lines.Add('');
    Memo1.Lines.Add(FormatFloat('##0.00000000000000000000', LRValue));
  finally
    FreeLibrary(LDLLHandle);
  end;
end;

//=============================================================================
//  SafeLoadLibrary 関数で DLL をロードする場合
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  LDllHandle   : THandle;
  LOldCtrlWord : Word;
  LNewCtrlWord : Word;
  LRValue      : Double;
begin
  LOldCtrlWord := Get8087CW;

  // これが現在の設定値になる
  Set8087CW($1072);

  LDllHandle := SafeLoadLibrary(PChar(FDllName));
  try
    // 現在の設定値を取得
    // SafeLoadLibrary では設定値は変しない
    LNewCtrlWord := Get8087CW;

    Memo1.Lines.Clear;
    Memo1.Lines.Add('$' + IntToHex(LOldCtrlWord, 4));
    Memo1.Lines.Add('$' + IntToHex(LNewCtrlWord, 4));

    LRValue := 0.1;
    Memo1.Lines.Add('');
    Memo1.Lines.Add(FormatFloat('##0.00000000000000000000', LRValue));
  finally
    FreeLibrary(LDLLHandle);
  end;
end;

end.





10_Delphi XE2 以降の BCD 演算


[03_演算誤差のテスト] のサンプルでは,BCD 演算は専用の関数を使用していました.Delphi XE2 では,BCD 演算にも通常の算術演算子が使用できるようになりました.
Delphi XE までのコードもそのまま使用できますが,関数によっては「使用することは推奨されていません」の警告が現れる場合があります.
通常の演算子なので,説明するまでもありませんが,簡単な例を掲載しておきます.下のコードは,最初の BCD のサンプルと同じ計算をするサンプルです.


図24
実行結果

リスト21
Delphi XE2 以降の BCD 演算
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls, ExtCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Panel1: TPanel;
    procedure Button1Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

uses Data.FMTBcd;

{$R *.DFM}

//=============================================================================
//  Delphi XE2以降のBCD演算
//  通常の算術演算子が使用可能
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  A : TBcd;
  B : TBcd;
  i : Integer;
begin
  A := 0;
  B := 0.1;
  for i := 0 to 9 do begin
    A := A + B;
  end;
  A := A * 50;
  Panel1.Caption := FormatBcd('000.0000000000', A);
end;

end.




11_多倍長演算 ( 長い桁数の演算)


コンピュータで扱える数値の桁は有限で,多くの場合 64 ビット ( 10 進で 18 桁程度)までです.
長い桁数の計算を高精度で計算するには,例えば,10 進の任意の桁を進法とする方法があります.本サンプルは 9999 までの値を 1 桁として整数配列の要素に格納します.つまり,万進法ということになります.
数値計算では最低限四則演算ができれば,三角関数等の超越関数類は,以下のマクローリン展開式 (テイラー展開式) 等を使用して求めることができます.
CPU の内部の FPU (浮動小数点演算部) でもこのような方法で関数を計算していますが,実際にはハードウェアで実現しているため非常に高速で計算されます.
整数の多倍長の四則演算用の関数と,マクローリン展開式を用いた Arctan (逆正接) の値を計算する関数を作成してみました.次のサンプルは,それらを使用したテストです.ListOut で,TStringList に計算結果を格納して,それを TMemo に表示します.
plLongDigit.pas をプロジェクトに追加しています.


図25
多倍長演算の例
  • 10 進で 20 桁の演算を実行
  • 上の 2 つの値に対して
  • 加算すると,0500 0200... 以下省略
  • 次が減算の結果
  • 次が最初の値に 4 を掛けた結果
  • 最後は最初の値を 4 で割った結果

リスト22
多倍長四則演算のテスト
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type
  TForm1 = class(TForm)
    Memo1: TMemo;
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

uses plLongDigit, MMSystem;

{$R *.dfm}

{ TForm1 }

//=============================================================================
//  多倍長計算ユニットplLongDigitのテスト
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  A : array of Integer;
  B : array of Integer;
  C : array of Integer;
  I : Integer;
  M : Integer;
  N : Integer;
  SL : TStringList;
begin
  M := 10000; //万進法
  N := 5;     //万進法で5桁

  SetLength(A, N + 1);
  SetLength(B, N + 1);
  SetLength(C, N + 1);

  for I := 0 to N do begin
    A[I] := 0;
    B[I] := 0;
    C[I] := 0;
  end;

  //4000000000001006000をセット
  A[5] :=  400;   //万進法の5桁目
  A[4] :=    0;   //万進法の4桁目
  A[3] :=    0;   //万進法の3桁目
  A[2] :=  100;   //万進法の2桁目
  A[1] := 6000;   //万進法の1桁目

   //1000200030004005000をセット
  B[5] :=  100;   //万進法の5桁目
  B[4] :=  200;   //万進法の4桁目
  B[3] :=  300;   //万進法の3桁目
  B[2] :=  400;   //万進法の2桁目
  B[1] := 5000;   //万進法の1桁目

  Memo1.Lines.Clear;
  SL := TStringList.Create;
  //元の値をリスト
  ListOut (A, M, N, SL);
  ListOut (B, M, N, SL);
  SL.Add(' ');
  try
    //加算
    Tasu (A, B, C, M, N);
    ListOut(C, M, N, SL);
    //減算
    Hiku (A, B, C, M, N);
    ListOut (C, M, N, SL);
    //乗算
    Kakeru (A, 4, C, M, N);
    ListOut (C, M, N, SL);
    //除算
    Waru (A, 4, C, M, N);
    ListOut (C, M, N, SL);
    Memo1.Text := SL.Text;
  finally
    FreeAndNil(SL);
  end;
end;

end.


作成した整数の多倍長の四則演算用の関数と,マクローリン展開式を用いた Arctan (逆正接) の値を計算するコードです. plLongDigit.pas という名前のユニットにしています.
TStringList に計算結果を格納するルーチンが ListOut です.


リスト23
多倍長の四則演算と,マクローリン展開式を用いた Arctan (逆正接) の計算用のユニット
unit plLongDigit;

interface

uses
  SysUtils, Classes, Forms;

  procedure Tasu (A,B : array Of Integer; var C: array of Integer; M,N :Integer);
  procedure Hiku (A,B : array Of Integer; var C: array of Integer; M,N :Integer);
  procedure Kakeru (A: array of Integer; B: Integer; var C: array of Integer;
    M, N: Integer);
  procedure Waru (A: array of Integer; B: Integer; var C: array of Integer;
    M, N: Integer);
  procedure AT(Bunbo,Kousu: Integer; var C: array of Integer; M,N: Integer);
  procedure ListOut (A: array of Integer; M,N: Integer; SL : TStringList);

implementation

uses Math;

//-----------------------------------------------------------------------------
//  M進法N桁の加算(Mは10の整数乗)
//  M進法N桁の数A,Bを配列A,Bに記録.その和Cを計算する
//-----------------------------------------------------------------------------
procedure Tasu(A,B: array of Integer; var C:array of Integer; M,N:Integer);
var
  CK : Integer;
  K  : Integer;
  IC : Integer;
begin
  IC := 0;
  for K := 1 to N do begin
    CK := A[K] + B[K] + IC ;
    if CK >= M then begin
      CK := CK - M;
      IC := 1;
    end else begin
      IC := 0;
    end;
    C[K] := CK;
    Application.ProcessMessages;
  end;
end;

//-----------------------------------------------------------------------------
//  M進法N桁の引き算(Mは10の整数乗)
//  M進法N桁の数A,Bを配列A,Bに記録.その差A-Bをを計算しCに格納する
//-----------------------------------------------------------------------------
procedure Hiku(A,B:array of Integer; var C:array of Integer; M,N:Integer);
var
  IB : Integer;
  AK : Integer;
  BK : Integer;
  K  : Integer;
begin
  IB := 0;
  for K := 1 to N do begin
    AK := A[K];
    BK := B[K] + IB;
    if AK < BK then begin
      AK := AK + M;
      IB := 1;
    end else begin
      IB := 0;
    end;
    C[K] := AK - BK;
    Application.ProcessMessages;
  end;
end;

//-----------------------------------------------------------------------------
//  M進法N桁の乗算(Mは10の整数乗)
//  M進法N桁の数AとM進法1桁の数Bの積を計算し,その結果を配列Cに格納する
//  数Bを大きな桁にすると大変な桁になってしまう.1桁ならM進法で1桁増加のみ
//-----------------------------------------------------------------------------
procedure Kakeru(A: array of Integer; B: Integer; var C: array of Integer;
  M, N: Integer);
var
  CK : Integer;
  IC : Integer;
  K  : Integer;
begin
  IC := 0;
  for K := 1 to N do begin
    CK := A[K] * B + IC;
    if K <> N then begin
      if CK >= M then begin
        IC := CK div M;
        CK := CK -IC * M;
      end else begin
        IC := 0;
      end;
    end;
    C[K] := CK;
    Application.ProcessMessages;
  end;
end;

//-----------------------------------------------------------------------------
//  M進法N桁の乗算(Mは10の整数乗)
//  M進法N桁の数AをM進法1桁の数Bで割り,その結果を配列Cに格納する
//-----------------------------------------------------------------------------
procedure Waru(A: array of Integer; B: Integer; var C: array of Integer;
  M, N: Integer);
var
  P  : Integer;
  Q  : Integer;
  R  : Integer;
  K  : Integer;
begin
  R := 0;
  for K := N downto 1 do begin
    P := A[K] + R * M;
    Q := P div B;
    R := P - Q * B;
    C[K] := Q;
    Application.ProcessMessages;
  end;
end;

//-----------------------------------------------------------------------------
//  M進法N桁の関数例(Mは10の整数乗)
//  逆正接の値を多倍長の級数計算でKousu項まで求め,結果を配列Cに格納する
//  計算はテイラー展開式.CPUのFPU(浮動小数点演算ユニット)では超越関数と呼ばれ
//  ている関数をこのような計算で求めている.ただしarctanの算出アルゴリズにこれ
//  が使用されているとか限らない
//  計算するのはarctan(1/Bunbo)の値
//-----------------------------------------------------------------------------
procedure AT(Bunbo, Kousu: Integer; var C: array of Integer; M,N: Integer);
var
  H    : Integer;
  BB   : Integer;
  I    : Integer;
  KOU  : Integer;
  A    : array of Integer;
  B    : array of Integer;
begin
  H  := 3;
  BB := Bunbo * Bunbo;

  SetLength(A, N + 1);
  SetLength(B, N + 1);
  for I := 0 to N do begin
    C[I] := 0;
  end;

  A[N] := M;
  Waru (A, Bunbo, A, M, N);
  Tasu (C, A, C, M, N);
  for KOU := 1 to (Kousu div 2) do begin
    Waru (A, BB, A, M, N);
    Waru (A, H, B, M, N);
    Hiku (C, B, C, M, N);
    H := H + 2;
    Waru (A, BB, A, M, N);
    Waru (A, H, B, M, N);
    Tasu (C, B, C, M, N);
    H := H + 2;
  end;
end;

//-----------------------------------------------------------------------------
//  M進法N桁の値をTStringListに格納(Mは10の整数乗)
//  万進法であれば10進で4桁ごとに区切り,1行はM進法で5桁とする
//  Log10の使用にはusesにMathが必要
//-----------------------------------------------------------------------------
procedure ListOut(A: array of Integer; M,N: Integer; SL : TStringList);
var
  I    : Integer;
  J    : Integer;
  L    : Integer;
  Str  : String;
  Fmt  : String;
  Keta : Integer;
begin
  Keta := Trunc(Log10(M));
  Fmt  := StringOfChar('0', Keta);
  I    := N;
  for L := 1 to (N div 5) + 1 do begin
    Str := '';
    for J := 1 to 5 do begin
      Str := Str + '  '+ FormatFloat(Fmt, A[I]);
      I := I - 1;
      if I = 0 then begin
        SL.Add(Str);
        exit;
      end;
    end;
    SL.Add(Str);
  end;
end;

end.




12_円周率 π の値を 10,000 桁まで計算


多倍長演算のルーチンを作成しましたので,実際の使用例として円周率 π の値を計算してみることにします.万進法で 10,000 桁まで計算します.π の計算には以下のマチンの公式を用いています.
ほとんどのコンピュータでは何秒もかからないでしょう.筆者の環境では 2278 ms でした.変数 Keta の値を変更するだけで計算桁数を変更することができます.
plLongDigit.pas をプロジェクトに追加しています.


図26
実行結果
  • 万進法の最後の桁は丸め誤差のため不正確

リスト24
マチンの公式を使用して,円周率πの値を計算する
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type
  TForm1 = class(TForm)
    Memo1: TMemo;
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

uses plLongDigit, MMSystem;

{$R *.dfm}

{ TForm1 }

//=============================================================================
//  マチンの公式を用いてπの値を10,000桁まで計算する
//  多倍長計算として,万進法(1桁が10進の4桁)を使用
//  M進法の最後の桁はまるめ誤差のため厳密な値ではない
//
//  TimeGetTimeにはusseにMMSystemが必要
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  M       : Integer;
  N       : Integer;
  Keta    : Integer;
  Kou5    : Integer;
  Kou239  : Integer;
  X       : array of Integer;
  XX      : array of Integer;
  Y       : array of Integer;
  Z       : array of Integer;
  ZZ      : array of Integer;
  TStart  : DWORD;
  TEnd    : DWORD;
  SL      : TStringList;
begin
  Memo1.Lines.Clear;
  Memo1.Update;

  TStart := TimeGetTime;

  //計算するπの桁数
  Keta := 10000;

  M := 10000;       //万進法
  N := Keta div 4;  //万進法でN桁

  //arctanの計算に必要なテイラー展開式の概略項数
  Kou5   := Trunc((Keta * 1.403) / 2);
  Kou239 := Trunc((Keta * 0.421) / 2);
  Kou5   := Kou5 + Kou5 div 10;
  Kou239 := Kou239 + Kou239 div 10;

  SetLength (X,  N+1);
  SetLength (XX, N+1);
  SetLength (Y,  N+1);
  SetLength (Z,  N+1);
  SetLength (ZZ, N+1);

  //arctan(1/5)の値をテイラー展開のKou5まで計算して配列Xに格納.それに4を掛ける
  AT (5, Kou5, X, M, N);
  Kakeru (X, 4, XX, M, N);
  //arctan(1/239)の値をテイラー展開のKou239まで計算して配列Yに格納
  AT (239, Kou239, Y, M, N);

  Hiku (XX, Y, Z, M, N);
  Kakeru (Z, 4, ZZ, M, N);

  TEnd := TimeGetTime;

  //結果を表示
  SL := TStringList.Create;
  try
    ListOut (ZZ, M, N, SL);
    SL.Add (' ');
    SL.Add (IntToStr(TEnd-TStart)+' ms ');
    Memo1.Text := SL.Text;
  finally
    FreeAndNil(SL);
  end;
end;

end.