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

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

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

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

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

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

 

Parents
  • 基本 float で計算するプログラムの様ですが、ceil() や sqrt() の double の関数、0.01 や 1.0 、10.9 の double の値が使用されており、float と double で精度が異なる環境では実行パフォーマンスに影響がある気がします。
    RX65N + CC-RX で double をソフトで計算する設定とかだと影響も小さくないのでは。

  • 自己レス

    > 基本 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

Reply
  • 自己レス

    > 基本 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

Children
  • fujita nozomuさん
    IKUZOです、すばらしいレポートですね、やっぱりプロは自分とは違うなーという感じですね、非常に興味を持ちました、自分の作成したプログラムも調整すると別物になるかもですね。
  • >10倍ちょっとの高速化が果たせました
    凄いですね!
    凄いです。
    CPUを知り尽くし、コンパイラを知り尽くしてるfujitaさんだからこそと思います。

    >今回はこの辺りで止めておきますが
    いやもう十分です。
    素晴らしい成果です。
  • 自己レス

    > 更なる高速化を目指すには raytracer.hpp の中身に手を入れる必要が出てくると思います。

    raytracer.hpp の中で単位ベクトルの計算をしている箇所があり、

      vec3 operator!()              const { return *this*(1.0f/sqrtf(*this%*this));  }  // Normalized vector
    

    1.0f/sqrtf() で平方根の逆数を求めていますが、高速に平方根の逆数を求めるアルゴリズムが知られておりこれを適用することで高速化ができそうです。1.0f/sqrtf() を RX64M 用に効率的なコードに翻訳したとすると平方根の計算に 16サイクル、直値 1.0f のロードに 1サイクル、除算に 16サイクルの合計 33サイクルを要することとなりますが、先にリンクした記事のコードを g++ でコンパイルすると

    $ rx-elf-g++ -O3 -mcpu=rx64m -c Q_rsqrt.cpp ; rx-elf-objdump -d Q_rsqrt.o
    
    Q_rsqrt.o:     file format elf32-rx-le
    
    
    Disassembly of section P:
    
    00000000 <__Z7Q_rsqrtf>:
       0:   fd 81 15                        shlr    #1, r1, r5
       3:   fb 42 df 59 37 5f               mov.l   #0x5f3759df, r4
       9:   ff 05 54                        sub     r5, r4, r5
       c:   fd 72 31 00 00 00 3f            fmul    #0x3f000000, r1
      13:   fc 8f 51                        fmul    r5, r1
      16:   fc 8f 51                        fmul    r5, r1
      19:   fb 42 00 00 c0 3f               mov.l   #0x3fc00000, r4
      1f:   ff 81 14                        fsub    r1, r4, r1
      22:   fc 8f 51                        fmul    r5, r1
      25:   02                              rts
    
    $
    

    インライン展開することを前提として rts を除き合計 14サイクルで平方根の逆数が計算できることとなります。

    raytracer.hpp の該当箇所より前の方に先にリンクした記事のコードをほゞそのまゝ

    static inline float Q_rsqrt( float number ) __attribute__((always_inline));
    static inline float Q_rsqrt( float number )
    {
    	union {
    		float f;
    		uint32_t i;
    	} conv;
    	
    	float x2;
    	const float threehalfs = 1.5F;
    
    	x2 = number * 0.5F;
    	conv.f  = number;
    	conv.i  = 0x5f3759df - ( conv.i >> 1 );
    	conv.f  = conv.f * ( threehalfs - ( x2 * conv.f * conv.f ) );
    	return conv.f;
    }
    

    コピペして追加し、該当箇所を

      vec3 operator!()              const { return *this*(Q_rsqrt(*this%*this));  }  // Normalized vector
    

    と変更するだけで対応できます。

    これで計算時間のみを計測すると

    → 1040(m秒)

    変更前の 1.385m秒に対しておよそ 25% の計算時間の短縮ができました。

    出力を変更前のものと比較すると演算の誤差による違いもあるのですが

    表示をしても気になるものではないようです。

  • > 出力を変更前のものと比較すると演算の誤差による違いもあるのですが表示をしても気になるものではないようです。

    …………そんなふうに考えていた時期が俺にもありました

    (↓の画像をクリックしてみてね)

    重ねてみたら誤差ありすぎワロタwww

    Q_rsqrt() の conv.f = conv.f * ( threehalfs - ( x2 * conv.f * conv.f ) ); は 2回反復して誤差を減らすのが良いようです。

    static inline float Q_rsqrt( float number ) __attribute__((always_inline));
    static inline float Q_rsqrt( float number )
    {
    	union {
    		float f;
    		uint32_t i;
    	} conv;
    	
    	float x2;
    	const float threehalfs = 1.5F;
    
    	x2 = number * 0.5F;
    	conv.f  = number;
    	conv.i  = 0x5f3759df - ( conv.i >> 1 );
    	conv.f  = conv.f * ( threehalfs - ( x2 * conv.f * conv.f ) );
    	conv.f  = conv.f * ( threehalfs - ( x2 * conv.f * conv.f ) );
    	return conv.f;
    }
    

    これで重ねてもさほど気にならない程度まで演算精度を高めることができるようです。

    但し計算時間が

    →1102(m秒)

    に落ちてしまいます(これは悔しい)。