RX65N Envision Kit でレイ・トレーサーを走らせてみました。

M5Stack や、ESP32 では、定番のようなので、ソースを取得、元々 Arduino 用なので、改造して動かしてみました。

ESP32(160MHz) では13秒らしいですが、120MHzのRX65では7.7秒なので、かなり高速ですね。

※速度比較の為320×240でレンダリングしてます。

ソースコードは以下に・・

https://github.com/hirakuni45/RX/tree/master/RTK5_RAYTRACER

 

  • > ESP32(160MHz) では13秒らしいですが、120MHzのRX65では7.7秒なので、かなり高速ですね。

    ESP32はデュアルコアですがそれが活かされれば逆転もありそうです。
  • RX65 のハードウェアーマニュアルを眺めていたら、ROM キャッシュ制御がある事に気が付き、それを「有効」にしたら、6.8秒と、約10%程度速度改善する事が判りました。
    今までそれを無効にして頑張っていた事は少し痛いですww

    ※ESP32 はデュアルコアですが、それを生かしたプログラムにするには、排他制御や、リソースの管理、スレッド制御など、実装方法をかなり改修しないと出来ないので、このソースコードの場合「机上の空論」でしょうね・・
    ※単純に、偶数ラインと奇数ラインでコアを分ければ良さそうですが、それでもソースの修正は必要です。
  • 基本 float で計算するプログラムの様ですが、ceil() や sqrt() の double の関数、0.01 や 1.0 、10.9 の double の値が使用されており、float と double で精度が異なる環境では実行パフォーマンスに影響がある気がします。
    RX65N + CC-RX で double をソフトで計算する設定とかだと影響も小さくないのでは。

  • > ※ESP32 はデュアルコアですが、それを生かしたプログラムにするには、排他制御や、リソースの管理、スレッド制御など、実装方法をかなり改修しないと出来ないので、

    大した作業ではないでしょう。
  • それなら、実際にコードを実装して、時間計測をお願いしますw
    ※変更箇所は、オリジナルのソースコード開発者に送ってあげて下さい。
  • ルネサス運営のサービスで他社製品の話もどうかと思っていますが、お願いしますということですが機材は提供していただけますか?
  • makefile を拝見したところ g++ を使用して RXv1 の命令を吐かせているようですが、折角の RX65N なのだから RXv2 命令に正しく対応してる筈(よく知らん)の CC-RX を使うか、あるいは g++ を工夫して使用して 3オペランド命令や FSQRT 命令が活用されればいくらかのパフォーマンス向上は期待できるのでは。

    3オペランド命令が使えない RXv1 と 使える RXv2 の極端な比較

    $ cat -n 3ope.cpp
         1  float mul(float, float a, float b)
         2  {
         3      return a * b;
         4  }
         5
         6  float add(float, float a, float b)
         7  {
         8      return a + b;
         9  }
        10
        11  float sub(float, float a, float b)
        12  {
        13      return a - b;
        14  }
    
    $ rx-elf-g++ -Wall -Wextra -O2 -c 3ope.cpp && rx-elf-objdump -d 3ope.o
    
    3ope.o:     file format elf32-rx-le
    
    
    Disassembly of section P:
    
    00000000 <__Z3mulfff>:
       0:   ef 21                           mov.l   r2, r1
       2:   fc 8f 31                        fmul    r3, r1
       5:   02                              rts
    
    00000006 <__Z3addfff>:
       6:   ef 21                           mov.l   r2, r1
       8:   fc 8b 31                        fadd    r3, r1
       b:   02                              rts
    
    0000000c <__Z3subfff>:
       c:   ef 21                           mov.l   r2, r1
       e:   fc 83 31                        fsub    r3, r1
      11:   02                              rts
    
    $ rx-elf-g++ -mcpu=rx64m -Wall -Wextra -O2 -c 3ope.cpp && rx-elf-objdump -d 3op
    e.o
    
    3ope.o:     file format elf32-rx-le
    
    
    Disassembly of section P:
    
    00000000 <__Z3mulfff>:
       0:   ff b1 23                        fmul    r2, r3, r1
       3:   02                              rts
    
    00000004 <__Z3addfff>:
       4:   ff a1 23                        fadd    r2, r3, r1
       7:   02                              rts
    
    00000008 <__Z3subfff>:
       8:   ff 81 32                        fsub    r3, r2, r1
       b:   02                              rts
    
    $
    

    FSQRT 命令が使えない RXv1 と 使える RXv2 の極端な比較

    $ cat -n fsqrt.cpp
         1  #include <cmath>
         2
         3  #if defined(__RXv2__)
         4  #define sqrtf(_x) \
         5  ({ \
         6      float _y; \
         7      __asm __volatile( \
         8          "fsqrt %1, %0\n" \
         9          : "=r"(_y) \
        10          : "g"(float(_x)) \
        11      ); \
        12      _y; \
        13  })
        14  #endif
        15
        16  float hoge(const float a[3])
        17  {
        18      return sqrtf(a[0]) + sqrtf(a[1]) + sqrtf(a[2]);
        19  }
    
    $ rx-elf-g++ -Wall -Wextra -O2 -c fsqrt.cpp && rx-elf-objdump -d fsqrt.o
    
    fsqrt.o:     file format elf32-rx-le
    
    
    Disassembly of section P:
    
    00000000 <__Z4hogePKf>:
       0:   6e 6a                           pushm   r6-r10
       2:   ef 17                           mov.l   r1, r7
       4:   ec 11                           mov.l   [r1], r1
       6:   05 00 00 00                     bsr.a   6 <__Z4hogePKf+0x6>
       a:   ef 1a                           mov.l   r1, r10
       c:   a8 79                           mov.l   4[r7], r1
       e:   05 00 00 00                     bsr.a   e <__Z4hogePKf+0xe>
      12:   ef 16                           mov.l   r1, r6
      14:   a8 f1                           mov.l   8[r7], r1
      16:   05 00 00 00                     bsr.a   16 <__Z4hogePKf+0x16>
      1a:   ef a7                           mov.l   r10, r7
      1c:   fc 8b 67                        fadd    r6, r7
      1f:   fc 8b 71                        fadd    r7, r1
      22:   3f 6a 05                        rtsd    #20, r6-r10
    
    $ rx-elf-g++ -Wall -Wextra -O2 -mcpu=rx64m -c fsqrt.cpp && rx-elf-objdump -d fs
    qrt.o
    
    fsqrt.o:     file format elf32-rx-le
    
    
    Disassembly of section P:
    
    00000000 <__Z4hogePKf>:
       0:   fc a0 13                        fsqrt   [r1].l, r3
       3:   fc a1 14 01                     fsqrt   4[r1].l, r4
       7:   fc a1 15 02                     fsqrt   8[r1].l, r5
       b:   ff a1 34                        fadd    r3, r4, r1
       e:   fc 8b 51                        fadd    r5, r1
      11:   02                              rts
    
    $
    
  • 自己レス
    FSQRT のマクロ、raytracer.hpp に使うとコンパイラのバグ? に当たるのか異常なパラメタが渡されてエラーになるので確認中。
  • 自己レス

    > 基本 float で計算するプログラムの様ですが、ceil() や sqrt() の double の関数、0.01 や 1.0 、10.9 の double の値が使用されており、float と double で精度が異なる環境では実行パフォーマンスに影響がある気がします。

    RX65N と同一の RXv2 コア採用の RX64M@96MHz を搭載している GR-KAEDE で動作させて検証してみました。液晶は適当なのが手持ちでなかったのと面倒だったので表示はしていません。計算のみです。

    web コンパイラ ではコンパイルオプションを弄ったりできないので、web コンパイラで空のプロジェクトを作成してビルドを行いそれをダウンロードし、ローカルで makefile を弄ってローカルでビルド等しています。

    レイトレーサーのプログラム本体 raytracer.hpp が C++11 でないとビルドできないようなので makefile のコンパイルオプションに -std=c++11 を追加、toolchain も https://gcc-renesas.com/ で配布している最新の GCC for Renesas 4.8.4.201801-GNURX Windows Toolchain (ELF) に変更して下記のスケッチをビルドして実行します。

    /* GR-KAEDE Sketch Template V1.22 */
    
    #define OutputPPM 1
    
    // -std=c++11 を指定すると WCharacter.h でエラーになることへの対策
    int isascii(int);
    int toascii(int);
    
    #include <Arduino.h>
    
    extern "C" void draw_pixel(int, int, int, int, int) __attribute__((noinline));
    
    #include "raytracer.hpp"
    
    void setup()
    {
        const int w = 320, h = 240;
    
        Serial1.begin(230400);
        Serial1.println("start");
    #if OutputPPM
        Serial1.println("P3");
        Serial1.print(w);
        Serial1.print(" ");
        Serial1.println(h);
        Serial1.println("255");
    #endif
        uint32_t start = millis();
        doRaytrace(1, w, h);
        Serial1.println(millis() - start);
    }
    
    void loop()
    {
    }
    
    void draw_pixel(int x, int y, int r, int g, int b)
    {
        (void)x;
        (void)y;
    #if OutputPPM
        Serial1.println(r);
        Serial1.println(g);
        Serial1.println(b);
    #else
        (void)r;
        (void)g;
        (void)b;
    #endif
    }
    

    Serial1 に PPM 形式 で画像イメージが出力されるので、GIMP2 で読み込んでみると

      

    それらしい画像が表示されたので計算はできているようです。

    スケッチ中の OutputPPM の値を 0 に変更し PPM の出力を止め計算時間だけを計測してみます。

    → 30425(m秒)

    かなり遅いですね。

    GR-KAEDE は web コンパイラでのコンパイルで RXv2 命令を有効化する -mcpu=rx64m が指定されているのは良いのですが最適化指示が設定されておらず効率の悪いコードを出力します。ちょっとこれではどうしようもないので makefile を弄って CFLAGS に -O3 を追加し、再度計算時間を計測してみます。

    → 15092(m秒)

     先のと較べ半分以下の計算時間となりました。がまだ遅いですね。

    GR-KAEDE は web コンパイラでのコンパイルでは double の演算を 64bit で行う設定となっており、GR-KAEDE に搭載されている RX64M は 64bit の浮動小数点演算を機能として持っておらずソフトウェアでの実装となっているため double の演算は float のそれに比べ大変重いものとなっています。今回のレイトレーサーのプログラムは float も double も 32bit である Arduino で動作させるものだった(?)為か一部 double の直値が使用されていたり sqrt() や ceil() の double の数学関数が使用されており、GR-KAEDE の仕様に引っかかりそうです。raytracer.hpp の中の double の直値を float に修正、同様に sqrt() → sqrtf()、ceil() → ceilf() の修正を行い再度ビルドして計算時間を計測してみます。

    → 3990(m秒)

    かなり改善しました。が折角なのでもう少し攻めてみましょう。

    RX64M の RXv2 コアは浮動小数点演算命令の中に平方根を計算する FSQRT 命令というのが追加されています。今回のレイトレーサーのプログラムは平方根の関数を複数個所で呼んでいるのでこれを FSQRT 命令に置き換えれば幾らかの計算時間の短縮が期待できそうです。

    static inline float fsqrt(float x) __attribute__((always_inline));
    static inline float fsqrt(float x)
    {
        __asm __volatile(
            "fsqrt %0, %0\n" \
            : "+r"(x) \
        );
        return x;
    }
    #define sqrtf(x) fsqrt(x)
    

    スケッチの raytracer.hpp をインクルードしている箇所の上に上記のコードを挿入すると単精度の平方根関数である sqrtf() を呼んでいる部分で FSQRT 命令が使用されます。これをビルドし実行すると

    → 1416(m秒)

    大きな効果がありました。良い感じです。

    他、前述の通り今回のレイトレーサーのプログラムは数学関数として sqrt() の他に「引数の値より大きい最小の整数の値」を返す ceil() が使用されており、これをなんとかすることでレイトレーサーの計算部分で外部関数の使用を完全になくすことができます。

    static inline int roundUp(float x) __attribute__((always_inline));
    static inline int roundUp(float x)
    {
        int y;
        __asm __volatile(
            "pushc fpsw\n"
            "mvtc #0b10, fpsw\n"
            "round %0, %0\n"
            "popc fpsw\n"
            : "=r"(y) \
            : "r"(x) \
        );
        return y;
    }
    #define ceilf(x) roundUp(x)
    

    スケッチの raytracer.hpp をインクルードしている箇所の上に上記のコードを挿入することで ceilf() を置き換えます。これをビルドし実行すると

    → 1385(m秒)

    先の sqrtf() の置き換えと比べ効果は僅かな結果となりました。

    今回はこの辺りで止めておきますが同じボードで更なる高速化を目指すには raytracer.hpp の中身に手を入れる必要が出てくると思います。

    今回の実験で、同一の toolchain、同一のコンパイルオプションの条件で、僅かなコードの修正で 15092(m秒) → 1385(m秒) と 10倍ちょっとの高速化が果たせました。今回使用した GR-KAEDE 以外でも、マイコンの性能がどれだけ引き出せるかは

    • ツールの特性
    • マイコンの機能
    • プログラムの内容

    以上の 3点の理解が大きいことは共通してるのではないかと思います。異なる製品の性能比較を公平に行うには両製品への深い理解が要求され、易しい仕事ではないのかもしれません。

    gr-kaede.raytracer.zip

  • fujita nozomuさん
    IKUZOです、すばらしいレポートですね、やっぱりプロは自分とは違うなーという感じですね、非常に興味を持ちました、自分の作成したプログラムも調整すると別物になるかもですね。