Euphoriaのパフォーマンスについての小技


一般的な小技

  • プログラムの動作速度が十分高速なときは、それ以上の高速化をすることは諦めてください。その代わりに単純で可読性に優れたものにしましょう。

  • プログラムが非常に低速であるときは、恐らく後述の小技では問題は解決しないでしょう。この場合はプログラム全体のアルゴリズムを探したり再検討して、より良いものに変更する必要があります。

  • 僅かに動作速度を向上させる最も簡単な方法は実行時型検査を無効にすることです。次の行を挿入します:
               without type_check
    
    メインの.ex ファイルの先頭に、つまり全てのinclude命令文の前に記述してください。一般的に定義した型とインクルードされたファイルに依存しますが、動作速度は0から20パーセントほど向上します。多くの標準インクルードファイルではユーザ定義による型検査を使用しています。ユーザ定義による型検査を完全に無効にするとプログラムの動作速度が少し向上する場合があります。

    さらに、これら全ての命令文を、

               with trace
               with profile
               with profile_time
    
    必ず削除するか、コメントアウトしてください。with trace (trace()の呼び出しが一切無いとしても)、 および with profile では10%以上の動作速度の低下が軽く発生します。with profile_timeでは1%の動作速度低下が発生することがあります。さらに、各オプションはメモリーを別に消費します。

  • 整数値の計算は浮動小数点数の計算よりも高速です

  • 可能な限り変数はアトムではなく整数として宣言してください。また可能な限りシーケンスはオブジェクトとして宣言してください。これにより通常は数パーセント動作速度が向上します。

  • 浮動小数点数の計算を含む式では、浮動小数点数形式を定数値として記述すると通常より高速になります。すなわちxが浮動小数点数の値であり、x = 9.9であるとき、

    これを:
               x = x * 5
    
    こうします:
               x = x * 5.0
    
    これにより、いちいちインタプリタが整数5から浮動小数点数5.0へ変換する手間を省くことができます。

  • Euphoriaでは、if, elsifおよびandorを含むwhile条件文では短絡評価を行います。条件文が真か偽であるか決定したときEuphoriaは条件文の評価を終了します。実例としてif構文では:
            if x > 20 and y = 0 then
                ...
            end if
    
    "x > 20" が真のときだけ "y = 0"のテストを行います。

    実行速度を最大にするために、この順序でテストを行うことができます。この場合は"x > 20" が偽ならば"y = 0"も偽になります。

    一般的に条件文 "A and B" ではEuphoriaはAが偽(0)のときは式Bの評価を行いません。同様に、条件文 "A or B"では、 Aが真(非0)のときBは評価されません。

    単純なif構文は高度な最適化が行われます。現在のインタプリタのバージョンおいて通常は、入れ子になったifで整数を比較する場合は短絡評価のif構文より少しに高速です。すなわち:

           if x > 20 then
               if y = 0 then
                   ...
               end if
           end if
    

  • プライベート変数、ローカル変数およびグローバル変数のアクセス速度は同一です。

  • ハードコードしたリテラルの数を挿入する代わりに定数を定義しても動作速度は低下しません。速度では:
               y = x * MAX
    
    次と完全に同じです:
               y = x * 1000
    
    なお、このように以前に定義されていました:
               constant MAX = 1000
    
  • プログラムで大量のコメントを記述しても動作速度は低下しません。コメントは完全に無視されます。コメントは一切実行されません。ただしプログラム開始時に読み込み時間が数ミリ秒増える場合がありますが、将来の保守で支払う対価より非常に安価であり、さらにプログラムをバインドするかプログラムをC言語に翻訳する場合は、コメントは全て除去されるため、対価は絶対零度になります。


動作速度の計測

あらゆるプログラミング言語において、特にEuphoiraでは、動作速度に関しての結論を出す前に実際に計測を行う必要があります。

Euphoriaでは実行回数プロファイル時間プロファイル(DOS32専用)の両方が提供されています。詳しくはrefman.docを参照してください。これらのプロファイル結果に大抵は驚愕するでしょう。なお、プログラムで高い割合の合計時間を使用している箇所あるときは、その場所(または最も実行回数が多い箇所)に絞って改良を行ってください。合計時間の0.01%のみ使用するコード部分は書き直す箇所ではありません。通常はコードを一箇所、または数箇所に手を入れると意義深い差が生じことがあります。

time()関数を使用してコードの実行速度を計測することもできます。すなわち、

        atom t
        t = time()
        for i = 1 to 10000 do
            -- ここに小さなコード片が入ります。
        end for
        ? time() - t

どちらの方法が高速か見るためにコード片を別のものに書き直すことがあります。


ループの高速化方法

プロファイルによりプログラムの動作速度上の問題点を提示できます。これらは通常ループの内側にあります。ループ内側の計算を見て、ループを毎回実行する必要は本当にあるか、ループは以前にも一度実行されたか、あなた自身に問い合わせます。


乗算ループから加算ループへの変換

加算は乗算より高速です。場合によりループ変数を乗算から加算へ置き換えることができます。これは:

        for i = 0 to 199 do
            poke(screen_memory+i*320, 0)
        end for
こうなります:
        x = screen_memory
        for i = 0 to 199 do
            poke(x, 0)
            x = x + 320 
        end for

変数に対する結果の保存
  • 後で再計算するより、計算結果を変数に保存するほうが高速です。さらに何か単純な添字操作であっても変数に1つ追加することで値を保存します。

  • 多重に入れ子からなる添字を持つシーケンスでは、コードをこのように変更するとより高速になります:
               for i = 1 to 1000 do
                   y[a][i] = y[a][i]+1
               end for
    
    変更後:
               ya = y[a]
               for i = 1 to 1000 do
                   ya[i] = ya[i] + 1
               end for
               y[a] = ya
    
    添字の処理にループ反復を使うときは2ごとではなく4ごとに行ってください。ya = y[a] と y[a] = yaの操作は非常に安価です。これらはポインタをコピーするようなものです。これらはシーケンス全体をコピーしません。

  • 新しくシーケンスを作成するときに{a,b,c}を使用すると対価はわずかです。可能ならば、ループ前で変数を格納するのではなく変数の格納箇所を重要なループの外側に移動してからループの内側で変数を参照してください。


インラインルーチン呼び出し

非常に頻繁に呼び出されている高速な小規模ルーチンがあるとき、それはルーチンとして呼ぶのではなく、インライン展開((Inline expansion)を行って処理したほうが処理時間を節約できます。ただしコードの可読性が落ちることがあるため、ルーチンの呼び出しが頻繁に発生する箇所のみインライン展開にしたほうが良いでしょう(訳注:外部プリプロセッサと併用することで、この問題を解決できることがあります)。


シーケンスの操作

Euphoriaでは巨大なシーケンスのデータの操作は命令文を一度使用するだけで処理できます。これは1要素ごとに毎回ループを回して処理をする手間を省きます。すなわち、

        x = {1,3,5,7,9}
        y = {2,4,6,8,10}

        z = x + y
ループ版:
        z = repeat(0, 5)  -- 必要であれば。
        for i = 1 to 5 do
            z[i] = x[i] + y[i]
        end for

ほとんどのインタプリタ言語では、ループ内でスカラー演算を処理するより、シーケンス(配列)全体をひとつの命令文を処理するほうが非常に高速です。これはインタプリタでは命令文を実行するごとに膨大なオーバーヘッドが生じるからです。Euphoriaでは違います。Euphoriaは非常に洗練されており、インタプリタ処理のオーバーヘッドが少ないのですが、シーケンス操作では常に優位とは限りません。唯一の解決方法は両方の時間を計測することです。通常は1つの命令文だけでシーケンスを処理するとき要素ごと対価は低いですが、シーケンスの確保と開放にはオーバヘッドが関連しますが他のシーケンス操作手法によりオーバヘッド規模が変動することがあります。


いくつか特殊な場合における最適化

Euphoriaは特定の特別な場合に自動的に最適化を行います。変数xとyは後述の任意の式です。

        x + 1      -- 一般的に x + y より高速です。
        1 + x      -- 一般的に y + x より高速です。
        x * 2      -- 一般的に x * y より高速です。
        2 * x      -- 一般的に y * x より高速です。
        x / 2      -- 一般的に x / y より高速です。
        floor(x/y) -- x と y が整数ならば、than x/y より高速です。
        floor(x/2) -- floor(x/y) より高速です。

上述のxは単純に編集であり、yは任意の変数または式です:

        x = append(x, y)   -- 一般的に  z = append(x, y)  より高速です。
        x = prepend(x, y)  -- 一般的に  z = prepend(x, y) より高速です。

        x = x & y          -- xがyより大きいときは
                           -- 一般的に z = x & y より高速です。
シーケンスが"増大する"ループを記述したときデータを追加するか連結することにより、一般に処理時間は追加する要素の数の(N)の2乗に比例して増大します。 しかし、特殊な最適化形式としてappend(), prepend()またはを上述のリストにある連結演算子を使用できますが、実際の処理時間はN(おおよそ)に比例して増大します。これにより極端に長いシーケンスを作成するとき膨大な処理時間を節約して保存することができます(最大サイズのシーケンスを作成するためにrepeat()を使用することもでき、さらに要素を埋めつくすための方法は後述されています)。


代入操作

より高速に動作させるには、これを:

        左側 = 左側 演算子 演算式
このように変更します:
        左側 演算子= 演算式
常に左側には最低2つの添字、または最低1つの添字とスライスが含まれます。全て単純な形式であるときは2つ形式は同じ速度で動作します(または非常に似通っている)。


ピクセルグラフィックスの小技

  • モード19は非常に高速なモードであるためアニメーショングラフィックスおよびゲームに向いています。

  • ビデオメモリはCPUにはキャッシュされません(モード19では)。通常は一般的なメモリーの確保をするよりスクリーンにデータを読み書きするほうが処理時間がかかります。これはv仮想スクリーンの効率を上げるためには、全てのイメージを更新するためにallocate()メモリーブロックを取得してから、定期的にmem_copy()で実スクリーンメモリーにイメージを反映させます。必ずしも(低速な)スクリーンメモリーを読み取る必要はありません。

  • ピクセルを打つとき、モード257であるならばスクリーン上部付近の描画速度は高速ですが下部付近の描画速度は遅いです。


テキストモードの小技

テキストをスクリーンに書き込むときに、puts()またはprintf()を使用すると描画速度は遅いです。DOS32では必要に応じて、ビデオメモリーに書き込むか、display_text_image()を使用すると高速に描画できます。puts()でスクリーンへの出力は非常にオーバヘッドがありますが、出力文字あたりでは比較的小さな対価が加算されます。exw (WIN32)ではオーバーヘッドは非常に高いです(Windows 95/98/ME以降では)。 LinuxおよびFreeBSDではDOS32とWIN32のテキスト出力速度の中間くらいです。したがってput()を文字ごとに呼び出すではなく、長い文字列を構成して呼び出したほうが意味があります。しかし欠点として1行より長い文字列を構成する方法がありません。

テキスト出力が遅くなる主な原因はオペレーティングシステム側のオーバヘッドです。


ライブラリルーチン

一般的なルーチンの数種類は非常に高速です。恐らくC言語またはアセンブリ言語を使用したり他の全ての方法を使用してたとしても、これより処理を高速にできないでしょう。これらは次の通りです:

  • mem_copy()
  • mem_set()
  • repeat()

他のルーチンも非常に高速ですが速度が重要なときは場合により、さら処理を高速にできることがあります。

        x = repeat(0,100)
        for i = 1 to 100 do
            x[i] = i
        end for
は次のものよりも多少高速です:
        x = {}
        for i = 1 to 100 do
            x = append(x, i)
        end for
append()はxの大きさが増えるたびに領域の確保と再確保を行います。repeat()は最初に一度だけ領域を確保します(不親切なことにappend()はxになにか追加するたびに十分な領域を確保しません。これはもっと領域が必要になったときに多少確保され、再確保の回数により縮小されます)。

これは:

        remainder(x, p)
こう書き換えられます:
        and_bits(x, p-1)
これによりpは整数の2の累乗が非常に高速に行えます。必ずxは非負数の32bit整数値である必要があります。

arctan()arccos()またはarcsin()より高速です。


検索

Euphoriafind() は50要素までシーケンスの値の検索方法としては非常に高速です。その他に、ハッシュテーブル (demo\hash.ex) または 二分木 (demo\tree.ex)の使用を検討できることがあります。


ソート

ほとんどの場合において、include\sort.eにあるシェルソートを使用できます。

大量のデータをソートするときは、demo\allsorts.eにあるソートのうち一つを試したほうが良いことがあります(すなわち、 素晴らしいソート)。データがメモリーに収めるには大きすぎるときであっても、Euphoriaの自動メモリースワップ機能に依存しないでください。その代わりに一度に2000から3000レコードをソートして、連続でテンポラリファイルに書き出してください。そして全てソートが終わった後にテンポラリファイルを1の巨大なソート済みファイルとして統合してください。

なお、データが整数値でのみ構成されており全て非常に狭い範囲にあるならば、demo\allsorts.eにあるバケツソート を試してみてください。


キャッシュメモリーの活用

CPUの速度の高速化とともに、オンチップ・キャッシュ・メモリーとメイン・メモリーまたはDRAM(ダイナミック・ランダム・アクセス・メモリー)間の速度の差は非常に大きくなります。コンピュータには256MB以上のメモリーを搭載しているかもしれませんが、Pentiumでは8K(データ) + 8KB(インストラクション)のキャッシュか、MMX PentiumまたはPentium II/IIIでは、これに + 16KB(インストラクション)の一次キャッシュがあるだけです。ほとんどのマシンでは256KBまたは512MB以上の"二次"キャッシュを備えています(訳注:古い記述ですので必要であれば調べてください)。

初めから終わりまで、アルゴリズムは2000から3000以上の要素を持つ長いシーケンスの中をひとつずつ通過しながら、何度も要素ごとに小さな操作を実行しますが、オンチップデータキャッシュは効率よく使用されません。次の要素へ移動する前に、いくつかの処理が要素ごとに適用されますが、一気に通過したほうが良いことがあります。これと同じ引数を持つプログラムではデータが巨大なためスワップが開始されてしまい、最も最後に使用されたデータはディスクへスワップアウトされます。

これらのキャッシュの効果はEuphoriaでは低レベルコンパイル言語とは違い顕著な差は出ませんが、これらは測定可能です。


マシン語とC言語コードの使用

Euphoriaから32bit x86機械語で記述されたルーチンを呼び出すことができます。WIN32LinuxおよびFreeBSDでは.dll または .soファイルにあるC言語ルーチンを呼び出すことができ、C言語ルーチンからEuphoriaルーチンを呼び出すことができます。Euphoriaでは直に処理できない何かをするために、または動作速度を改善するためにC言語コードまたは機械語を呼び出すことがあります。

動作速度を向上するには、機械語またはC言語ルーチンが呼び出されるごとに膨大な作業量を処理する必要があり、そうしなければ引数の準備や呼び出しで処理時間が独占されてオーバヘッドが発生してしまい、あまり動作速度が向上しないことがあります。

多くのプログラムではCPU時間のほとんどを複数の内部コアの処理で消費します。この部分のコードをC言語または機械語にできるならば、Euphoiraプログラムの大半を残しながら、Euphoriaの安全性と柔軟性を犠牲にすることなくC言語に匹敵する動作速度を達成できることがあります。

 
Euphoria → C言語トランスレータの使用

version 3.0から、完全なEuphoria → C言語トランスレータがインストールパッケージに含まれるようになりました。 これは任意のEuphoriaプログラムをC言語ソースファイルの一式へと翻訳してC言語コンパイラでコンパイルすることができるようになります。

トランスレータにより生成された実行可能ファイルはインタプリタと同様に実行できますがインタプリタを使用したときより高速です。動作速度は0.3倍から5倍数以上の高速化されます。