Garminウォッチフェイスの作成:パフォーマンスを参考に、どのような実装をすればパフォーマンスアップになるのか、ちょっと調べてみた。
計測するウォッチフェイス
今回使用したのは、作成中のアナログ時計ウォッチフェイス。
時針が赤、分針・秒針が白の三角形。
画面の周囲に目盛りを描画したもの。目盛りは時間を表す部分は2ドットの幅で長めに描画し、時間と時間の間は5分割とした。
計測結果
一応、以下のプログラムで計測した結果は、実行部分のみが変わり、グラフィック描画、表示部分の時間の変化はなかったため、実行部分の時間のみを記載する。
プログラム中の変数centerは時計の中心座標を表している。
プログラム1
20年ほど前の感覚があり、sin/cosを求める計算量を少なくするプログラムをまず作ってい見た。昔は四則演算よりsin/cosの計算が遅かったので。
そこで、3角形の頂点と目盛りの始終点の座標の計算には、加法定理だったかな?、変異角のsin/cosだけを求めて、後は四則演算で座標を求めていく方式にしてみた。
以下は、目盛りを描画する部分だけを抜粋したもの。
// centerを中心に、angleを変異角として、start位置から点列を作成する
// centerを中心に円周上の点を作る感じ
class DeltaPosition {
var vSin;
var vCos;
var centerPosition;
var position;
function initialize(angle, center, start) {
vSin = Math.sin(angle);
vCos = Math.cos(angle);
centerPosition = center;
position = start;
}
function setPosition(start) {
position = start;
}
// 現在の円周上の点を取り出す
function getPosition() {
return [centerPosition[0] + position[0], centerPosition[1] + position[1]];
}
// angle分移動した円周上の点を取り出す
function next() {
position = [position[0] * vCos - position[1] * vSin, position[0] * vSin + position[1] * vCos];
return getPosition();
}
}
// 変異角度を使い座標を生成しハッシュマークを表示する
// 角度の生成はDeltaPositionクラスを使用する
function drawHashMarks(dc) {
var vS = new DeltaPosition(Math.PI / 30.0, center, [0, 0 - center[1]]);
var vEl = new DeltaPosition(Math.PI / 30.0, center, [0, 10 - center[1]]); // 時のマーク
var vEs = new DeltaPosition(Math.PI / 30.0, center, [0, 5 - center[1]]); // 12分のマーク
dc.setPenWidth(2);
dc.drawLine(vS.getPosition()[0], vS.getPosition()[1], vEl.getPosition()[0], vEl.getPosition()[1]);
for (var i = 1; i < 60; ++i) {
var vNS = vS.next();
var vNEl = vEl.next();
var vNEs = vEs.next();
if (i % 5 == 0) {
dc.setPenWidth(2);
dc.drawLine(vNS[0], vNS[1], vNEl[0], vNEl[1]);
}
else {
dc.setPenWidth(1);
dc.drawLine(vNS[0], vNS[1], vNEs[0], vNEs[1]);
}
}
}
この実装にした場合の結果は131ms。
プログラム2
比較対象として、目盛りの座標を求める方法に、目盛りの角度からsin/cosを毎回求めたものを作成してみた。同様に目盛りを描画する部分のみを抜粋したもの。
function drawHashMarks1(dc) {
for (var i = 0; i < 60; ++i) {
var vSin = Math.sin(Math.PI / 30.0 * i);
var vCos = Math.cos(Math.PI / 30.0 * i);
if (i % 5 == 0) {
dc.setPenWidth(2);
dc.drawLine(center[0] + center[0] * vSin, center[1] + center[0] * vCos,
center[0] + (center[0] - 10) * vSin, center[1] + (center[0] - 10) * vCos);
}
else {
dc.setPenWidth(1);
dc.drawLine(center[0] + center[0] * vSin, center[1] + center[0] * vCos,
center[0] + (center[0] - 5) * vSin, center[1] + (center[0] - 5) * vCos);
}
}
}
この実装にした場合の結果は51ms。サンプル1より明らかに少なくなった。
サンプル3
サンプル1とサンプル2で何が違うか、差異点が大きいので、まずサンプル1で遅いと思われる部分。座標を構築するために利用しているDeltaPositionクラスをnewしているのがいけないのではないかと考え、この部分をnewしないコードにしてみた。
これは、やはり一昔前の考え方で、メモリの取得開放というのはコストが結構高いのではないかと思ったから。
function drawHashMarks2(dc) {
var position = [[0, 0 - center[1]], [0, 10 - center[1]], [0, 5 - center[1]]];
var vSin = Math.sin(Math.PI / 30.0);
var vCos = Math.cos(Math.PI / 30.0);
dc.setPenWidth(2);
dc.drawLine(center[0] + position[0][0], center[1] + position[0][1],
center[0] + position[1][0], center[1] + position[1][1]);
for (var i = 1; i < 60; ++i) {
for (var j = 0; j < 3; ++j) {
position[j] = [position[j][0] * vCos - position[j][1] * vSin, position[j][0] * vSin + position[j][1] * vCos];
}
if (i % 5 == 0) {
dc.setPenWidth(2);
dc.drawLine(center[0] + position[0][0], center[1] + position[0][1],
center[0] + position[1][0], center[1] + position[1][1]);
}
else {
dc.setPenWidth(1);
dc.drawLine(center[0] + position[0][0], center[1] + position[0][1],
center[0] + position[2][0], center[1] + position[2][1]);
}
}
}
この実装にした場合の結果は93ms。サンプル1よりは早くなったが、サンプル2よりは遅い。
ここまででの結果考察
sin/cosを求める数で遅くなるわけでもなく、new等のメモリ管理で劇的に遅くなるでもなかったので、ここは単純に演算回数が少なくなればなるほど早くなるのか。
サンプル4
サンプル2をもとに、計算回数を少なくするような実装にしてみたもの。
sin/cosの角度の計算に使っているMath.PI / 30.0
の計算と長短目盛りの長さの計算をfor文の外に出したもの。
// 角度の絶対値を使用し座標を生成しハッシュマークを表示する
function drawHashMarks1(dc) {
var vLong = center[0] - 10;
var vShort = center[0] - 5;
var aDel = Math.PI / 30.0;
for (var i = 0; i < 60; ++i) {
var vAngle = aDel * i;
var vSin = Math.sin(vAngle);
var vCos = Math.cos(vAngle);
if (i % 5 == 0) {
dc.setPenWidth(2);
dc.drawLine(center[0] + center[0] * vSin, center[1] + center[0] * vCos,
center[0] + vLong * vSin, center[1] + vLong * vCos);
}
else {
dc.setPenWidth(1);
dc.drawLine(center[0] + center[0] * vSin, center[1] + center[0] * vCos,
center[0] + vShort * vSin, center[1] + vShort * vCos);
}
}
}
この実装にした場合の結果は43ms。サンプル2の51msより8ms早くなっている。
とりあえずの結論
今回のサンプルを見る限り、実行処理部分に関しては、
- 関数の呼び出しなども含めできるだけ処理数が少なくなるような実装を心掛ける必要がある
ということか。
Monkey CはC#やC++などとは異なり、コンパイル・リンクなどでプログラムの最適化がされないようので、自分で最適化をしなければいけないということになる。
もしかしたらPythonなどと同じ、インタプリタな言語なのかもしれない。
コメント