実数の注意点・回答編
回答がすぐに見えないように、別記事に分ける
数値の精度に注意
// case-1 ---- プログラムが止まらない
#define PI 3.1415926535
int main() {
double th , y ;
// 0〜πまで100分割でsinを求める
for( th = 0.0 ; th != PI ; th += PI / 100.0 ) {
y = sin( th ) ;
printf( "%lf %lf¥n" , th , y ) ;
}
return 0 ;
}
このプログラムの問題点は、終了条件が th != PI で書かれている点。
コンピュータの中では、データはすべて2進数で扱い、データを保存する桁数も有限である。
このため、0.0314159265 を 100 回 加えても、桁末尾の誤差のため、3.14159265 != 3.1415926535となってしまう。(ただしこの説明は10進数で説明しているけど、実際は2進数で同じような現象が発生している。)
int型とdouble型での暗黙の型変換
// case-2 ---- y の値が全てゼロ
int main() {
int th ;
double y ;
for( th = 0 ; th <= 360 ; th += 5 ) {
y = sin( th / 180 * 3.1415926535 ) ;
printf( "%d %lf¥n" , th , y ) ;
}
return 0 ;
}
このプログラムの問題点は、th / 180 。これがゼロになる原因。
コンピュータの中では、整数型 int と、実数 double 型では、計算の仕方が異なる。特に、実数型は、小数点の位置や指数を考えながら計算を行う必要があるため、処理に時間がかかる。このため、大量のデータを扱う場合にはコンピュータにとって簡単な計算となるように書く必要がある。
整数型は、小数点以下の値を持たない。このためコンピュータの中では、th = 5 の時、th / 180 を計算すると、5/180 = 0.0277 ではなく、5/180 = 0 で計算を行う(小数点以下は切り捨て)。
C言語の原則: 暗黙の型変換
同じ精度で計算できるのなら同じ精度の型で計算する。
精度が異なる場合は、精度の高い型に変換してから計算する。
int 演算子 int = int
int 演算子 double = double
このようなことが発生するので、y=sin(th…)の行は、以下のように書くべきである。
y = sin( th / 180.0 * 3.1415926535 ) ; // 180.0 は double 型 y = sin( (double)th / 180 * 3.1415926535 ) ; // 型キャストで double 型 y = sin( double( th ) / 180 * 3.1415926535 ) ; // C++の型変換関数 y = sin( (double)th / 180.0 * 3.1415926535 ) ; // 暗黙の型変換を排除
数値の範囲に注意
// 16bit コンピュータの時代に、 // 画面上のマウスとターゲットの距離を計算したかった int distance( int x0 , int y0 , int x1 , int y1 ) { int dx = x1 - x0 ; int dy = y1 - y0 ; return sqrt( dx * dx + dy * dy ) ; }
例えば、このプログラムで (x0,y0)=(0,0) , (x1,y1)=(200,200) 出会った場合、sqrt( 40000 + 40000 ) という計算が出て来る。
ところで、16bit コンピュータにおける int 型は、どうやって覚えているのか?
符号あり整数型
コンピュータの中では、負の数を扱う必要から、2の補数表現が用いられる。
x = 0000,0000,0000,1010(2) = 10(10) ~x = 1111,1111,1111,0101 1の補数 ~x + 1 = 1111,1111,1111,0110 1の補数に+1 ⇒ -10(10) x + ~x = 1111,1111,1111,1111 x + ~x + 1 = 1,0000,0000,0000,0000 ≒ 0 // 16bit目ははみ出るからzero
このため、16bit コンピュータの int 型で扱える数は、
max = 0111,1111,1111,1111(2) = 32767(10) min = 1000,0000,0000,0000(2) = -32768(10)
40000 は、16bit コンピュータでは、扱える範囲を越えている。
ということで、前述のプログラムは、
// 16bit コンピュータなら、long int 型は 32bit int distance( int x0 , int y0 , int x1 , int y1 ) { long int dx = x1 - x0 ; long int dy = y1 - y0 ; return (int)sqrt( dx * dx + dy * dy ) ; } // スピードを気にしないのなら(sqrtがdouble型だし...) double distance( double x0 , double y0 , double x1 , double y1 ) { double dx = x1 - x0 ; double dy = y1 - y0 ; return sqrt( dx * dx + dy * dy ) ; }
実数の注意点
C言語でプログラムを作成していて、簡単な数値計算のプログラムでも動かないと悩んだことはないだろうか?解らなくて友達のプログラムを真似したら動いたけど、なぜ自分のプログラムは動かなかったのか深く考えたことはあるだろうか?
まずは動く例
以下のプログラムは、見れば判るけど、th を 0度〜360度まで5度刻みで変化させながら、y = sin(th) の値を表示するプログラム。
// sin の値を出力 #include <stdio.h> #include <math.h> int main() { double th , y ; for( th = 0.0 ; th <= 360.0 ; th += 5.0 ) { y = sin( th / 180.0 * 3.1415926535 ) ; printf( "%lf %lf¥n" , th , y ) ; } return 0 ; }
動かないプログラム
では、以下のプログラムはどうだろうか?
// case-1 ---- プログラムが止まらない #define PI 3.1415926535 int main() { double th , y ; // 0〜πまで100分割でsinを求める for( th = 0.0 ; th != PI ; th += PI / 100.0 ) { y = sin( th ) ; printf( "%lf %lf¥n" , th , y ) ; } return 0 ; }
// case-2 ---- y の値が全てゼロ int main() { int th ; double y ; for( th = 0 ; th <= 360 ; th += 5 ) { y = sin( th / 180 * 3.1415926535 ) ; printf( "%d %lf¥n" , th , y ) ; } return 0 ; }
どちらも、何気なく読んでいると、動かない理由が判らないと思う。そして、元のプログラムと見比べながら、case-1 では、「!=」を「<=」に書き換えたり、case-2 では、「int th ;」を「double th ;」に書き換えたら動き出す。
では何が悪かったのか…
回答編
数値の範囲に注意
前節の回答編で示したが、数値の扱える値の範囲に注意すべきである。
私自身が自分で書いたプログラムで悩んだ例を以下に示す。
// 16bit コンピュータの時代に、 // 画面上のマウスとターゲットの距離を計算したかった int distance( int x0 , int y0 , int x1 , int y1 ) { int dx = x1 - x0 ; int dy = y1 - y0 ; return sqrt( dx * dx + dy * dy ) ; }
このプログラムを実行した時、通常はうまく動くのだけど、時々「sqrt は、負の値では計算できません」というエラーを表示する。
なぜだろうか?
回答編
2進数への変換(補助資料)
10進数で 123.45 は、1*102 + 2*101 + 3*100 + 4*10-1 + 5*10-2 を意味する。(あたりまえ)
2進数に変換する場合、整数部と小数部に分けて考えると、
10進数なら、それぞれを 10 で割る、10 をかけると 123 / 10 = 12.3 小数部に3 が出てくる。 0.45 * 10 = 4.5 整数部に4 が出てくる。 2進数なら、それぞれを 2 で割る、2をかけると...
123.45 を 2進数に変換 2)123 )10 = 1111011 )2  ̄ ̄ ̄ ̄ 2) 61 ... 1 次々と2で割って、余りを求める  ̄ ̄ ̄ ̄ 2) 30 ... 1  ̄ ̄ ̄ ̄ 2) 15 ... 0  ̄ ̄ ̄ ̄ 2) 7 ... 1  ̄ ̄ ̄ ̄ 2) 3 ... 1  ̄ ̄ ̄ ̄ 2) 1 ... 1  ̄ ̄ ̄ ̄ 0 ... 1 ✕2)0.45 )10 = 0.01110011001100...)2  ̄ ̄ ̄ ̄ ̄ ✕2)0.9 ... 0 次々と2倍して、整数部を求める  ̄ ̄ ̄ ̄ ̄ ✕2)1.8 ... 1 ※  ̄ ̄ ̄ ̄ ̄ ✕2)1.6 ... 1  ̄ ̄ ̄ ̄ ̄ ✕2)1.2 ... 1  ̄ ̄ ̄ ̄ ̄ ✕2)0.4 ... 0  ̄ ̄ ̄ ̄ ̄ ✕2)0.8 ... 0 ※の繰り返し  ̄ ̄ ̄ ̄ ̄ ✕2)1.6 ... 1  ̄ ̄ ̄ ̄ ̄ : よって、123.45 )10 = 1111011 .011100110011...)2
大域変数・局所変数・スコープ
繰り返しが動かない例
#include <stdio.h> int i ; void foo() { // foo() は 3個Aを表示するプログラム。 for( i = 0 ; i < 3 ; i++ ) printf( "A" ) ; } int main() { foo() ; return 0 ; }
では、
#include <stdio.h> int i ; void foo() { // foo() は 3個Aを表示するプログラム。 for( i = 0 ; i < 3 ; i++ ) printf( "A" ) ; } int main() { // A はいくつ表示される? for( i = 0 ; i < 3 ; i++ ) foo() ; return 0 ; }
大域変数と局所変数
編集中:もう少し加筆予定
制御構文について
プログラムの基本は、処理の順序を正しく理解していること。
まずは理解度確認
では、過去の電子情報3年プログラム応用のテスト問題から、以下のプログラムの実行順序を答えて下さい。
制御構文とフローチャート
構文の入れ子
型による処理速度の実験
授業のネタとするために、型によって計算時間がどう変化するか実験。レガシーなコンピュータを使ってきた人間には、float とか double とか出てきたら、「Z80な時代の頭」では数倍遅いのを期待したけど、FPU を搭載して当たり前のこの時代、そんなに差は出ない。
FPUという言葉さえ、最近は死語かな…
しかたがないので、macOS , Raspberry-Pi , Arduino 遅さの時代を逆行しながら実験。
// test.cxx #ifndef TYPE #define TYPE int #endif #include <stdio.h> TYPE foo( TYPE i ) { TYPE ans = 0 ; for( int j = 0 ; j < 30000 ; j++ ) { ans += i * i ; } return ans ; } int main() { TYPE y = 0 ; for( TYPE i = 0 ; i < 100000 ; i++ ) { y = foo( i ) ; } return 0 ; }
iMac で実験
iMac で実験。デスクトップ 64 bit マシンだし、そんなに差は出ないのは予想どおり。
bash-3.2$ uname -a Darwin imac2 17.4.0 Darwin Kernel Version 17.4.0: ... root:xnu-4570.41.2~1/RELEASE_X86_64 x86_64 bash-3.2$ g++ -O0 -DTYPE=int test.cxx bash-3.2$ time ./a.out user 0m6.857s bash-3.2$ g++ -O0 -DTYPE="long long int" test.cxx bash-3.2$ time ./a.out user 0m6.831s bash-3.2$ g++ -O0 -DTYPE=float test.cxx bash-3.2$ time ./a.out user 0m8.528s bash-3.2$ g++ -O0 -DTYPE=double test.cxx bash-3.2$ time ./a.out user 0m8.615s
Raspberry-Pi3 で実験
組み込み系の FPU などが貧弱なマシンを想定し、Raspberry-Pi 3 で同じことをやってみた。
64bit整数 long long int が想定外に遅いな。
raspberry-pi:~$ uname -a Linux raspberry-pi 4.14.22-v7+ #1096 SMP ...2018 armv7l GNU/Linux raspberry-pi:~$ gcc -O0 -DTYPE=int test.cxx raspberry-pi:~$ time ./a.out user 0m37.588s raspberry-pi:~$ gcc -O0 -DTYPE="long long int" test.cxx raspberry-pi:~$ time ./a.out user 1m18.535s raspberry-pi:~$ gcc -O0 -DTYPE=float test.cxx raspberry-pi:~$ time ./a.out user 0m52.206s raspberry-pi:~$ gcc -O0 -DTYPE=double test.cxx raspberry-pi:~$ time ./a.out user 0m50.287s
Arduino で実験
同じことを Arduino でやってみた。main のループは 1/10000 の回数にして、10000倍の時間を掲載…
ようやく「Z80 な頭」が期待している実験結果となったかな。
int 6880[sec] 16bit int long 6320[sec] 32bit int float 92080[sec] 32bit float double 92080[sec] Arduino Uno では、double = 32bit で float と同じ