~閑話休題 割り算は遅いか

 センサを利用したプログラムでは、データシートに書かれた導出式にしたがって計算式を書いていますが、Arduino UNOは8ビット・マイコンです。Windows 10などでは当たり前にハードの支援が期待できる浮動小数点の演算は、8ビット・マイコンではとても複雑な処理が内部で行われます。処理時間もかかります。

 Arduino IDE 1.8.8で実験しています。

2^16の計算

 16ビットの温度などのデータを読み出した後、そのデータに対して2^16で除算するというケースがあります。Arduinoでは2^16はpow(2,16)です。

void setup() {
Serial.begin(9600);
float n = pow(2,16);
float m = 1/n;
Serial.println(n);
Serial.println(m,18);
}

void loop() {
// put your main code here, to run repeatedly:
}

 結果は、

 Googleの計算でも同じ結果になりました。

2^16 = 65536
1/2^16*1000000 = 15.2587890625

 2^16は、数学的には2を16回かけたことと同じですから、

void setup() {
Serial.begin(9600);
float n = 2*2*2*2*2*2*2*2*2*2*2*2*2*2*2*2;
float m = 1/n;
Serial.println(n);
Serial.println(m,18);
}

 結果は期待したとおりになりませんでした。

 float をdoubleと変えても同じです。Arduino UNOでは二つの型は同じ長さ4バイトです。

 SpresenseのArduino IDEで実行しました。正しい結果が出ました。困ったことにUNOとは違います。

 UNOに戻します。試行錯誤して、2を14回かけたときまでは正常に計算し、それ以上ではマイナスの値になることがわかりました。そして、次のように記述することで、正しい結果が得られました。

void setup() {
Serial.begin(9600);
float n = 2*2*2*2*2*2*2*2*2*2*2*2*2*2*2.0*2.0;
float m = 1/n;
Serial.println(n);
Serial.println(m,18);
}

 実行結果です。

どちらが高速に演算するのか

 2種類の計算方法の演算時間を確かめるために、10回、繰り返すようにスケッチを書きました。

double n ;
unsigned long time;

void setup() {
Serial.begin(9600);
Serial.println(pow(2,16));
n = 2*2*2*2*2*2*2*2*2*2*2*2*2*2*2.0*2.0;
Serial.println(n);
}

void loop() {
time = micros();
Serial.println(time);
Serial.print("pow 2^16 ");
for (int i = 0; i<10; i++){
double n = pow(2,16);
}
Serial.println(micros()- time);
time = micros();
Serial.println(micros());
Serial.print("1/ (pow 2^16) ");
for (int i = 0; i<10; i++){
double n = 1 / pow(2,16);
}
Serial.println(micros()- time);
time = micros();
Serial.println(micros());
Serial.print("2^16 ");
for (int i = 0; i<10; i++){
double n = 2*2*2*2*2*2*2*2*2*2*2*2*2*2*2.0*2.0;
}
Serial.println(micros()- time);
time = micros();
Serial.println(micros());
Serial.print("1/2^16 ");
for (int i = 0; i<10; i++){
double n = 1/(2*2*2*2*2*2*2*2*2*2*2*2*2*2*2.0*2.0);
}
Serial.println(micros()- time);
Serial.println("----------------");
delay(5000);
}

 実行結果です。1回目と、2回目以降で様子が異なります。

 10回を10000回に変更した実行結果です。ほとんどループの時間が変わりません。どうも、無駄なループだとコンパイラが判断して最適化した結果、ループ内の計算を何度も実行していないようです。

 

 C:\Program Files (x86)\Arduino\hardware\arduino\avr/platform.txtの最適化オプション -Osを-O0に変更しました(3か所)。
 その結果、いずれの処理時間もdouble n = 1/(2*2*2*2*2*2*2*2*2*2*2*2*2*2*2.0*2.0);と同じようになりました。想像ですが、この割り算は、最適化がかかっていなかったということですね。

関数を使う

 2を16回掛けるのはArduino UNOでは御法度で、pow()関数を使うべきだということがわかりました。しかし、pow(2,16)の結果は定数ですね。次のプログラムで処理速度を見ました。
 (1)はそのまま計算、(2)は事前に求めた数値で割る、(3)は逆数を掛けるという違いです。

(1)  Temp / pow(2,16)
(2)  Temp / 65536
(3)  Temp * 0.0000152587890625

 スケッチです。

double n ;
unsigned long time;
unsigned long time0;
float Temp = 25.6 ;

void setup() {
Serial.begin(9600);
Serial.println((1/pow(2,16)),16);
Serial.println((1.0/65536),16);
Serial.println(0.0000152587890625,16);
Serial.println("===========");
}

void loop() {
time0 = micros();
time = micros();
Serial.println(time);
time = micros();
Serial.println(micros()-time0);
Serial.print("1/ (pow 2^16) ");
for (int i = 0; i<10000; i++){
double n = Temp / pow(2,16);
}
Serial.println(micros()- time);
time = micros();
Serial.println(micros()-time0);
Serial.print("1/65536 ");
for (int i = 0; i<10000; i++){
double n = Temp / 65536;
}
Serial.println(micros()- time);
time = micros();
Serial.println(micros()-time0);
Serial.print("* 0.0000152587890625 ");
for (int i = 0; i<10000; i++){
double n = Temp * 0.0000152587890625;
}
Serial.println(micros()- time);
Serial.println("----------------");
delay(5000);
}

 実行結果です。乗算は割り算の半分の時間で実行できています。
 (pow 2^16) の実行時間が定数とほぼ同じということは、コンパイラは、実行時に累乗の計算させるコードを出力しているのではなく、コンパイル時に計算した値を定数として埋め込んでいると想像できます。

 実験の結果、割り算はなるべく使わないほうがよいことがわかりました。桁の多い数値計算は、事前に電卓で計算し、乗算になるように式を変形しておくと、処理時間が短縮できます。

前へ

SpresenseでLチカから始める (24) Wireライブラリ 温度気圧BMP280

次へ

SpresenseでLチカから始める (25) Wireライブラリ 温度気圧MS5837-30BA