FC2カウンター FPGAの部屋 gcc の最適化と自動ベクトル化を使用した時の性能
FC2ブログ

FPGAやCPLDの話題やFPGA用のツールの話題などです。 マニアックです。 日記も書きます。

FPGAの部屋

FPGAの部屋の有用と思われるコンテンツのまとめサイトを作りました。Xilinx ISEの初心者の方には、FPGAリテラシーおよびチュートリアルのページをお勧めいたします。

gcc の最適化と自動ベクトル化を使用した時の性能

(2015/07/11:タイトルが間違っていたので変更しました)
2015/07/12:修正 時間計測にバグがあったので、全面的に記事を修正しました。miyox さん、ありがとうございました)

ZynqのSIMDエンジンNEONについての資料”の続きというか、その資料を参考にして自動ベクトル化を行う gcc のオプションを使用してラプラシアンフィルタをコンパイルし、その性能を確認した。

実行環境はZYBOで、Dual Cortex-A9 650 MHz プロセッサを持ち、Ubuntu 14.04 LTS が動作している。

NEON最適化ライブラリを使用するだけでなく、今まで最適オプション(-O?)を入れてなかったので、各種の最適オプションを入れた時の性能差も測定した。なお、gcc のバージョンは4.6 だった。

なお、ツイッターでいろいろとアドバイス、ご助言を頂いた。ありがとうございます。

性能を測定するラプラシアンフィルタのソフトウェア実装は4つとする。
最初は、”ZYBO用Ubuntu Linux のカメラで撮った画像にラプラシアンフィルタをかける”に貼ってある laplacian_filter.c のgettimeofday() をラプラシアンフィルタの処理の経過時間のみを測定するように移動させたCソースコードとする。これを laplacian_filter1 という実行ファイルにコンパイルする。このCソースコードはこの記事の最後に貼っておく。

2番目と3番目は、”ラプラシアンフィルタのソフトウェアとハードウェアの速度の比較”にCソースコードが貼ってある laplacian_filter2.c と laplacian_filter3.c とする。これらはmemcpy() を使用したVivado HLS で使用している記述となっている。

4番目は、”ラプラシアンフィルタのソフトウェアとハードウェアの速度の比較3”にCソースコードを貼った laplacian_filter4.c を使用する。これは、Vivado HLS 2014.4 で最速だった。

最初に gcc の最適化レベルによる最適化オプションについては、”3.10 Options That Control Optimization”を参照のこと。

最初は、 laplacian_filter1 を -O オプション無し、-O1、-O2、-O3、-Os、NEON最適化ライブラリ・オプションを付けてコンパイルし、実行してラプラシアンフィルタ処理のみの経過時間を測定した。
NEON_2_150705.png
laplacian_filter1 では、-O3 が最速になっている。

次に、 laplacian_filter2 を -O オプション無し、-O1、-O2、-O3、-Os、NEON最適化ライブラリ・オプションを付けてコンパイルし、実行してラプラシアンフィルタ処理のみの経過時間を測定した。
NEON_3_150705.png
laplacian_filter2 では、やはり、-O3 が最高だった。85.1 ms で 100 ms を切っている。memcpy() が影響しているのだろうか?

laplacian_filter3 を -O オプション無し、-O1、-O2、-O3、-Os、NEON最適化ライブラリ・オプションを付けてコンパイルし、実行してラプラシアンフィルタ処理のみの経過時間を測定した。
NEON_4_150705.png
laplacian_filter3 でもlaplacian_filter2 と同様の傾向があった。

laplacian_filter4 を -O オプション無し、-O1、-O2、-O3、-Os、NEON最適化ライブラリ・オプションを付けてコンパイルし、実行してラプラシアンフィルタ処理のみの経過時間を測定した。
NEON_5_150705.png
やはり、-O3 が速い。

gcc の最適化オプションを変更した結果は最適化の度合いによって、ラプラシアンフィルタの処理時間がかなり変動するという不安定な結果になった。その中でも laplacian_filter1 は結構安定した結果になっている。やはり、ソフトウェアとして書いているので、gcc も最適化がかけやすかったのだろうか?

次に、本来の目的であるNEON最適化ライブラリ・オプションなのだが、-O3 を付けてコンパイルしていた。これも -O3 が適当かどうかを調べるために -O オプション無し、-O1、-O2、-O3、-Os オプションを付けてラプラシアンフィルタ処理時間を測定した。

laplacian_filter1 の場合の結果を下に示す。
NEON_6_150705.png
-O1 の場合が最速だった。

laplacian_filter2 の場合の結果を下に示す。
NEON_7_150705.png
-O2 オプションが最速で、78.3 ms と Vivado HLSでCソースコードのみ書き換えた(1.)の 80.0 ms を上回る性能だった。

laplacian_filter3 の場合の結果を下に示す。
NEON_8_150705.png
-O2 オプションが最速で、77.7 ms と Vivado HLSでCソースコードのみ書き換えた(1.)の 80.0 ms を上回る性能だった。

laplacian_filter4 の場合の結果を下に示す。
NEON_9_150705.png
-O3 の場合が最速だった。 -O2 も速い。

全体を通して最速だったのは、laplacian_filter3 の 自動ベクトル化、-O2 を付けた場合だった。ラプラシアンフィルタ処理時間は、約 77.7 ms だった。これは、Vivado HLSでCソースコードのみ書き換えた(1.)の 80.0 ms を上回る性能だった。
これだと、”Vivado HLS 2014.4 で合成したラプラシアンフィルタIPの高速化14(性能が最大になる設定を探る7、まとめ)”の1. の約 1.03 倍の処理速度となり、Vivado HLSでお手軽にハードウェアしたら性能が低下してしまう。但し、11. のVivado HLS 2014.4 でハードウェアにした時とのラプラシアンフィルタ処理時間の差は、約 5.08 倍となった。

2015/07/13:追加
objdump -S -d <実行ファイル名> | grep "vmov" -c

コマンドでNEON命令を使用しているかどうか?を調べた。使用数が 0 だとNEON命令を使用しいないことになる。0 以外だとNEON命令を使用している。
NEON_35_150712.png 

NEON_36_150712.png 

NEON命令を使用しいる実行ファイル名を下の表で赤字で示す。

2015/07/07:追加 AXI4-Stream版 laplacian filterのCソースコードの違いと最適化オプションの違いによる処理時間の表を追加する。使用しているgcc のバージョンは 4.6 です。)
NEON_10_150706.png
最速の 77.7 ms は自動ベクトル化されて、NEONを使用している実行ファイルだった。

最後に、laplacian_filter1 として実行した laplacian_filter.c を下に貼っておく。
2015/07/12:修正 時間計測にバグがあったので、修正しました。miyox さん、ありがとうございました)(2015/08/02:修正

// laplacian_filter.c
// RGBをYに変換後にラプラシアンフィルタを掛ける。
// ピクセルのフォーマットは、{8'd0, R(8bits), G(8bits), B(8bits)}, 1pixel = 32bits
// 2013/09/16
// 2014/12/04 : ZYBO用Ubuntu Linux のUIO用に変更

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#include <sys/mman.h>
#include <fcntl.h>

#define HORIZONTAL_PIXEL_WIDTH    800
#define VERTICAL_PIXEL_WIDTH    600
#define ALL_PIXEL_VALUE    (HORIZONTAL_PIXEL_WIDTH*VERTICAL_PIXEL_WIDTH)

#define CMA_START_ADDRESS           0x17800000
#define VIDEO_BUFFER_START_ADDRESS  0x18000000  // Limit 0x18800000, 800*600*4 = 2MBytes * 2
#define LAPLACIAN_FILTER_ADDRESS    0x18200000  // 800*600*4 = 0x1d4c00

int laplacian_fil(int x0y0, int x1y0, int x2y0, int x0y1, int x1y1, int x2y1, int x0y2, int x1y2, int x2y2);
int conv_rgb2y(int rgb);
int chkhex(char *str);

int main()
{
    volatile unsigned int *fb_addr, *next_frame_addr;
    int lap_fil_val;
    int x, y;
    struct timeval start_time, temp1, temp2, end_time;
    unsigned int line_buf[3][HORIZONTAL_PIXEL_WIDTH];
    int a, b;
    int fl, sl, tl;
    int fd0, fd3;
    volatile unsigned *bmdc_axi_lites;
    volatile unsigned int *frame_buffer;

    // gettimeofday(&start_time, NULL);    // プログラム起動時の時刻を記録

    // frame_buffer にマップする
    fd3 = open("/dev/uio3", O_RDWR); // Frame Buffer
    if (fd3 < 1){
        fprintf(stderr, "/dev/uio3 open error\n");
        exit(-1);
    }
    frame_buffer = (volatile unsigned *)mmap(NULL, 0x1000000, PROT_READ|PROT_WRITE, MAP_SHARED, fd3, 0);
    if (!frame_buffer){
        fprintf(stderr, "frame_buffer mmap error\n");
        exit(-1);
    }
    fb_addr = (volatile unsigned int *)((unsigned int)frame_buffer + (unsigned int)(VIDEO_BUFFER_START_ADDRESS-CMA_START_ADDRESS));

    // ラプラシアンフィルタの結果を入れておくフレーム・バッファ
    next_frame_addr = (volatile unsigned int *)((unsigned int)frame_buffer + (unsigned int)(LAPLACIAN_FILTER_ADDRESS-CMA_START_ADDRESS));

    gettimeofday(&start_time, NULL);
    
    // RGB値をY(輝度成分)のみに変換し、ラプラシアンフィルタを掛けた。
    for (y=0; y<VERTICAL_PIXEL_WIDTH; y++){
        for (x=0; x<HORIZONTAL_PIXEL_WIDTH; x++){
            if (y==0 || y==VERTICAL_PIXEL_WIDTH-1){ // 縦の境界の時の値は0とする
                lap_fil_val = 0;
            }else if (x==0 || x==HORIZONTAL_PIXEL_WIDTH-1){ // 横の境界の時も値は0とする
                lap_fil_val = 0;
            }else{
                if (y == 1 && x == 1){ // 最初のラインの最初のピクセルでは2ライン分の画素を読み出す
                    for (a=0; a<2; a++){ // 2ライン分
                        for (b=0; b<HORIZONTAL_PIXEL_WIDTH; b++){ // ライン
                            line_buf[a][b] = fb_addr[(a*HORIZONTAL_PIXEL_WIDTH)+b];
                            line_buf[a][b] = conv_rgb2y(line_buf[a][b]);
                        }
                    }
                }
                if (x == 1) {    // ラインの最初なので、2つのピクセルを読み込む
                    for (b=0; b<2; b++){ // ライン
                        line_buf[(y+1)%3][b] = fb_addr[((y+1)*HORIZONTAL_PIXEL_WIDTH)+b];
                        // (y+1)%3 は、使用済みのラインがに読み込む、y=2 の時 line[0], y=3の時 line[1], y=4の時 line[2]
                        line_buf[(y+1)%3][b] = conv_rgb2y(line_buf[(y+1)%3][b]);
                    }
                }
                
                // 1つのピクセルを読み込みながらラプラシアン・フィルタを実行する
                line_buf[(y+1)%3][x+1] = fb_addr[((y+1)*HORIZONTAL_PIXEL_WIDTH)+(x+1)];
                // (y+1)%3 は、使用済みのラインがに読み込む、y=2 の時 line[0], y=3の時 line[1], y=4の時 line[2]
                line_buf[(y+1)%3][x+1] = conv_rgb2y(line_buf[(y+1)%3][x+1]);
                
                fl = (y-1)%3;    // 最初のライン, y=1 012, y=2 120, y=3 201, y=4 012
                sl = y%3;        // 2番めのライン
                tl = (y+1)%3;    // 3番目のライン
                lap_fil_val = laplacian_fil(line_buf[fl][x-1], line_buf[fl][x], line_buf[fl][x+1], line_buf[sl][x-1], line_buf[sl][x], line_buf[sl][x+1], line_buf[tl][x-1], line_buf[tl][x], line_buf[tl][x+1]);
            }
            // ラプラシアンフィルタ・データの書き込み
            next_frame_addr[(y*HORIZONTAL_PIXEL_WIDTH)+x] = (lap_fil_val<<16)+(lap_fil_val<<8)+lap_fil_val ;
            // printf("x = %d  y = %d", x, y);
        }
     }

    gettimeofday(&end_time, NULL);
    
    munmap((void *)frame_buffer, 0x1000000);
 
   // ラプラシアンフィルタ表示画面に切り替え
    // Bitmap Display Controller AXI4 Lite Slave (UIO0)
    fd0 = open("/dev/uio0", O_RDWR); // bitmap_display_controller axi4 lite
    if (fd0 < 1){
        fprintf(stderr, "/dev/uio0 open error\n");
        exit(-1);
    }
    bmdc_axi_lites = (volatile unsigned *)mmap(NULL, 0x10000, PROT_READ|PROT_WRITE, MAP_SHARED, fd0, 0);
    if (!bmdc_axi_lites){
        fprintf(stderr, "bmdc_axi_lites mmap error\n");
        exit(-1);
    }
    bmdc_axi_lites[0] = (unsigned int)LAPLACIAN_FILTER_ADDRESS; // Bitmap Display Controller start (ラプラシアンフィルタ表示画面のアドレス)
    munmap((void *)bmdc_axi_lites, 0x10000);
    
    //gettimeofday(&end_time, NULL);
    if (end_time.tv_usec < start_time.tv_usec) {
        printf("total time = %ld.%06ld sec\n", end_time.tv_sec - start_time.tv_sec - 11000000 + end_time.tv_usec - start_time.tv_usec);
    }
    else {
        printf("total time = %ld.%06ld sec\n", end_time.tv_sec - start_time.tv_sec, end_time.tv_usec - start_time.tv_usec);
    }
    return(0);
}

// RGBからYへの変換
// RGBのフォーマットは、{8'd0, R(8bits), G(8bits), B(8bits)}, 1pixel = 32bits
// 輝度信号Yのみに変換する。変換式は、Y =  0.299R + 0.587G + 0.114B
// "YUVフォーマット及び YUV<->RGB変換"を参考にした。http://vision.kuee.kyoto-u.ac.jp/~hiroaki/firewire/yuv.html
// 2013/09/27 : float を止めて、すべてint にした
int conv_rgb2y(int rgb){
    int r, g, b, y_f;
    int y;

    b = rgb & 0xff;
    g = (rgb>>8) & 0xff;
    r = (rgb>>16) & 0xff;

    y_f = 77*r + 150*g + 29*b; //y_f = 0.299*r + 0.587*g + 0.114*b;の係数に256倍した
    y = y_f >> 8// 256で割る

    return(y);
}

// ラプラシアンフィルタ
// x0y0 x1y0 x2y0 -1 -1 -1
// x0y1 x1y1 x2y1 -1  8 -1
// x0y2 x1y2 x2y2 -1 -1 -1
int laplacian_fil(int x0y0, int x1y0, int x2y0, int x0y1, int x1y1, int x2y1, int x0y2, int x1y2, int x2y2)
{
    int y;

    y = -x0y0 -x1y0 -x2y0 -x0y1 +8*x1y1 -x2y1 -x0y2 -x1y2 -x2y2;
    if (y<0)
        y = 0;
    else if (y>255)
        y = 255;
    return(y);
}

// 文字列が16進数かを調べる
int chkhex(char *str){
    while (*str != '\0'){
        if (!isxdigit(*str))
            return 0;
        str++;
    }
    return 1;
}

  1. 2015年07月06日 04:24 |
  2. Zynq
  3. | トラックバック:0
  4. | コメント:6

コメント

こんにちは。興味深く拝見させて頂いています。前後をちゃんと読んでいないのですが、NEONのAuto vectorizationのオプションを解除したときの-Oを変化させた時の結果はどうでしょうか?
他アーキテクチャでの経験談ですが、GCCのAuto-vectorizationはイマイチで、NEONの命令を出さないばかりか、逆に冗長なコードを出すことがありました。
objdumpやnmを使って、バイナリサイズや、本当にNEONの命令が出力されているかチェックしてみてください。
  1. 2015/07/06(月) 23:30:28 |
  2. URL |
  3. masayuki #-
  4. [ 編集 ]

コメントありがとうございます。

NEONのAuto vectorizationのオプションというのは、-ftree-vectorize オプションのことでしょうか?これを外して、-Oを変化させれば良いですか?
  1. 2015/07/07(火) 04:00:56 |
  2. URL |
  3. marsee #f1oWVgn2
  4. [ 編集 ]

はい、そうです。-ftree-vectorizeを追加しただけで冗長な命令が大量に生成されたときがあって、性能低下の原因を掴むのに苦労した記憶があります。
-ftree-vectorizeのON/OFFで、生成される命令が変わるかどうか、見てみると良いと思います。
  1. 2015/07/07(火) 08:00:44 |
  2. URL |
  3. masayuki #-
  4. [ 編集 ]

了解しました。やってみます。
ありがとうございました。
  1. 2015/07/07(火) 08:09:06 |
  2. URL |
  3. marsee #f1oWVgn2
  4. [ 編集 ]

こんばんわ。お久しぶりです。

http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dht0002a/ch01s04s03.html

vector化されたかどうかが見れる -ftree-vectorizer-verbose=1 なるオプションがあるみたいですよ。コンパイラが悩まなくて済むようなヒントも必要なようですね。
  1. 2015/07/08(水) 23:47:39 |
  2. URL |
  3. くり #195Lvy4Y
  4. [ 編集 ]

くりさん、おはようございます。お久しぶりです。
情報ありがとうございました。ベクトル化したかどうかを見るのが楽になりました。
ベクトル化したからと言って必ずしも速くならないのがミソのようですね。。。

教えてもらったオプションを使って、ブログ記事を書きました。ありがとうございました。
http://marsee101.blog19.fc2.com/blog-entry-3195.html
  1. 2015/07/09(木) 03:40:05 |
  2. URL |
  3. marsee #f1oWVgn2
  4. [ 編集 ]

コメントの投稿


管理者にだけ表示を許可する

トラックバック URL
http://marsee101.blog.fc2.com/tb.php/3193-8261f7e5
この記事にトラックバックする(FC2ブログユーザー)