VBAのFixやIntの計算誤差は浮動小数点レジスタの精度がかかわっている

Table of Content

問題

Double型のデータをIntやFixで切り捨てた場合に誤差がでます。
浮動小数点なので当然誤差は出ますが、CDblのキャストのタイミングでも誤差が発生します。

ExcelVBAのイミデイトウィンドウで下記を実行してください。

?Fix(0.6# * 10 )
5
?Fix(CDbl(0.6# * 10))
6

環境によってはFix関数に渡す前にCDblでキャストした場合と、しない場合で計算結果が変わります。

再現環境
 Office2010 32bit
 Windows7 64biy
 CPU Intel(R) Core(TM) i5 CPU M450 2.4GHz

原因

この問題は、下記のページでも議論されています。(このページではVB6が話題ですが、基本的に同じです)

http://hanatyan.sakura.ne.jp/logbbs1/wforum.cgi?mode=allread&no=3276&page=0#3276

この原因には、計算途中の浮動小数点の精度とDouble型の精度に違いがあるために発生しています。

浮動小数点の演算の途中経過が64ビットの仮数を持つ80ビットのレジスタで計算しているが、Dobule型は53ビットの仮数を持つ64bitの浮動小数点なので、結果として誤差が表示してしまいます。

「VC++とかで同じようなコードを書いても再現しないじゃないか!やっぱりVBAは壊れているじゃないか!(憤怒)」 と怒る兄貴、姉貴もおられると思いますが、浮動小数点レジスタの仮数を64ビットの仮数で計算するか、53ビットで計算するかはプログラム中で変更することができます。

このことを確認するには、_controlfp_sの関数を実行して調べることができます。

_controlfp_s
http://msdn.microsoft.com/ja-jp/library/c9676k6h%28v=vs.80%29.aspx

この関数から取得できる値に_MCW_PCをマスクした結果で浮動小数点の精度が確認できます。

_MCW_PC (精度制御)

Mask 定数 value
0x00030000 _PC_24 (24 ビット)
_PC_53 (53 ビット)
_PC_64(64 ビット)
0x00020000
0x00010000
0x00000000

つまり、VC++のデフォルトでは、_MCW_PCの_PC_53がたっている状態であり、53ビットの仮数を持つ64bitの浮動小数点になっているため計算誤差がでないのです。

VBAでの浮動小数点レジスタの確認方法

残念ながら、VBAで直接、_controlfp_s関数を呼ぶことはできません。
しかし、VBAではDLLを実行できるので、DLL経由で_controlfp_sを実行することで、確認することができます。

まず、VC++でDLL側のコードを記述します。

DLL側のコード

# include <windows.h>
# include <float.h>
int __stdcall GetControlFP() 
{ 
  int err,sts;
  err=_controlfp_s(&sts,0,0 );
  return sts;
} 
int WINAPI DllMain(HINSTANCE hInst, DWORD fdwReason, PVOID pvReserved) 
{ 
    return TRUE; 
} 

VisualStdio2008SP1 で32bitでコンパイルした結果
http://needtec.sakura.ne.jp/vba/checkfloat/VBADll.zip

※もし、64bitのVBAを使用している場合、64bitでビルドして64bitのDLLを作成する必要があります。

作成したDLLをVBAから呼び出します。

Option Explicit
' パスを修正すること
Declare Function GetControlFP Lib "C:\Users\xxxxx\Documents\Visual Studio 2008\Projects\VBADll\Release\VBADll.dll" () As Long

Public Sub TestCon()
    '
    '  _MCW_PC (精度制御) 0x00030000
    '
    ' _PC_24 (24 ビット) 0x00020000
    ' _PC_53 (53 ビット) 0x00010000
    ' _PC_64 (64 ビット) 0x00000000
    Msgbox hex$(GetControlFP())
End Sub

先にあげた検証環境では「C001F」という結果がかえってきました。
これにより、精度制御が_PC_64ビットであり、浮動小数点の演算の途中経過が64ビットの仮数を持つ80ビットのレジスタで計算していることが確認できました。

じゃあ、VBAたんは無罪なのね!

・・・じつは、参照しているCOMやDLLなどにより変更される可能性があり、この結果は全ての状況で正しいとは限りません。

たとえば、Jetを使うと途中の精度がかわったりするバグがあったりもします。
http://support.microsoft.com/kb/308702/ja

・・・やっぱり壊れているじゃないか!(憤怒)

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です