CS/JS – Mathクラスメソッドの実行速度

概要

2点間の距離を比較するときに、sqrtといった関数で距離を求める汎用メソッド/関数を使うのに対して、距離の2乗のままで比較して計算スピードを上げることが考えられる。

昔の大型計算機で大きな規模の方程式を解くときなどは、計算スピードを上げるために距離の2乗のままで比較をしていたりしたが、簡単なシミュレーションやアニメーションなどで、それがどれくらい効いてくるのかがよくわからなかった。

そこで、他の関数も含めて、Mathクラスのクラスメソッドとして定義されている浮動小数点系の関数群の実行時間を計ってみた。

結果概要

加減乗除算の間では実行時間にあまり差はなく、これらを1としたときの各関数の実行時間程度は以下のようになった。

+-*/ 1
abs、sqrt、exp、log、random 1
sin、cos 2~3
tan、atan 4~5
atan2 5~6
asin、acos 6~7
pow(**) 7~8

一般的な方針

先の結果から、浮動小数点の計算を含むコードに関して、以下のようにまとめられる。

  • 距離の計算などにsqrtを気軽に使ってもよい(足し算を一つ増やす程度)
  • exp、logも気軽に使ってよい
  • 累乗計算はコストが高い
    • 距離計算などで自乗項が出てくる場合、乗算に変更した方がよい
  • 三角関数は、加減乗除の2~5倍のコスト
  • 逆三角関数は、加減乗除の5~7倍のコスト

参考

実行環境1

  • mouse製ノートPC
  • Celeron-325U/1.70GHz
  • RAM 8GB
  • Windows 10 Home(64bit)

コンソールからテストコードを実行。

ここでa = 1.1、b = -2.2で、各関数の繰り返し回数は100万回。時間の単位はミリ秒。

a+b 17 17 17
a-b 26 26 28
a*b 20 18 20
a/b 16 17 18
a**b 156 157 157
abs(a) 29 27 30
abs(b) 30 29 30
sqrt(a) 29 30 31
sin(a) 71 71 71
cos(a) 59 61 61
tan(a) 83 84 84
asin(a) 125 128 128
acos(a) 124 127 125
atan(a) 85 84 87
atan2(a) 138 140 146
exp(a) 15 17 17
log(a) 15 17 18
random(a) 26 27 27

実行環境2

  • 自作PC
  • Intel Core i5-4670/3.40GHz
  • RAM 4.00GB
a+b  9  10  9
a-b 12 11 12
a*b 7 9 7
a/b 7 7 7
a**b 72 71 72
abs(a) 11 12 17
abs(b) 12 17 12
sqrt(a) 13 13 13
sin(a) 27 25 23
cos(a) 24 25 25
tan(a) 43 43 42
asin(a) 53 53 54
acos(a) 53 50 51
atan(a) 41 41 41
atan2(a) 61 62 58
exp(a) 7 7 8
log(a) 6 7 7
random(a) 12 13 12

テストコード

 

CS/JS – Math.signが使えない話

CoffeeScriptで書いたコードが、ローカルでもネット上のWordPressでも動くことを確認したが、Android端末で動かないという事象が発生した。

同じ症状がWindowsマシンのExplorerでも起こったので、開発ツールで確認したところ、”Math.signはサポートされていない”とのこと。

調べてみると、JavaScript | MDN(Mozilla Developer Network)のMath.sign()のところでブラウザ実装状況を見ると、

  • デスクトップでは、Chrome38、FireFox(Gecko)25、Opera25はサポート、IEとSafariは未サポート
  • モバイルでは、Firefox Mobile(Gecko)25のみサポートで、あとは全滅

当分、この関数は利用できそうにないので、以下のように値を絶対値で割って取り出すのが手っ取り早い(可読性は悪くなるが)。

 

スクリプトへの引数渡し

たとえば以下のような場合。

  • CoffeeScript、Javascriptの外部スクリプトに引数を渡したい
  • 外部スクリプトの定数定義を柔軟に変更可能なようにしたい

定数となる変数定義をスクリプト内に書くと、定数の値を変更したいときに再コンパイルやアップロードなどが必要になって煩雑。

以下の方法が考えられる。

  • HTMLの外部スクリプト呼び出しより前に、変数定義のscript要素を書く(変数のスコープはグローバルになる)
    • その際、行の末尾にセミコロン(;)をつけること
  • 外部スクリプト内で上で定義した変数を利用する

HTML

外部スクリプト

CoffeeScriptも実行時にはJSにコンパイルしたものを読み込むので、変数定義もJavascriptの文法で末尾にセミコロン(;)をつけなければならない。

上の例では、定数を意識して変数名を大文字のスネークケースとしたが、通常の変数名でも実行に問題はない。

 

CoffeeScript – geom2dパッケージ

パッケージ名

taustation_geom2d

クラス

taustation_geom2d.Vector
2次元ベクトルを保持し、各種演算を行うメソッドを提供する。
taustation_geom2d.MovingAgent
位置と速度を保持し、互いの位置関係を判定するメソッドを提供する。

コードの骨組み

 

CoffeeScript – geom2d.Vectorクラス

パッケージ

taustation_geom2d

クラス名

taustation_geom2d.Vector

クラス仕様

概要

x、y二つの成分を持ち、絶対値の計算や加減乗算など2次元ベクトルの代数演算を行うメソッドを提供する。

クラス変数

なし

クラス・メソッド

distance(pos1, pos2)
2つのVectorオブジェクトpos1、pos2を点を表す位置ベクトルとして、2点間の距離を返す。

インスタンス・プロパティ

x
ベクトルのx成分
y
ベクトルのy成分

コンストラクタ

constructor(x, y)
x成分とY成分を指定してVectorのインスタンスを生成する。

インスタンス・メソッド

基本メソッド

set(x, y)
Vectorのx成分とy成分をセットする。
toString()
Vectorの文字列形式の表現を返す。
getClone()
元のVectorオブジェクトと同じ内容のクローンを生成して返す。

演算

abs
Vectorの絶対値(長さ)を返す。
times(coeff)
Vectorに定数coeffを乗じたベクトルを新たに生成して返す。
timesEq(coeff)
Vectorに定数coeffを乗じて変更する。新しいオブジェクトは生成しない。
div(coeff)
Vectorを定数coeffで除したベクトルを新たに生成して返す。
divEq(coeff)
Vectorを定数coeffで除して変更する。新しいオブジェクトは生成しない。
plus(other)
Vectorにotherベクトルを加えたベクトルを新たに生成して返す。
plusEq(other)
Vectorにotherベクトルを加えて変更する。新しいオブジェクトは生成しない。
minus(other)
Vectorからotherベクトルを減じたベクトルを新たに生成して返す。
minusEq(other)
Vectorからotherベクトルを減じて変更する。新しいオブジェクトは生成しない。
inprod(other)
Vectorとotherベクトルの内積値を返す。
ang(other)
Vectorとotherのなす角を計算する(0~π)。

新たなVectorの生成

getUnit()
Vectorと同じ向きの単位ベクトル(長さが1)を新たに生成して返す。
getInverse()
Vectorと逆向きのベクトルを新たに生成して返す。
getOrthogonalR()
Vectorから右に90度回転したベクトルを新たに生成して返す。
getOrthogonalL()
Vectorから左に90度回転したベクトルを新たに生成して返す。
getUnitOrthogonalR()
Vectorから右に90度回転した単位ベクトルを新たに生成して返す。
getUnitOrthogonalL()
Vectorから左に90度回転した単位ベクトルを新たに生成して返す。

判定

isHeadingToRightOf(other)
このVectorオブジェクトの進行方向が、引数のVectorオブジェクトotherの進行方向より右方を指しているときtrue、左方を指しているときfalseを返す。
isHeadingToLeftOf(other)
isHeadingToRightOf()と左右逆の判定結果を返す
isInFrontOf(movingAgent)
このVectorオブジェクトが位置ベクトルとして、その点が引数のMovingAgentオブジェクトの前方(左右180度より進行方向の側)にあるときtrue、後方にあるときfalseを返す。真横の時は前方と判定される。
isIntheRearOf(movingAgent)
isInFrontOf()と前後逆の判定結果を返す。
isOnTheRightOf(movingAgent)
このVectorオブジェクトが位置ベクトルとして、その点が引数のMovingAgentオブジェクトの右方にあるときtrue、左方にあるときfalseを返す。進行方向の延長線上にあるときは右方と判定される。
isOnTheLeftOf(movingAgent)
isOnTheRightOf()と左右逆の判定結果を返す。

コード

 

CoffeeScript – グローバル変数

グローバルにならない

CoffeeScriptでグローバル変数を期待して以下のように書いたとする。

一見すると問題なく動いているように見えるが、これを別ファイルのパッケージにして動かそうとすると問題が顕在化する。以下のように”globalVarが存在しないよ”というエラーが出るのである。

これら2つのファイルをjsにコンパイルしてブラウザのscript要素で読み込んだ場合、そのイメージは以下のようになる。

パッケージでグローバル定義したつもりだが、コンパイル時に無名関数でクロージングされるため、グローバルにならず、他のファイルから読み込むことができない。

解決策1

グローバル変数をwindowオブジェクトの要素にしてしまう。

解決策2

rootコンテキストで、以下のパターンを使う。

 

CoffeeScript – パッケージ管理

パッケージ化の方法について

簡単な方法~齟齬あり

クラス/オブジェクトのプロパティにする

クラスやオブジェクトをパッケージの名前とし、パッケージ内のクラスなどはそのプロパティとして定義する。

ただしこの方法は、最適とは言えない。

1行目で名前空間に用いるクラスを定義し、7行目でMyClassPack1のプロパティとしている。

10行目で名前空間に用いるオブジェクトを定義し、16行目でMyClassPack2のプロパティとしている。

20行目でMyClass単独を用いた場合、直前に13行で定義されたMyClassが参照される。

パッケージ内での名前参照の祖語

パッケージ間で名前が重複する場合、下手をすると想定外のオブジェクトが使われてしまう。

次の例は、Pack1Pack2の2つのパッケージで同じクラス名を使い、各パッケージの中ではパッケージ名なしで使おうとしたもの。

ところが29行目で、Pack1のクラスのはずのところがInnerClassにはPack2のものが使われてしまっている。

これは、25行目以降の実行文の前にグローバル変数InnerClassの内容Pack1のものからPack2のものへと上書き定義されたため。

結局この方法だと、各パッケージの中でもクラス名の前にパッケージ名をつけないといけないようで、コーディング上は元のクラス名に識別子を組み込むのとあまり変わらない。

有効な方法~即時実行関数

先の方法は、すべてグローバル領域で行われていたため、パッケージ内でのみ参照するクラスも上書きされてしまった。

そこで、パッケージごとに即時実行される無名関数で括ってみる。

1行目のようにグローバル領域でパッケージ名に使うオブジェクト/クラスを定義し、パッケージごとに即時無名関数で括る。

パッケージ内ではパッケージなしでアクセスし、外からアクセスするときはパッケージ名を付してアクセス。

これで一応、パッケージ化とコーディングの簡易化を両立させることができる。

パッケージ名について

パッケージ名は、「世界規模での名前衝突を避ける」ように潔癖を極めるなら、Javaで行われているようなドメイン名管理が答となる。

なお、Javaの場合はimport文で指定したパッケージはクラス名のみで使えるが、Javascriptではエイリアスを明示的に指定しなければならないらしい。

JavaScriptスタイルガイドでは、「長い型名をエイリアスし可読性を向上させる」ために以下のように示唆されている。

“ローカルのエイリアスを使うことで長い型名の可読性を向上できる場合はそうしてください. エイリアスの名前は型名の最後の部分にしてください.”

なお、npmのサイトでいろいろなパッケージを見てみると、区切りにはアンダースコア(_)ではなくマイナス(-)を使っている。

 

CoffeeScript – タイマ

setInterval

setInterval()関数の引数に、アニメーションフレームを描画する関数と時間間隔を指定する。時間間隔はミリ秒単位。

描画の開始/停止をコントロールするには、setInterval()関数の戻り値を変数に保存しておき、停止するときにclearInterval()関数の引数でその変数を指定する。

一時停止/再開の機能は特に実装されていないが、描画関数を工夫することで可能。

描画関数を、以下のように無名関数で書くこともできる。

この処理を他のオブジェクトのメソッドの中に書いて、オブジェクトのプロパティを利用する場合は、1行目のアローをファットアロー(=>)にする。

 

CoffeeScript – クラスのメソッド

アクセス可能性

以下のコードで、クラスの関数へのアクセス可能性を確認する。

各関数へのアクセス可能性は以下の通り。

定義\引用 クラス インスタンス メソッド内
(@なし)
 メソッド内
(@つき)
fnc: ->  TypeError  ○ ReffError
@fnc: ->  ○ TypeError ReffError TypeError
fnc = -> TypeError TypeError TypeError
@fnc = -> TypeError ReffError TypeError

なお、上記コードの定義部分のJavaScriptコンパイル結果は以下の通り。

クラス・メソッド

クラス内で@つきでメソッドを定義すると、クラスメソッドになる。

上記のいずれもJavaScriptへのコンパイル結果は同じになる。

クラスメソッドは、クラス内で呼び出すときでもクラス名をつける。

publicなインスタンス・メソッド

最も多く用いる形で、クラス内でコロン(:)によって関数を定義すると、インスタンス・メソッドになる。

JavaScriptへのコンパイル結果では、prototype宣言されている。

同じクラスのメソッド内で呼び出すときは、@つきで。

privateなクラス・メソッド

クラス内でfnc = ->のように定義すると、クラスの外からは見えないprivateなメソッドになる。

JavaScriptコードでは、クラス内のスコープで宣言されており、外からは見えない。

 

CoffeeScript – クラスのプロパティ

アクセス可能性

以下のコードで、クラスに関する変数へのアクセス可能性を確認する。

各変数へのアクセス可能性は以下の通り。

定義\引用 クラス インスタンス メソッド内
(@なし)
メソッド内
(@つき)
クラス直下 v=val undef undef undef
@v=val undef undef RefError undef
v:val undef RefError
@v:val undef RefError undef
コンストラクタ v=val undef undef RefError undef
@v=val undef RefError

この表から、以下のように整理できる。特にpublicなインスタンス・プロパティとstaticなクラス・プロパティを使っていくことになる。

クラス直下で@v:val定義 staticなクラス・プロパティ
クラス直下でv:val publicなインスタンス・プロパティ
 コンストラクタで@v=val publicなインスタンス・プロパティ
 クラス直下でv=val  privateなクラス・プロパティ

なお、定義部分のJavaScriptへのコンパイル結果は以下の通り。

クラス・プロパティ

クラス直下で@v:valでプロパティを定義する場合、プロパティはクラスに一つの共有のものとして扱われる。コンパイルされたJavaScriptコードでは正に”[クラス名].[プロパティ名]“となっている

インスタンスの中から参照する場合も、[クラス名].[プロパティ名]で参照する。

publicなインスタンス・プロパティ

クラス直下でv:valと定義した場合とコンストラクタで@v=valと定義した場合は、いずれもインスタンス・プロパティとして扱われる。

挙動としては同じなのでどちらでもよさそうだが、インスタンス・プロパティとしてはコンストラクタで定義する形式の方がよいと考えた。

以下のように、それぞれの定義方法でJavaScriptのコードが異なるが、メソッドのようにクラス内で共通化したい場合はprototypeがよいが、そもそもインスタンスごとに異なる値をとることを想定しているプロパティの場合は、thisによる定義の方が意味に即している。

privateなクラス・プロパティ

クラス直下でv=valと定義すると、そのクラスの各インスタンスで共有されたprivateなプロパティになる。

JavaScriptコードを見るとクラス宣言の最初でvar宣言されているため、クラス内スコープで外からは見えない変数となる。