Skip to content

ベンチマークノート

本資料では、実際のターゲットデバイスに最も適した結果を得るために、パフォーマンスやメモリ消費量のベンチマークを行う際の注意点について説明します。

メモリベンチマーク

Duktapeのローメモリオプションを有効にする

Duktapeの低メモリ設定オプションを有効にしてベンチマークを行う場合、ターゲットがこれらのオプションを有効にして実際に実行される場合、有効にしてください。例えば、ターゲットが128-256kBのシステムRAMを持っている場合、ローメモリオプションは非常に推奨されるでしょう:

  • doc/low-memory.rst
  • config/examples/low_memory.yaml

DUK_USE_GC_TORTURE を有効にして、実際のハードメモリ使用量を確認する

テスト用にDUK_USE_GC_TORTUREを有効にして、実際のメモリ、つまり緊急ガベージコレクションが行われるときに収集されない、実際に到達可能なオブジェクトを測定する。この設定オプションは、実際に到達可能なメモリだけがいつでも使用されたままになるように、すべての割り当てに対して完全なマークアンドスイープ・ガベージコレクション・パスを引き起こします。

このオプションを有効にしていない状態で見た見かけのメモリ使用量は、実際に必要な量とかなり異なる場合があります。この差は通常関係ありません。もしメモリが不足した場合、緊急ガベージコレクションが到達不可能なオブジェクトを解放することができます。

この違いは、システム内の他のコンポーネントがメモリ不足に陥った場合、通常、メモリを解放する緊急ガベージコレクションを起動できないため、実際に問題となることがあります。しかし、Duktapeにプール・アロケータを使用する場合、これは問題ではありません:すべてのDuktapeの割り当ては、事前に割り当てられたプールに含まれます。

ターゲットがプールアロケーターを使用している場合は、プールアロケーターを使用して使用量を測定します。

valgrind --tool=massif` による測定は比較的正確ですが(GC torture が有効な場合)、プールアロケータを使用した場合には存在しない割り当てオーバーヘッドが含まれます。プールアロケータは、オーバーヘッドとヒープの断片化を減らすために、低メモリターゲットに推奨されます。

実際のターゲットがプールアロケータを使用する場合、ベンチマークはそのアロケータに対して行われ、プールエントリサイズは実際に実行されるアプリケーションコードに最適化されている必要があります。valgrind massifが報告した使用量と実際のプールアロケータの使用量の差は、かなり大きくなることがあります。しかし、プール構成の最適化が不十分な場合、無駄なプールエントリバイトによるメモリ割り当てのオーバーヘッドも大きくなる可能性があります。

プロセスRSSなどを使った測定は非常に不正確で、達成可能な実際のメモリ使用量を正確に反映しないため、可能であれば避けるべきです。プールアロケータなしで測定する場合、GC拷問を有効にすることと組み合わせたvalgrind massifは、はるかに良いオプションです。

DUK_USE_GC_TORTUREの測定影響例

コールバック指向のコードにありがちな、無名関数インスタンスを大量に生成するプログラムを例にしてみましょう:

js
function test() {
    for (var i = 0; i < 10000; i++) {
        var ignored = function () {};
    }
}
test();

このような無名関数は、それぞれデフォルトの .prototype オブジェクト(.constructor参照を使用して関数を指す)との参照ループにあるため、関数は参照カウントでは収集されず、マークアンドスイープで解放されます。マークアンドスイープは定期的に実行されますが、割り当てに失敗した場合は緊急マークアンドスイープが発動されます。

Duktapeをx64でデフォルトのままコンパイルすると(ローメモリオプションやROMビルトインなどは一切なし)、valgrind massifで以下のメモリ使用量が表示されます:

...
    KB
539.2^                                                                      :
     |         #       :      ::      :              :      :@       :      :
     |         #       :      :      ::      :      ::      :@      @:     ::
     |         #      ::      :      ::      :      ::      :@     :@:    @::
     |        @#      ::     ::      ::      :      ::     ::@     :@:    @::
     |        @#     :::     ::     :::    :::     :::     ::@    ::@:    @::
     |        @#     :::    :::     :::    : :     :::    :::@    ::@:   :@::
     |       @@#    @:::    :::    ::::    : :     :::    :::@    ::@:   :@::
     |       @@#    @:::   @:::    ::::   :: :    ::::    :::@   :::@:  ::@::
     |      @@@#   :@:::   @:::   :::::   :: :    ::::   ::::@  ::::@:  ::@::
     |      @@@#   :@:::  :@:::   :::::   :: :    ::::  :::::@  ::::@:  ::@::
     |     @@@@#   :@:::  :@:::  ::::::  ::: :   :::::  :::::@  ::::@: :::@::
     |  @  @@@@#  ::@:::  :@:::  ::::::  ::: :   :::::  :::::@ :::::@: :::@::
     |  @  @@@@#  ::@:::  :@:::  :::::: :::: :  :::::: ::::::@ :::::@: :::@::
     |  @::@@@@#  ::@::: ::@:::  :::::: :::: :  :::::: ::::::@::::::@:::::@::
     | @@: @@@@#  ::@::::::@::: ::::::: :::: :  :::::: @:::::@::::::@:::::@:::
     | @@: @@@@#::::@::::::@::: :::::::::::: ::::::::::@:::::@::::::@:::::@:::
     | @@: @@@@#: ::@::::::@::: :::::::::::: :: :::::::@:::::@::::::@:::::@:::
     | @@: @@@@#: ::@::::::@::: :::::::::::: :: :::::::@:::::@::::::@:::::@:::
     | @@: @@@@#: ::@::::::@::: :::::::::::: :: :::::::@:::::@::::::@:::::@:::
   0 +----------------------------------------------------------------------->Mi
     0                                                                   138.9

このことから、このプログラムは540kBのメモリを使用しているように見えます。これは非常に誤解を招きやすいのですが、実はこの使用量のほぼ全てがmark-and-sweepによって定期的に収集される収集可能なゴミなのです(上記では "spiking "と表現しています)。特に、メモリが不足した場合(具体的には、メモリの割り当てに失敗した場合)、緊急のmark-and-sweepパスにより、そのメモリは解放され、他の用途に使用できるようになる。

DUK_USE_GC_TORTUREを有効にすると、全く異なる結果が得られます:

...
    KB
118.2^#
     |#
     |#:@@:@::::::::    :     :::             @::  :: : : : : ::
     |#:@ :@:: : : ::::::::::::::::::::@@:::::@: ::: :::::::::: :::@:::::::@::
     |#:@ :@:: : : :: : ::::: :::::::::@ : :: @: : : :::::::::: :::@:::::::@::
     |#:@ :@:: : : :: : ::::: :::::::::@ : :: @: : : :::::::::: :::@:::::::@::
     |#:@ :@:: : : :: : ::::: :::::::::@ : :: @: : : :::::::::: :::@:::::::@::
     |#:@ :@:: : : :: : ::::: :::::::::@ : :: @: : : :::::::::: :::@:::::::@::
     |#:@ :@:: : : :: : ::::: :::::::::@ : :: @: : : :::::::::: :::@:::::::@::
     |#:@ :@:: : : :: : ::::: :::::::::@ : :: @: : : :::::::::: :::@:::::::@::
     |#:@ :@:: : : :: : ::::: :::::::::@ : :: @: : : :::::::::: :::@:::::::@::
     |#:@ :@:: : : :: : ::::: :::::::::@ : :: @: : : :::::::::: :::@:::::::@::
     |#:@ :@:: : : :: : ::::: :::::::::@ : :: @: : : :::::::::: :::@:::::::@::
     |#:@ :@:: : : :: : ::::: :::::::::@ : :: @: : : :::::::::: :::@:::::::@::
     |#:@ :@:: : : :: : ::::: :::::::::@ : :: @: : : :::::::::: :::@:::::::@::
     |#:@ :@:: : : :: : ::::: :::::::::@ : :: @: : : :::::::::: :::@:::::::@::
     |#:@ :@:: : : :: : ::::: :::::::::@ : :: @: : : :::::::::: :::@:::::::@::
     |#:@ :@:: : : :: : ::::: :::::::::@ : :: @: : : :::::::::: :::@:::::::@::
     |#:@ :@:: : : :: : ::::: :::::::::@ : :: @: : : :::::::::: :::@:::::::@::
     |#:@ :@:: : : :: : ::::: :::::::::@ : :: @: : : :::::::::: :::@:::::::@::
   0 +----------------------------------------------------------------------->Gi
     0                                                                   20.29

実際のメモリ使用量は120kBで、valgrindで見た見かけのメモリ使用量の約22%に過ぎません。このハードメモリ使用量は、アプリケーションがより多くのメモリを割り当てられるかどうかを決定する、本当に重要なものです。

パフォーマンスベンチマーキング

Duktapeのパフォーマンスオプションを有効にする

メモリに制約のあるデバイスで実行していて、コードのフットプリントなどよりもパフォーマンスを優先するのでなければ、Duktapeのパフォーマンス・オプションを有効にする必要があります。詳細については、以下を参照してください:

  • doc/performance-sensitive.rst
  • config/examples/performance_sensitive.yaml

メモリと同様に、実際のターゲットに関連するオプションで測定することが重要である。ほとんどのローメモリオプションとパフォーマンスオプションを同時に有効にすることが可能です(RAMが比較的少なく、コードのROMフットプリントが問題でない場合は理にかなっています)。Duktape のローメモリ・オプションは性能に影響を与えるかもしれません。特に、ヒープ・ポインタ圧縮は比較的大きな性能影響を与えるので、最終的なターゲットがヒープ・ポインタ圧縮を使用するかどうかによって、考慮することが重要です。

デフォルトでファンクションコードを使用するテスト

グローバルコード(プログラムコード)とevalコードは、関数コード(function () { ... }式の中に存在するステートメント)とは意味的に重要な違いがあります.式内に存在するステートメントです。Duktapeでは、この2種類のコンパイルされたコードの性能差は非常に大きいです。具体的な違いは、globalコードとevalコードにはローカル変数が存在せず、すべての変数アクセスは内部スローパスを経由し、実際にはグローバル・オブジェクトのプロパティの読み取りと書き込みであるということです。

具体例として、関数内の空ループ:

sh
$ cat test.js
function test() {
    for (var i = 0; i < 1e7; i++) {
    }
}
test();

$ time ./duk.O2.140 test.js
real   0m0.256s
user   0m0.256s
sys    0m0.000s

関数外での空ループ:

sh
$ cat test.js
// Note that 'i' is actually a property of the global object.
for (var i = 0; i < 1e7; i++) {
}

$ time ./duk.O2.140 _test.js
real   0m4.325s
user   0m4.319s
sys    0m0.004s

グローバルコード内のループは、関数内よりも20倍遅く実行されます。実用的なコードの性能差は、変数アクセスが何回行われるかに依存します。

ほとんどのプログラムでは、実際にパフォーマンスに関係するコードの大部分は関数の中にあります。特に、CommonJSのモジュールはすべて自動的に匿名ラッパー関数の中に入っているので、すべてのモジュールコードは高速パスを使って実行されます。ベンチマークを行う場合、ターゲット上で実際にコードを実行するのと同じように、パフォーマンス上重要なコードを関数の中に入れて測定するのが最適なデフォルトとなります。

しかし、ターゲットが実際にグローバルまたはevalコンテキストでパフォーマンスに関連するコードを実行する場合(特定のアプリケーションではかなり可能性があります)、もちろん、関数の外でそのコードを測定することが賢明です。