2009/01/29(木)[NiosII][ACM] 高速化・ベンチマーク測定

@IT MONOistで新記事があがっとります.FPGAですが,Xilinxで,且つlinux環境下という個人的に役立ちそうな情報です.\HDLシミュレータの紹介・インスコ方法から記載されていますので,良い資料になるでしょう.

-ザ・組み込み-ソフトウェアのハードウェア化(6)\追い出したソフトウェアを“ハードウェア化”する準備\鳥海 佳孝 設計アナリスト 2009/1/29

こうやって入門記事はそれなりに出てくるものなので,あえて競合する必要は無いなぁと思う次第であります.\そこで我々中途半端層の使命は,本能の赴くままに遊んで資料を残していくことだけ.見返りを求めて手を動かしていてはいけないと悟りました(キリッ\まぁ突き詰めて考えれば,誰かに認めてもらいたくて行動しているのでしょうがねぇ(笑)



QuartusIIもperlやらコアなところはcygwin環境でまわしてるようなので、Windowsよりもlinux版のほうが軽いような気がするのですが,実際のところはどうなんですかね….


閑話休題.本編は以降へ...



[FPGA][NiosII][ACM] 高速化・ベンチマーク測定

ALTERAで行こう!で紹介されている,暫定公開 → 軽量動画フォーマットの研究のアレをナニして楽しよう!検証していこうという企画であります(しれっ\管理日誌のほうに,マイコン側の展開高速化や時期verの構想の話があったりするので,NEEKで動画するならこれだろう,と.

全部ソフトだと話しにならないので,YUV210→ RGB変換処理をカスタム命令にしてみまんた.コアは/fを適用し,MMU/MPUは殺してあります.D-cacheも有効にして期待が高まりますが...?

緒言ぽいもの:最適化あり,Nios/f・Icache/Dcacheあり,Release向けbuild

~ばらつきは数ミリオーダで存在する模様.\この日記を書きながらコードを見てると,明らかに要らん処理がはいってます.全く同じ処理の比較ではないので,これはちょっとイカサマくさいです.それでも結構なステップを端折ることができているでしょう.

以下,測定結果.画像はどこかの果物の写真を拾ってきました.それなりに大きいかな?

File dot size MCU size
NATU03~1.ACM(natu03.bmp)405x31551x40
CHERRY~1.ACM(cherry04.bmp)405x31551x40
CHERRY~2.ACM(cherry05.bmp)405x31551x40


【カスタム命令ナシ】

FILE: NATU03~1.ACM, size = 45912

Total Time: 0.233295 seconds (23105221 clock-cycles)

Section %Time (sec) Time (clocks)Occurrences
mcu decode72.50.16922167592802040
dcb decode16.90.03945390741812240

FILE: CHERRY~1.ACM, size = 48362

Total Time: 0.234586 seconds (23232991 clock-cycles)

Section %Time (sec) Time (clocks)Occurrences
mcu decode72.90.17110169453172040
dcb decode17.60.04137409739012240

FILE: CHERRY~2.ACM, size = 33638

Total Time: 0.216018 seconds (21394127 clock-cycles)

Section %Time (sec) Time (clocks)Occurrences
mcu decode71.30.15393152447562040
dcb decode11.30.02450242644812240

【カスタム命令アリ】

FILE: NATU03~1.ACM, size = 45912

Total Time: 0.149199 seconds (14776427 clock-cycles)

Section %Time (sec) Time (clocks)Occurrences
mcu decode58.40.0871486302262040
dcb decode25.40.03783374633812240

FILE: CHERRY~1.ACM, size = 48362

Total Time: 0.15123 seconds (14977634 clock-cycles)

Section %Time (sec) Time (clocks)Occurrences
mcu decode58.90.0890788210802040
dcb decode26.30.03976393770212240

FILE: CHERRY~2.ACM, size = 33638

Total Time: 0.133595 seconds (13231058 clock-cycles)

Section %Time (sec) Time (clocks)Occurrences
mcu decode540.0721071410352040
dcb decode17.20.02301227874412240


カスタム命令

HDL持ってくるの忘れたので,後日貼ります.誰が書いても同じようなモンになりそうですしネw

// Verilog Custom Instruction Template File for Internal Register Logic

module custom_inst_yuv2rgb (
    clk,        // CPU system clock (required for multi-cycle or extended multi-cycle)
    reset,      // CPU master asynchronous active high reset (required for multi-cycle or extended multicycle)
    clk_en,     // Clock-qualifier (required for multi-cycle or extended multi-cycle)
    start,      // Active high signal used to specify that inputs are valid (required for multi-cycle or extended multi-cycle)
    done,       // Active high signal used to notify the CPU that result is valid (required for variable multi-cycle or extended variable multi-cycle)
    dataa,      // Operand A (always required)
    datab,      // Operand B (optional)
    result      // Result (always required)
);

// INPUTS
input clk;
input reset;
input clk_en;
input start;
input [31:0] dataa; // Y(0.255)
input [31:0] datab; // u(-128..127), v(-128..127) 

// OUTPUTS
output reg done;
output reg [31:0] result;



// temporary regs.
reg signed [17:0] calc_R, calc_G, calc_B ;

// to refer parameter
wire signed [8:0] dy_i ;
wire signed [8:0] du_i, dv_i ;

// internal regs/wires
reg [3:0] conv_state ;

wire signed [17:0] d_Rv1, d_Rv2 ;
wire signed [17:0] d_Gu1, d_Gv2 ;
wire signed [17:0] d_Bu1, d_Bu2 ;

localparam STAT_IDLE   = 4'b0001;
localparam STAT_STEP1  = 4'b0010;
localparam STAT_STEP2  = 4'b0100;
localparam STAT_FINISH = 4'b1000;

assign dy_i = { 1'b0,      dataa[ 7: 0] } ; // unsigned 0..255
assign du_i = { datab[15], datab[15: 8] } ; // signed -128..127
assign dv_i = { datab[7],  datab[ 7: 0] } ; // signed -128..127


    always @ (posedge reset or posedge clk) begin
        if (reset) begin
            conv_state <= STAT_IDLE ;
            done   <= 1'b0 ;
            result <= 32'h00000000 ;
            calc_R <= 18'h0;
            calc_G <= 18'h0;
            calc_B <= 18'h0;
        end
        else begin
            if (clk_en) begin
                case (conv_state)
                    STAT_IDLE:  begin
                            if (start) begin
                                calc_R[17:0] <= {1'b0, dy_i[8:0], 8'h0000 } ;
                                calc_G[17:0] <= {1'b0, dy_i[8:0], 8'h0000 } ;
                                calc_B[17:0] <= {1'b0, dy_i[8:0], 8'h0000 } ;
                                conv_state <= STAT_STEP1 ;
                            end
                            else begin
                                done <= 1'b0 ;
                            end
                        end
                    STAT_STEP1: begin
                                calc_R <= calc_R + d_Rv1 ;
                                calc_G <= calc_G + d_Gu1 ;
                                calc_B <= calc_B + d_Bu1 ;
                                conv_state <= STAT_STEP2 ;
                        end
                    STAT_STEP2: begin
                                calc_R <= calc_R + d_Rv2 ;
                                calc_G <= calc_G + d_Gv2 ;
                                calc_B <= calc_B + d_Bu2 ;
                                conv_state <= STAT_FINISH ;
                        end
                    STAT_FINISH: begin
                                done <= 1'b1 ;
                                result[31:24] <= 8'h0 ;

                                if (calc_R[17]==1'b1)
                                    result[23:16] <=  8'h0 ;
                                else if (calc_R[16]==1'b1)
                                    result[23:16] <= 8'hFF ;
                                else
                                    result[23:16] <= calc_R[15:8] ;

                                if (calc_G[17]==1'b1)
                                    result[15: 8] <=  8'h0 ;
                                else if (calc_G[16]==1'b1)
                                    result[15: 8] <= 8'hFF ;
                                else
                                    result[15: 8] <= calc_G[15:8] ;

                                if (calc_B[17]==1'b1)
                                    result[ 7: 0] <=  8'h0 ;
                                else if (calc_B[16]==1'b1)
                                    result[ 7: 0] <= 8'hFF ;
                                else
                                    result[ 7: 0] <= calc_B[15:8] ;

                                conv_state <= STAT_IDLE ;
                        end
                endcase
            end
        end
    end




assign d_Rv1 = { dv_i[8], dv_i[8:0], 8'h00 };   // v x 256
MUL_RV  MUL_RV_inst (   // 9'h067;
    .clken ( clk_en ),
    .clock ( clk ),
    .dataa  ( dv_i ),
    .result ( d_Rv2 )
    );

MUL_GU  MUL_GU_inst(    // 9'hfa8;
    .clken ( clk_en ),
    .clock ( clk ),
    .dataa  ( du_i ) ,
    .result ( d_Gu1 )
    );

MUL_GV  MUL_GV_inst(    // 9'hf49;
    .clken ( clk_en ),
    .clock ( clk ),
    .dataa  ( dv_i ) ,
    .result ( d_Gv2 )
    );


assign d_Bu1 = {du_i[8], du_i[8:0], 8'h00 };    // u x 256
MUL_BU  MUL_BU_inst (   // 9'h0c6;
    .clken ( clk_en ),
    .clock ( clk ),
    .dataa  ( du_i ),   // signed 9bit (mul 198. you must mult 256 and add)
    .result ( d_Bu2 )       // 18bit
    );


endmodule

※MUL_xxは乗算器をMega Wizardで生成しました.それぞれ9bitx9bit符号有りの乗算器です.



ソースコード(抜粋)

未掲載部分はオリジナルと同一.ベータ版というか動作保障ナシで各自自己責任でお使いください.\改造ポイントと,計測ポイントを示すために貼っておきます.ノークレームノーリターンでお願いします.(?)

UJIYA_CI_YUV2RGB_MACRO は 引数2つのカスタムインストラクション番号0呼び出しです.これももってくるの忘れたので,後日追記しておきます.


// カスタム命令呼び出しのマクロ
#define UJIYA_CI_YUV2RGB_N      0x00
#define UJIYA_CI_YUV2RGB_MACRO(n,Y,UV)\
    __builtin_custom_inii( UJIYA_CI_YUV2RGB_N, (Y),(UV))

int mcu_decode (                    // MCUのワードサイズを返す 
        unsigned short *pMCU,       // MCUデータポインタ 
        mcu_rgb32 *pP,              // 展開先ポインタ 
        int cmode                   // 出力カラーモード 
    )
{
    int i,n,x,y;
    int r,g,b;
    int tu[16],tv[16],ty[16], *yy;
    int c,mcu_n;


    // U成分とV成分の展開 
PERF_BEGIN( PERFORMANCE_COUNTER_BASE, 2);
    c = dcb_decode(pMCU, tu, -128);
PERF_END(PERFORMANCE_COUNTER_BASE, 2);
    pMCU  += c;
    mcu_n  = c;
PERF_BEGIN( PERFORMANCE_COUNTER_BASE, 2);
    c = dcb_decode(pMCU, tv, -128);
PERF_END(PERFORMANCE_COUNTER_BASE, 2);
    pMCU  += c;
    mcu_n += c;

    // Y成分の展開とRGB変換 
    i = 0; // U,Vのindex
    for(n=0 ; n<4 ; n++) {
PERF_BEGIN( PERFORMANCE_COUNTER_BASE, 2);
        c = dcb_decode(pMCU, ty, 0);
PERF_END(PERFORMANCE_COUNTER_BASE, 2);
        pMCU  += c;
        mcu_n += c;

        yy = &ty[0] ;   // yyはシーケンシャル
        for(y=0 ; y<4 ; y++) {          // YUV420をRGBに変換 
            for(x=0 ; x<4 ; x++) {
#if 1  /* ここでカスタム命令とソフト処理とを切り替える */
mcu_rgb32 d_rgb ;
                d_rgb = UJIYA_CI_YUV2RGB_MACRO(0, *yy, (((tu[i]&0xFF)<<8) | (tv[i]&0xFF))) ; // & bitmask[cmode];
                *pP++ =  d_rgb;
#else
                r  = (256 * (*yy)               + 359 * tv[i]) >> 8;
                g  = (256 * (*yy) -  88 * tu[i] - 183 * tv[i]) >> 8;
                b  = (256 * (*yy) + 454 * tu[i]              ) >> 8;

                if (r < 0) {
                    r = 0;
                } else {
                    r += dither[cmode][y][x];
                    if (r > 255) r = 255;
                }
                if (g < 0) {
                    g = 0;
                } else {
                    g += dither[cmode][y][x];
                    if (g > 255) g = 255;
                }
                if (b < 0) {
                    b = 0;
                } else {
                    b += dither[cmode][y][x];
                    if (b > 255) b = 255;
                }
                *pP++ = mcu_rgb32_pack(r, g, b) & bitmask[cmode];   // ビット落としをシミュレート 
#endif
                // Y,U,V index更新. U,Vはxが2dot単位だから,展開してしまったほうが分岐が減ると思うのね.
                yy++ ;
                if ((x & 3) == 3) {
                    i-- ;
                }
                else if (x & 1) {
                    i++;
                }
            }
            if (y & 1) {
                i += 4 ;
            }

            pP += 4;    // 4dot進める
        }
        if (n==1) {
            i -= 2 ;
            pP -= 4 ; // 左下の座標へ移動.
        }
        else {
            i -= 6 ;
            pP += (4 - 8*4) ; // 右上の座標へ移動.
        }
    }

    return (mcu_n);
}

int load_acm2mem( char* fname, gr_off_bitmap** pOffBitmap )
{
FRESULT res ;
FIL fp ;
UINT rsz ;
ACM_HEADER* acm_header ;
gr_off_bitmap* pRet = NULL;
int x, y, mcu_blocks ;
int xx, yy ;
unsigned int p[8*8]; // p[8][8] ;
unsigned int* pBuft ;
unsigned short* pACM = NULL;
unsigned short* pACM_buf = NULL;
int mcu_words;

    if (pOffBitmap==NULL) { return E_PAR ; }
    *pOffBitmap = NULL ;
    if ((res = f_open (&fp, fname, FA_READ)) != FR_OK) {
        return (res | 0x1000);
    }
    pACM = malloc(fp.fsize);
    if (!pACM){ goto ERROR ; }// error
    res = f_read(&fp, pACM, fp.fsize, &rsz) ;
    if (rsz < fp.fsize) { goto ERROR ;  }  // error
    acm_header = (ACM_HEADER*)pACM ;

    pRet = (gr_off_bitmap*)malloc( sizeof(gr_off_bitmap) + acm_header->width * acm_header->height * sizeof(unsigned int)  ) ;
    if (pRet==NULL) { goto ERROR; }
    pRet->width  = acm_header->width ;
    pRet->height = acm_header->height ;
    pRet->depth  = 4;   // とりあえずRGB888で固定する.
    pBuft = (unsigned int*)&pRet->data[0] ;

    pACM_buf = &acm_header->data[0] ;
    mcu_blocks = 0 ;

    PERF_RESET( PERFORMANCE_COUNTER_BASE );
    PERF_START_MEASURING( PERFORMANCE_COUNTER_BASE );

    for(y=0 ; y< acm_header->height ; y+=8) {
        for(x=0 ; x< acm_header->width ; x+=8) {
            if (++mcu_blocks > acm_header->wNumOfMCU) {
            }
        // デコードしてみる 
PERF_BEGIN( PERFORMANCE_COUNTER_BASE, 1);
            mcu_words = mcu_decode(pACM_buf, &p[0], 0); // output color mode=0(as RGB888)
PERF_END(PERFORMANCE_COUNTER_BASE, 1);
            pACM_buf += mcu_words ;
            for(yy=0 ; yy<8 ; yy++) {
                for(xx=0 ; xx<8 ; xx++) {
                    if((x+xx)< acm_header->width && (y+yy)< acm_header->height) {
                        pBuft[ (y+yy) * acm_header->width + (x+xx) ] = p[yy*8+xx] ;// p[yy][xx] ;
                    }
                }
            }
        }
    }
    PERF_STOP_MEASURING(PERFORMANCE_COUNTER_BASE) ;
    perf_print_formatted_report( (void*)PERFORMANCE_COUNTER_BASE, alt_get_cpu_freq(), 2, "mcu decode","dcb decode" );

ERROR:
    if (pACM) { free(pACM) ; pACM = NULL; }
    f_close(&fp) ;
    *pOffBitmap = pRet ;

return E_OK ;
}

goto文否定信仰気味でしたが,アセンブラレベルで想像できるときや,ヘタに書いて可読性が下がるとき・個人でいじくってるときの逃げなどでは大いに活用してよいと思います.これもC++の例外を触ってから考えが変わりました...\制御できる(問題を全て把握した上での)使用は同じだろう,と.あまりグダグダ書くと叩かれそうなのでこの辺で(笑)


少しだけ考察(未検証)

YUVの配列要素番号の算出を,オリジナルでは(乗算+加算)としてあります.とりあえず乗算は悪という思想で展開してみましたが,サイクル数を見るとcycloneIIのコア/fでは4サイクル程度*1….\命令フェッチ考えるとヘタにステップが増えるほうが遅くなるという罠.フルアセンブラでチマチマ最適化してやるのがベストですね.レジスタ変数にすればある程度スタックアクセスも減らせるかもですね.

ステップ数,メインメモリからのデータ読み出し,命令読み出しを考慮して最適化しないと,実際の最速を得ることができないでしょう.特にI/Dどちらもcache有効なコアなので,単純に演算速度だけが聞いてくるわけでもないですネ.\いかにコア内部でデータ移動を閉じられるかがカギですね.


gccのインラインアセンブラか….AVRでちょっとかじった程度だからなぁ….




  • Update 2.Feb.2009.
    • "後日掲載"アイテムを追加(HDL/マクロ/bmpサイズ)

*1 : 要確認.HW乗算器使用でコレだった気がする.後日データシート引用します..どこいったっけ...