ページ 11

pow関数を用いた整数計算について

Posted: 2012年10月03日(水) 00:35
by MoKo
はじめまして。Mokoと申します。

C言語プログラムを始めて3ヶ月くらいになります。
20年くらい前、大学でBASICを使ったことがあります。(つまりそんな年齢です。お恥ずかしい・・・)

環境はWindows7にEclipse4.2をインストールして実行しています。
コンパイラはMinGWというのでしょうか、Eclipseに付属のものを使用しています。

整数10のべき乗計算をしようと思い、以下のようなプログラムを作成してみました。

コード:

/* sample-1 */
#include <stdio.h>
#include <math.h>

int main ( void )
{
    int counter = 0;
    int answer = 0;

    for ( ; counter < 5; counter++ ) {
        answer = (int)pow ( 10.0, (double)counter );
        printf ( "%d\n", answer );
    }

    return 0;
}
実行結果は、

1
10
99
1000
9999

となりました。当然期待した結果ではありません。適当にいろいろいじって次のように修正したところ、
こちらの欲しい結果が表示されました。
(一度計算結果をdouble型の変数に代入し、改めてint型にキャストしています。)

コード:

/* sample-2 */
#include <stdio.h>
#include <math.h>

int main ( void )
{
    int counter = 0;
    double answer1 = 0.0;
    int answer2 = 0;

    for ( ; counter < 5; counter++ )
    {
        answer1 = pow ( 10.0, (double)counter );
        answer2 = (int)answer1;
        printf ( "%d\n", answer2 );
    }

    return 0;
}
質問1.
sample-1とsample-2で、なぜ結果が異なるのでしょうか。(なぜsample-2だと正しい結果が得られるので
しょうか。)
double型とint型ではデータのサイズが異なるため、キャストすると情報がこぼれてしまう、というところは
なんとなく(あやふやですみません)理解しているつもり(またまたすみません)です。
特に疑問に思うのは、double型の変数に代入することでいった何が起きるのか?というところです。
あるいはEclipseの使い方を間違えていたりするのでしょうか。

質問2.
sample-2のようなコードは正しいのでしょうか。つまり、整数のべき乗計算を行う際、sample-2のように
一度double型の変数に代入し、その後intへキャストすれば必ず正しい解を得られるのでしょうか。

以上、よろしくお願いいたします。

Re: pow関数を用いた整数計算について

Posted: 2012年10月03日(水) 03:00
by box

コード:

// sample-1とsample-2で結果が異なる理由についてはよくわかりません。
// 私のところでは、sample-2もsample-1と同じ結果でした。
// pow関数の内部処理で10^2がちょうど100にならず、99.9999......のような値に
// なった結果、それをint型にキャストして99になってしまう、というような
// ことが起きているのではないか、という勝手な推測です。
// 整数のべき乗を求めるのであれば、pow関数を使わない方がいいような気がします。

#include <stdio.h>

int main(void)
{
    int pow, i;

    for (pow = 1, i = 0; i < 5; i++) {
        printf("%d\n", pow);
        pow *= 10;
    }
    return 0;
}

Re: pow関数を用いた整数計算について

Posted: 2012年10月03日(水) 21:07
by みけCAT
EPSを追加することにより正常動作になりました。

コード:

/* sample-1 */
#include <stdio.h>
#include <math.h>
 
#define EPS (1e-8)
 
int main ( void )
{
    int counter = 0;
    int answer = 0;
 
    for ( ; counter < 5; counter++ ) {
        answer = (int)(pow ( 10.0, (double)counter )+EPS);
        printf ( "%d\n", answer );
    }
 
    return 0;
}

Re: pow関数を用いた整数計算について

Posted: 2012年10月03日(水) 22:34
by かずま
Ubuntu 10.04 の gcc 4.4.3 では、sample-1 の結果が sample-2 と同じものになりました。

私は次のように考えました。

pow(x, y) ですが、x > 0 のとき、exp(log(x)*y) で計算することができます。
しかし、これでは、x や y が整数のときでも、小数の計算になり、誤差が入ります。
pow の実装によっては、引数が整数の場合は特別扱いして、誤差が入らないように
なっているものもあるようですが、Mokoさんの環境ではそうなっていないのでしょう。

誤差があるため、本来 100.0000 となるべきものが 99.99999999999999987 などと
なっていたとしましょう。

コンパイラが MinGW ということは、gcc ですが、これは関数の返却値が double の
場合、浮動小数点レジスタに値を入れて返します。
Intel の CPU の浮動小数点レジスタは 80ビットで、その中の 64ビットが仮数です。
2進で 64ビットだと 10進で約19桁の精度があります。
これを int に変換するとき、小数分は切り捨てとなるので 99 になります。

次に、double の変数を経由する場合です。
double は 64ビットで、仮数部は 52 ビットですが、仮数の先頭 1ビット目の 1が
省略されているので、それを補うと精度は 53ビットです。10進で約16桁となります。
浮動小数点レジスタの 64ビットの仮数を double の 53ビットに変換するときは、
切り捨てではなく、丸めがおこなわれるので、100.0000000000000 となります。

では、すべてのコンパイラで、このような結果になると思うかもしれませんが、
double pow(double x, double y); と宣言されているのですから、結果を
double の変数に明示的に代入しなくても、double への変換を行って、それから、
int に 変換するようなコードをコンパイラが生成するようになっていれば、
sample-1 でも sample-2 と同じ結果になるでしょう。

Re: pow関数を用いた整数計算について

Posted: 2012年10月03日(水) 23:16
by たいちう
何故99になるかについては、既に説明されている通りでしょう。

どうすべきか、ということについてですが、
浮動小数点数の計算には誤差が付きものであることを認識し、
powを使わない方法(10のべき乗などは自前で計算できる)、
intへのキャストではなく、
(int)floor(x + 0.5)などの自前のint型への変換を用意する、
などがコンパイラに依存しない確実な方法だと思います。

Re: pow関数を用いた整数計算について

Posted: 2012年10月04日(木) 16:48
by beatle
C99からはround関数が標準の仲間入りをしたので、それを使えば一番近い整数へ直すことができます。

Re: pow関数を用いた整数計算について

Posted: 2012年10月04日(木) 23:11
by MoKo
質問者のMoKoです。

boxさま、みけCATさま、かずまさま、たいちうさま、beatleさま、ご回答ありがとうございました。
こんな短時間にこれだけのご回答をいただけるとは、いい時代になったものですね(感謝、感謝)。

>> boxさま

> 整数のべき乗を求めるのであれば、pow関数を使わない方がいいような気がします。

どうやらそのようです。今回は必ず底が10であるため、関数にするまでもないですが、
汎用的に使用する状況では、関数を作成してみようと思います。
「int pow ( int base, int exp )」のようになるのでしょうか・・・
今回は、doubleからintへのキャストで、場合により結果が異なるのが非常に気になったため
質問させていただきました。


>> みけCATさま

> EPSを追加することにより正常動作になりました。

たしかにこちらの環境でもうまくいきました。
「#define」について調べてみたところ、「プリプロセッサがコンパイル時に行う前処理」で、
「#define 文字列1 文字列2」の場合、文字列1を文字列2に置き換える。とありました。
つまり、

コード:

#define EPS (1e-8)
answer = (int)(pow ( 10.0, (double)counter )+EPS);
とあれば、

コード:

answer = (int)(pow ( 10.0, (double)counter )+(1e-8));
となるわけですよね?(コードを直接書き換えても同じ結果が得られるようです。)
すると、今度は新たな疑問が出てきます。「8」とした根拠は何なのでしょうか?


>> かずまさま

> コンパイラが MinGW ということは、gcc ですが、これは関数の返却値が double の
> 場合、浮動小数点レジスタに値を入れて返します。
> Intel の CPU の浮動小数点レジスタは 80ビットで、その中の 64ビットが仮数です。
> 2進で 64ビットだと 10進で約19桁の精度があります。
> これを int に変換するとき、小数分は切り捨てとなるので 99 になります。
>
> 次に、double の変数を経由する場合です。
> double は 64ビットで、仮数部は 52 ビットですが、仮数の先頭 1ビット目の 1が
> 省略されているので、それを補うと精度は 53ビットです。10進で約16桁となります。
> 浮動小数点レジスタの 64ビットの仮数を double の 53ビットに変換するときは、
> 切り捨てではなく、丸めがおこなわれるので、100.0000000000000 となります。

この部分が、ズバリ、期待していた内容の回答でした。
実際、この回答だけでは「???」な感じだったのですが、丁寧な解説のおかげで、
Webで調べるいい足がかりとなりました。

ウィキペディアにも、『x86のFPUは内部表現に80ビット(拡張倍精度)表現を使っている。』とありました。

プログラマのみなさんはこういうことを前提にプログラムを作成されているのですね。
正直、別世界のできごとのようです。


>> たいちう さま

私の質問2へのご回答、ありがとうございます。

> (int)floor(x + 0.5)などの自前のint型への変換を用意する、
> などがコンパイラに依存しない確実な方法だと思います。

googleで「floor 0.5」と入力すると約48,700,000件ヒットしました。
こういう常套手段をササッと使えるようになると、一段と楽しくなるのでしょうね。
(お仕事でプログラムをやっている人は楽しむ余裕もないのでしょうが・・・)


>> beatleさま

> C99からはround関数が標準の仲間入りをしたので、それを使えば一番近い整数へ直すことができます。

調べてみたところ、round関数の戻り値もdouble型のようですが・・・
仮数部を「1.00000...」にしてくれると考えてもよろしいのでしょうか?
たいちう さまのおっしゃる、『コンパイラに依存しない確実な方法』と考えてよろしいのでしょうか?
とりあえず、限りなく解答に近いヒントをもらったということで、さらに調べてみようと思います。


みなさま、本当にありがとうございました!
これで解決とさせていただきます。

Re: pow関数を用いた整数計算について

Posted: 2012年10月05日(金) 06:25
by beatle
MoKo さんが書きました:>> beatleさま

> C99からはround関数が標準の仲間入りをしたので、それを使えば一番近い整数へ直すことができます。

調べてみたところ、round関数の戻り値もdouble型のようですが・・・
仮数部を「1.00000...」にしてくれると考えてもよろしいのでしょうか?
たいちう さまのおっしゃる、『コンパイラに依存しない確実な方法』と考えてよろしいのでしょうか?
そう、double型で返すのですよね。なんででしょうね。(恐らくintとdoubleの表せる桁数の差だと思います)

roundの戻り値は、渡した小数に最も近い整数となるので、そのままintにキャストすれば求めたい整数になります。
C99の規格で定められている関数ですから、C99準拠のコンパイラなら移植性があります。

Re: pow関数を用いた整数計算について

Posted: 2012年10月06日(土) 22:51
by みけCAT
MoKo さんが書きました:>> みけCATさま

> EPSを追加することにより正常動作になりました。

たしかにこちらの環境でもうまくいきました。
「#define」について調べてみたところ、「プリプロセッサがコンパイル時に行う前処理」で、
「#define 文字列1 文字列2」の場合、文字列1を文字列2に置き換える。とありました。
つまり、

コード:

#define EPS (1e-8)
answer = (int)(pow ( 10.0, (double)counter )+EPS);
とあれば、

コード:

answer = (int)(pow ( 10.0, (double)counter )+(1e-8));
となるわけですよね?(コードを直接書き換えても同じ結果が得られるようです。)
すると、今度は新たな疑問が出てきます。「8」とした根拠は何なのでしょうか?
EPSに1e-8を使用したのは適当です。
実数を扱う場合、このように小さな数を用いて誤差を吸収するとうまくいくことがあります。